diff options
Diffstat (limited to 'editor')
-rw-r--r-- | editor/action_map_editor.cpp | 775 | ||||
-rw-r--r-- | editor/action_map_editor.h | 139 | ||||
-rw-r--r-- | editor/editor_settings_dialog.cpp | 2 | ||||
-rw-r--r-- | editor/event_listener_line_edit.cpp | 131 | ||||
-rw-r--r-- | editor/event_listener_line_edit.h | 74 | ||||
-rw-r--r-- | editor/input_event_configuration_dialog.cpp | 700 | ||||
-rw-r--r-- | editor/input_event_configuration_dialog.h | 124 | ||||
-rw-r--r-- | editor/plugins/input_event_editor_plugin.cpp | 3 | ||||
-rw-r--r-- | editor/project_settings_editor.cpp | 1 | ||||
-rw-r--r-- | editor/scene_tree_dock.cpp | 87 | ||||
-rw-r--r-- | editor/scene_tree_dock.h | 10 | ||||
-rw-r--r-- | editor/scene_tree_editor.cpp | 9 | ||||
-rw-r--r-- | editor/scene_tree_editor.h | 2 |
13 files changed, 1153 insertions, 904 deletions
diff --git a/editor/action_map_editor.cpp b/editor/action_map_editor.cpp index 4d48dd5ac5..9b1ff78727 100644 --- a/editor/action_map_editor.cpp +++ b/editor/action_map_editor.cpp @@ -28,777 +28,12 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "action_map_editor.h" - -#include "core/input/input_map.h" -#include "core/os/keyboard.h" +#include "editor/action_map_editor.h" #include "editor/editor_scale.h" -#include "scene/gui/separator.h" - -bool EventListenerLineEdit::_is_event_allowed(const Ref<InputEvent> &p_event) const { - const Ref<InputEventMouseButton> mb = p_event; - const Ref<InputEventKey> k = p_event; - const Ref<InputEventJoypadButton> jb = p_event; - const Ref<InputEventJoypadMotion> jm = p_event; - - return (mb.is_valid() && (allowed_input_types & INPUT_MOUSE_BUTTON)) || - (k.is_valid() && (allowed_input_types & INPUT_KEY)) || - (jb.is_valid() && (allowed_input_types & INPUT_JOY_BUTTON)) || - (jm.is_valid() && (allowed_input_types & INPUT_JOY_MOTION)); -} - -void EventListenerLineEdit::gui_input(const Ref<InputEvent> &p_event) { - const Ref<InputEventMouseMotion> mm = p_event; - if (mm.is_valid()) { - LineEdit::gui_input(p_event); - return; - } - - // Allow mouse button click on the clear button without being treated as an event. - const Ref<InputEventMouseButton> b = p_event; - if (b.is_valid() && _is_over_clear_button(b->get_position())) { - LineEdit::gui_input(p_event); - return; - } - - // First event will be an event which is used to focus this control - i.e. a mouse click, or a tab press. - // Ignore the first one so that clicking into the LineEdit does not override the current event. - // Ignore is reset to true when the control is unfocused. - if (ignore) { - ignore = false; - return; - } - - accept_event(); - if (!p_event->is_pressed() || p_event->is_echo() || p_event->is_match(event) || !_is_event_allowed(p_event)) { - return; - } - - event = p_event; - set_text(event->as_text()); - emit_signal("event_changed", event); -} - -void EventListenerLineEdit::_on_text_changed(const String &p_text) { - if (p_text.is_empty()) { - clear_event(); - } -} - -void EventListenerLineEdit::_on_focus() { - set_placeholder(TTR("Listening for input...")); -} - -void EventListenerLineEdit::_on_unfocus() { - ignore = true; - set_placeholder(TTR("Filter by event...")); -} - -Ref<InputEvent> EventListenerLineEdit::get_event() const { - return event; -} - -void EventListenerLineEdit::clear_event() { - if (event.is_valid()) { - event = Ref<InputEvent>(); - set_text(""); - emit_signal("event_changed", event); - } -} - -void EventListenerLineEdit::set_allowed_input_types(int input_types) { - allowed_input_types = input_types; -} - -int EventListenerLineEdit::get_allowed_input_types() const { - return allowed_input_types; -} - -void EventListenerLineEdit::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: { - connect("text_changed", callable_mp(this, &EventListenerLineEdit::_on_text_changed)); - connect("focus_entered", callable_mp(this, &EventListenerLineEdit::_on_focus)); - connect("focus_exited", callable_mp(this, &EventListenerLineEdit::_on_unfocus)); - set_right_icon(get_theme_icon(SNAME("Keyboard"), SNAME("EditorIcons"))); - set_clear_button_enabled(true); - } break; - } -} - -void EventListenerLineEdit::_bind_methods() { - ADD_SIGNAL(MethodInfo("event_changed", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"))); -} - -EventListenerLineEdit::EventListenerLineEdit() { - set_caret_blink_enabled(false); - set_placeholder(TTR("Filter by event...")); -} - -///////////////////////////////////////// - -// Maps to 2*axis if value is neg, or 2*axis+1 if value is pos. -static const char *_joy_axis_descriptions[(size_t)JoyAxis::MAX * 2] = { - TTRC("Left Stick Left, Joystick 0 Left"), - TTRC("Left Stick Right, Joystick 0 Right"), - TTRC("Left Stick Up, Joystick 0 Up"), - TTRC("Left Stick Down, Joystick 0 Down"), - TTRC("Right Stick Left, Joystick 1 Left"), - TTRC("Right Stick Right, Joystick 1 Right"), - TTRC("Right Stick Up, Joystick 1 Up"), - TTRC("Right Stick Down, Joystick 1 Down"), - TTRC("Joystick 2 Left"), - TTRC("Left Trigger, Sony L2, Xbox LT, Joystick 2 Right"), - TTRC("Joystick 2 Up"), - TTRC("Right Trigger, Sony R2, Xbox RT, Joystick 2 Down"), - TTRC("Joystick 3 Left"), - TTRC("Joystick 3 Right"), - TTRC("Joystick 3 Up"), - TTRC("Joystick 3 Down"), - TTRC("Joystick 4 Left"), - TTRC("Joystick 4 Right"), - TTRC("Joystick 4 Up"), - TTRC("Joystick 4 Down"), -}; - -String InputEventConfigurationDialog::get_event_text(const Ref<InputEvent> &p_event, bool p_include_device) const { - ERR_FAIL_COND_V_MSG(p_event.is_null(), String(), "Provided event is not a valid instance of InputEvent"); - - String text = p_event->as_text(); - - Ref<InputEventKey> key = p_event; - if (key.is_valid() && key->is_command_or_control_autoremap()) { -#ifdef MACOS_ENABLED - text = text.replace("Command", "Command/Ctrl"); -#else - text = text.replace("Ctrl", "Command/Ctrl"); -#endif - } - Ref<InputEventMouse> mouse = p_event; - Ref<InputEventJoypadMotion> jp_motion = p_event; - Ref<InputEventJoypadButton> jp_button = p_event; - if (jp_motion.is_valid()) { - // Joypad motion events will display slightly differently than what the event->as_text() provides. See #43660. - String desc = TTR("Unknown Joypad Axis"); - if (jp_motion->get_axis() < JoyAxis::MAX) { - desc = RTR(_joy_axis_descriptions[2 * (size_t)jp_motion->get_axis() + (jp_motion->get_axis_value() < 0 ? 0 : 1)]); - } - - text = vformat("Joypad Axis %s %s (%s)", itos((int64_t)jp_motion->get_axis()), jp_motion->get_axis_value() < 0 ? "-" : "+", desc); - } - if (p_include_device && (mouse.is_valid() || jp_button.is_valid() || jp_motion.is_valid())) { - String device_string = _get_device_string(p_event->get_device()); - text += vformat(" - %s", device_string); - } - - return text; -} - -void InputEventConfigurationDialog::_set_event(const Ref<InputEvent> &p_event, bool p_update_input_list_selection) { - if (p_event.is_valid()) { - event = p_event; - - // If the event is changed to something which is not the same as the listener, - // clear out the event from the listener text box to avoid confusion. - const Ref<InputEvent> listener_event = event_listener->get_event(); - if (listener_event.is_valid() && !listener_event->is_match(p_event)) { - event_listener->clear_event(); - } - - // Update Label - event_as_text->set_text(get_event_text(event, true)); - - Ref<InputEventKey> k = p_event; - Ref<InputEventMouseButton> mb = p_event; - Ref<InputEventJoypadButton> joyb = p_event; - Ref<InputEventJoypadMotion> joym = p_event; - Ref<InputEventWithModifiers> mod = p_event; - - // Update option values and visibility - bool show_mods = false; - bool show_device = false; - bool show_phys_key = false; - - if (mod.is_valid()) { - show_mods = true; - mod_checkboxes[MOD_ALT]->set_pressed(mod->is_alt_pressed()); - mod_checkboxes[MOD_SHIFT]->set_pressed(mod->is_shift_pressed()); - mod_checkboxes[MOD_CTRL]->set_pressed(mod->is_ctrl_pressed()); - mod_checkboxes[MOD_META]->set_pressed(mod->is_meta_pressed()); - - autoremap_command_or_control_checkbox->set_pressed(mod->is_command_or_control_autoremap()); - } - - if (k.is_valid()) { - show_phys_key = true; - physical_key_checkbox->set_pressed(k->get_physical_keycode() != Key::NONE && k->get_keycode() == Key::NONE); - - } else if (joyb.is_valid() || joym.is_valid() || mb.is_valid()) { - show_device = true; - _set_current_device(event->get_device()); - } - - mod_container->set_visible(show_mods); - device_container->set_visible(show_device); - physical_key_checkbox->set_visible(show_phys_key); - additional_options_container->show(); - - // Update selected item in input list. - if (p_update_input_list_selection && (k.is_valid() || joyb.is_valid() || joym.is_valid() || mb.is_valid())) { - TreeItem *category = input_list_tree->get_root()->get_first_child(); - while (category) { - TreeItem *input_item = category->get_first_child(); - - if (input_item != nullptr) { - // input_type should always be > 0, unless the tree structure has been misconfigured. - int input_type = input_item->get_parent()->get_meta("__type", 0); - if (input_type == 0) { - return; - } - - // If event type matches input types of this category. - if ((k.is_valid() && input_type == INPUT_KEY) || (joyb.is_valid() && input_type == INPUT_JOY_BUTTON) || (joym.is_valid() && input_type == INPUT_JOY_MOTION) || (mb.is_valid() && input_type == INPUT_MOUSE_BUTTON)) { - // Loop through all items of this category until one matches. - while (input_item) { - bool key_match = k.is_valid() && (Variant(k->get_keycode()) == input_item->get_meta("__keycode") || Variant(k->get_physical_keycode()) == input_item->get_meta("__keycode")); - bool joyb_match = joyb.is_valid() && Variant(joyb->get_button_index()) == input_item->get_meta("__index"); - bool joym_match = joym.is_valid() && Variant(joym->get_axis()) == input_item->get_meta("__axis") && joym->get_axis_value() == (float)input_item->get_meta("__value"); - bool mb_match = mb.is_valid() && Variant(mb->get_button_index()) == input_item->get_meta("__index"); - if (key_match || joyb_match || joym_match || mb_match) { - category->set_collapsed(false); - input_item->select(0); - input_list_tree->ensure_cursor_is_visible(); - return; - } - input_item = input_item->get_next(); - } - } - } - - category->set_collapsed(true); // Event not in this category, so collapse; - category = category->get_next(); - } - } - } else { - // Event is not valid, reset dialog - event = p_event; - event_listener->clear_event(); - event_as_text->set_text(TTR("No Event Configured")); - - additional_options_container->hide(); - input_list_tree->deselect_all(); - _update_input_list(); - } -} - -void InputEventConfigurationDialog::_on_listen_input_changed(const Ref<InputEvent> &p_event) { - // Ignore if invalid, echo or not pressed - if (p_event.is_null() || p_event->is_echo() || !p_event->is_pressed()) { - return; - } - - // Create an editable reference - Ref<InputEvent> received_event = p_event; - // Check what the type is and if it is allowed. - Ref<InputEventKey> k = received_event; - Ref<InputEventJoypadButton> joyb = received_event; - Ref<InputEventJoypadMotion> joym = received_event; - Ref<InputEventMouseButton> mb = received_event; - - int type = 0; - if (k.is_valid()) { - type = INPUT_KEY; - } else if (joyb.is_valid()) { - type = INPUT_JOY_BUTTON; - } else if (joym.is_valid()) { - type = INPUT_JOY_MOTION; - } else if (mb.is_valid()) { - type = INPUT_MOUSE_BUTTON; - } - - if (!(allowed_input_types & type)) { - return; - } - - if (joym.is_valid()) { - float axis_value = joym->get_axis_value(); - if (ABS(axis_value) < 0.9) { - // Ignore motion below 0.9 magnitude to avoid accidental touches - return; - } else { - // Always make the value 1 or -1 for display consistency - joym->set_axis_value(SIGN(axis_value)); - } - } - - if (k.is_valid()) { - k->set_pressed(false); // To avoid serialisation of 'pressed' property - doesn't matter for actions anyway. - // Maintain physical keycode option state - if (physical_key_checkbox->is_pressed()) { - k->set_keycode(Key::NONE); - } else { - k->set_physical_keycode(Key::NONE); - } - } - - Ref<InputEventWithModifiers> mod = received_event; - if (mod.is_valid()) { - mod->set_window_id(0); - } - - // Maintain device selection. - received_event->set_device(_get_current_device()); - - _set_event(received_event); -} - -void InputEventConfigurationDialog::_on_listen_focus_changed() { - if (event_listener->has_focus()) { - set_close_on_escape(false); - } else { - set_close_on_escape(true); - } -} - -void InputEventConfigurationDialog::_search_term_updated(const String &) { - _update_input_list(); -} - -void InputEventConfigurationDialog::_update_input_list() { - input_list_tree->clear(); - - TreeItem *root = input_list_tree->create_item(); - String search_term = input_list_search->get_text(); - - bool collapse = input_list_search->get_text().is_empty(); - - if (allowed_input_types & INPUT_KEY) { - TreeItem *kb_root = input_list_tree->create_item(root); - kb_root->set_text(0, TTR("Keyboard Keys")); - kb_root->set_icon(0, icon_cache.keyboard); - kb_root->set_collapsed(collapse); - kb_root->set_meta("__type", INPUT_KEY); - - for (int i = 0; i < keycode_get_count(); i++) { - String name = keycode_get_name_by_index(i); - - if (!search_term.is_empty() && name.findn(search_term) == -1) { - continue; - } - - TreeItem *item = input_list_tree->create_item(kb_root); - item->set_text(0, name); - item->set_meta("__keycode", keycode_get_value_by_index(i)); - } - } - - if (allowed_input_types & INPUT_MOUSE_BUTTON) { - TreeItem *mouse_root = input_list_tree->create_item(root); - mouse_root->set_text(0, TTR("Mouse Buttons")); - mouse_root->set_icon(0, icon_cache.mouse); - mouse_root->set_collapsed(collapse); - mouse_root->set_meta("__type", INPUT_MOUSE_BUTTON); - - MouseButton mouse_buttons[9] = { MouseButton::LEFT, MouseButton::RIGHT, MouseButton::MIDDLE, MouseButton::WHEEL_UP, MouseButton::WHEEL_DOWN, MouseButton::WHEEL_LEFT, MouseButton::WHEEL_RIGHT, MouseButton::MB_XBUTTON1, MouseButton::MB_XBUTTON2 }; - for (int i = 0; i < 9; i++) { - Ref<InputEventMouseButton> mb; - mb.instantiate(); - mb->set_button_index(mouse_buttons[i]); - String desc = get_event_text(mb, false); - - if (!search_term.is_empty() && desc.findn(search_term) == -1) { - continue; - } - - TreeItem *item = input_list_tree->create_item(mouse_root); - item->set_text(0, desc); - item->set_meta("__index", mouse_buttons[i]); - } - } - - if (allowed_input_types & INPUT_JOY_BUTTON) { - TreeItem *joyb_root = input_list_tree->create_item(root); - joyb_root->set_text(0, TTR("Joypad Buttons")); - joyb_root->set_icon(0, icon_cache.joypad_button); - joyb_root->set_collapsed(collapse); - joyb_root->set_meta("__type", INPUT_JOY_BUTTON); - - for (int i = 0; i < (int)JoyButton::MAX; i++) { - Ref<InputEventJoypadButton> joyb; - joyb.instantiate(); - joyb->set_button_index((JoyButton)i); - String desc = get_event_text(joyb, false); - - if (!search_term.is_empty() && desc.findn(search_term) == -1) { - continue; - } - - TreeItem *item = input_list_tree->create_item(joyb_root); - item->set_text(0, desc); - item->set_meta("__index", i); - } - } - - if (allowed_input_types & INPUT_JOY_MOTION) { - TreeItem *joya_root = input_list_tree->create_item(root); - joya_root->set_text(0, TTR("Joypad Axes")); - joya_root->set_icon(0, icon_cache.joypad_axis); - joya_root->set_collapsed(collapse); - joya_root->set_meta("__type", INPUT_JOY_MOTION); - - for (int i = 0; i < (int)JoyAxis::MAX * 2; i++) { - int axis = i / 2; - int direction = (i & 1) ? 1 : -1; - Ref<InputEventJoypadMotion> joym; - joym.instantiate(); - joym->set_axis((JoyAxis)axis); - joym->set_axis_value(direction); - String desc = get_event_text(joym, false); - - if (!search_term.is_empty() && desc.findn(search_term) == -1) { - continue; - } - - TreeItem *item = input_list_tree->create_item(joya_root); - item->set_text(0, desc); - item->set_meta("__axis", i >> 1); - item->set_meta("__value", (i & 1) ? 1 : -1); - } - } -} - -void InputEventConfigurationDialog::_mod_toggled(bool p_checked, int p_index) { - Ref<InputEventWithModifiers> ie = event; - - // Not event with modifiers - if (ie.is_null()) { - return; - } - - if (p_index == 0) { - ie->set_alt_pressed(p_checked); - } else if (p_index == 1) { - ie->set_shift_pressed(p_checked); - } else if (p_index == 2) { - if (!autoremap_command_or_control_checkbox->is_pressed()) { - ie->set_ctrl_pressed(p_checked); - } - } else if (p_index == 3) { - if (!autoremap_command_or_control_checkbox->is_pressed()) { - ie->set_meta_pressed(p_checked); - } - } - - _set_event(ie); -} - -void InputEventConfigurationDialog::_autoremap_command_or_control_toggled(bool p_checked) { - Ref<InputEventWithModifiers> ie = event; - if (ie.is_valid()) { - ie->set_command_or_control_autoremap(p_checked); - _set_event(ie); - } - - if (p_checked) { - mod_checkboxes[MOD_META]->hide(); - mod_checkboxes[MOD_CTRL]->hide(); - } else { - mod_checkboxes[MOD_META]->show(); - mod_checkboxes[MOD_CTRL]->show(); - } -} - -void InputEventConfigurationDialog::_physical_keycode_toggled(bool p_checked) { - Ref<InputEventKey> k = event; - - if (k.is_null()) { - return; - } - - if (p_checked) { - k->set_physical_keycode(k->get_keycode()); - k->set_keycode(Key::NONE); - } else { - k->set_keycode((Key)k->get_physical_keycode()); - k->set_physical_keycode(Key::NONE); - } - - _set_event(k); -} - -void InputEventConfigurationDialog::_input_list_item_selected() { - TreeItem *selected = input_list_tree->get_selected(); - - // Invalid tree selection - type only exists on the "category" items, which are not a valid selection. - if (selected->has_meta("__type")) { - return; - } - - InputType input_type = (InputType)(int)selected->get_parent()->get_meta("__type"); - - switch (input_type) { - case INPUT_KEY: { - Key keycode = (Key)(int)selected->get_meta("__keycode"); - Ref<InputEventKey> k; - k.instantiate(); - - if (physical_key_checkbox->is_pressed()) { - k->set_physical_keycode(keycode); - k->set_keycode(Key::NONE); - } else { - k->set_physical_keycode(Key::NONE); - k->set_keycode(keycode); - } - - // Maintain modifier state from checkboxes - k->set_alt_pressed(mod_checkboxes[MOD_ALT]->is_pressed()); - k->set_shift_pressed(mod_checkboxes[MOD_SHIFT]->is_pressed()); - if (autoremap_command_or_control_checkbox->is_pressed()) { - k->set_command_or_control_autoremap(true); - } else { - k->set_ctrl_pressed(mod_checkboxes[MOD_CTRL]->is_pressed()); - k->set_meta_pressed(mod_checkboxes[MOD_META]->is_pressed()); - } - - _set_event(k, false); - } break; - case INPUT_MOUSE_BUTTON: { - MouseButton idx = (MouseButton)(int)selected->get_meta("__index"); - Ref<InputEventMouseButton> mb; - mb.instantiate(); - mb->set_button_index(idx); - // Maintain modifier state from checkboxes - mb->set_alt_pressed(mod_checkboxes[MOD_ALT]->is_pressed()); - mb->set_shift_pressed(mod_checkboxes[MOD_SHIFT]->is_pressed()); - if (autoremap_command_or_control_checkbox->is_pressed()) { - mb->set_command_or_control_autoremap(true); - } else { - mb->set_ctrl_pressed(mod_checkboxes[MOD_CTRL]->is_pressed()); - mb->set_meta_pressed(mod_checkboxes[MOD_META]->is_pressed()); - } - - // Maintain selected device - mb->set_device(_get_current_device()); - - _set_event(mb, false); - } break; - case INPUT_JOY_BUTTON: { - JoyButton idx = (JoyButton)(int)selected->get_meta("__index"); - Ref<InputEventJoypadButton> jb = InputEventJoypadButton::create_reference(idx); - - // Maintain selected device - jb->set_device(_get_current_device()); - - _set_event(jb, false); - } break; - case INPUT_JOY_MOTION: { - JoyAxis axis = (JoyAxis)(int)selected->get_meta("__axis"); - int value = selected->get_meta("__value"); - - Ref<InputEventJoypadMotion> jm; - jm.instantiate(); - jm->set_axis(axis); - jm->set_axis_value(value); - - // Maintain selected device - jm->set_device(_get_current_device()); - - _set_event(jm, false); - } break; - } -} - -void InputEventConfigurationDialog::_device_selection_changed(int p_option_button_index) { - // Subtract 1 as option index 0 corresponds to "All Devices" (value of -1) - // and option index 1 corresponds to device 0, etc... - event->set_device(p_option_button_index - 1); - event_as_text->set_text(get_event_text(event, true)); -} - -void InputEventConfigurationDialog::_set_current_device(int p_device) { - device_id_option->select(p_device + 1); -} - -int InputEventConfigurationDialog::_get_current_device() const { - return device_id_option->get_selected() - 1; -} - -String InputEventConfigurationDialog::_get_device_string(int p_device) const { - if (p_device == InputMap::ALL_DEVICES) { - return TTR("All Devices"); - } - return TTR("Device") + " " + itos(p_device); -} - -void InputEventConfigurationDialog::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: - case NOTIFICATION_THEME_CHANGED: { - input_list_search->set_right_icon(input_list_search->get_theme_icon(SNAME("Search"), SNAME("EditorIcons"))); - - physical_key_checkbox->set_icon(get_theme_icon(SNAME("KeyboardPhysical"), SNAME("EditorIcons"))); - - icon_cache.keyboard = get_theme_icon(SNAME("Keyboard"), SNAME("EditorIcons")); - icon_cache.mouse = get_theme_icon(SNAME("Mouse"), SNAME("EditorIcons")); - icon_cache.joypad_button = get_theme_icon(SNAME("JoyButton"), SNAME("EditorIcons")); - icon_cache.joypad_axis = get_theme_icon(SNAME("JoyAxis"), SNAME("EditorIcons")); - - _update_input_list(); - } break; - } -} - -void InputEventConfigurationDialog::popup_and_configure(const Ref<InputEvent> &p_event) { - if (p_event.is_valid()) { - _set_event(p_event); - } else { - // Clear Event - _set_event(p_event); - - // Clear Checkbox Values - for (int i = 0; i < MOD_MAX; i++) { - mod_checkboxes[i]->set_pressed(false); - } - - // Enable the Physical Key checkbox by default to encourage its use. - // Physical Key should be used for most game inputs as it allows keys to work - // on non-QWERTY layouts out of the box. - // This is especially important for WASD movement layouts. - physical_key_checkbox->set_pressed(true); - - autoremap_command_or_control_checkbox->set_pressed(false); - - // Select "All Devices" by default. - device_id_option->select(0); - } - - popup_centered(Size2(0, 400) * EDSCALE); -} - -Ref<InputEvent> InputEventConfigurationDialog::get_event() const { - return event; -} - -void InputEventConfigurationDialog::set_allowed_input_types(int p_type_masks) { - allowed_input_types = p_type_masks; -} - -InputEventConfigurationDialog::InputEventConfigurationDialog() { - allowed_input_types = INPUT_KEY | INPUT_MOUSE_BUTTON | INPUT_JOY_BUTTON | INPUT_JOY_MOTION; - - set_title(TTR("Event Configuration")); - set_min_size(Size2i(550 * EDSCALE, 0)); // Min width - - VBoxContainer *main_vbox = memnew(VBoxContainer); - add_child(main_vbox); - - event_as_text = memnew(Label); - event_as_text->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); - event_as_text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); - event_as_text->add_theme_font_override("font", get_theme_font(SNAME("bold"), SNAME("EditorFonts"))); - event_as_text->add_theme_font_size_override("font_size", 18 * EDSCALE); - main_vbox->add_child(event_as_text); - - event_listener = memnew(EventListenerLineEdit); - event_listener->set_h_size_flags(Control::SIZE_EXPAND_FILL); - event_listener->set_stretch_ratio(0.75); - event_listener->connect("event_changed", callable_mp(this, &InputEventConfigurationDialog::_on_listen_input_changed)); - event_listener->connect("focus_entered", callable_mp(this, &InputEventConfigurationDialog::_on_listen_focus_changed)); - event_listener->connect("focus_exited", callable_mp(this, &InputEventConfigurationDialog::_on_listen_focus_changed)); - main_vbox->add_child(event_listener); - - main_vbox->add_child(memnew(HSeparator)); - - // List of all input options to manually select from. - VBoxContainer *manual_vbox = memnew(VBoxContainer); - manual_vbox->set_name(TTR("Manual Selection")); - manual_vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL); - main_vbox->add_child(manual_vbox); - - input_list_search = memnew(LineEdit); - input_list_search->set_h_size_flags(Control::SIZE_EXPAND_FILL); - input_list_search->set_placeholder(TTR("Filter Inputs")); - input_list_search->set_clear_button_enabled(true); - input_list_search->connect("text_changed", callable_mp(this, &InputEventConfigurationDialog::_search_term_updated)); - manual_vbox->add_child(input_list_search); - - input_list_tree = memnew(Tree); - input_list_tree->set_custom_minimum_size(Size2(0, 100 * EDSCALE)); // Min height for tree - input_list_tree->connect("item_selected", callable_mp(this, &InputEventConfigurationDialog::_input_list_item_selected)); - input_list_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); - manual_vbox->add_child(input_list_tree); - - input_list_tree->set_hide_root(true); - input_list_tree->set_columns(1); - - _update_input_list(); - - // Additional Options - additional_options_container = memnew(VBoxContainer); - additional_options_container->hide(); - - Label *opts_label = memnew(Label); - opts_label->set_theme_type_variation("HeaderSmall"); - opts_label->set_text(TTR("Additional Options")); - additional_options_container->add_child(opts_label); - - // Device Selection - device_container = memnew(HBoxContainer); - device_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); - - Label *device_label = memnew(Label); - device_label->set_theme_type_variation("HeaderSmall"); - device_label->set_text(TTR("Device:")); - device_container->add_child(device_label); - - device_id_option = memnew(OptionButton); - device_id_option->set_h_size_flags(Control::SIZE_EXPAND_FILL); - for (int i = -1; i < 8; i++) { - device_id_option->add_item(_get_device_string(i)); - } - device_id_option->connect("item_selected", callable_mp(this, &InputEventConfigurationDialog::_device_selection_changed)); - _set_current_device(InputMap::ALL_DEVICES); - device_container->add_child(device_id_option); - - device_container->hide(); - additional_options_container->add_child(device_container); - - // Modifier Selection - mod_container = memnew(HBoxContainer); - for (int i = 0; i < MOD_MAX; i++) { - String name = mods[i]; - mod_checkboxes[i] = memnew(CheckBox); - mod_checkboxes[i]->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_mod_toggled).bind(i)); - mod_checkboxes[i]->set_text(name); - mod_checkboxes[i]->set_tooltip_text(TTR(mods_tip[i])); - mod_container->add_child(mod_checkboxes[i]); - } - - mod_container->add_child(memnew(VSeparator)); - - autoremap_command_or_control_checkbox = memnew(CheckBox); - autoremap_command_or_control_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_autoremap_command_or_control_toggled)); - autoremap_command_or_control_checkbox->set_pressed(false); - autoremap_command_or_control_checkbox->set_text(TTR("Command / Control (auto)")); - autoremap_command_or_control_checkbox->set_tooltip_text(TTR("Automatically remaps between 'Meta' ('Command') and 'Control' depending on current platform.")); - mod_container->add_child(autoremap_command_or_control_checkbox); - - mod_container->hide(); - additional_options_container->add_child(mod_container); - - // Physical Key Checkbox - - physical_key_checkbox = memnew(CheckBox); - physical_key_checkbox->set_text(TTR("Use Physical Keycode")); - physical_key_checkbox->set_tooltip_text(TTR("Stores the physical position of the key on the keyboard rather than the key's value. Used for compatibility with non-latin layouts.\nThis should generally be enabled for most game shortcuts, but not in non-game applications.")); - physical_key_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_physical_keycode_toggled)); - physical_key_checkbox->hide(); - additional_options_container->add_child(physical_key_checkbox); - - main_vbox->add_child(additional_options_container); -} - -///////////////////////////////////////// +#include "editor/event_listener_line_edit.h" +#include "editor/input_event_configuration_dialog.h" +#include "scene/gui/check_button.h" +#include "scene/gui/tree.h" static bool _is_action_name_valid(const String &p_name) { const char32_t *cstr = p_name.get_data(); diff --git a/editor/action_map_editor.h b/editor/action_map_editor.h index f576a2b565..d56ee6f9eb 100644 --- a/editor/action_map_editor.h +++ b/editor/action_map_editor.h @@ -31,135 +31,16 @@ #ifndef ACTION_MAP_EDITOR_H #define ACTION_MAP_EDITOR_H -#include "scene/gui/check_box.h" -#include "scene/gui/check_button.h" -#include "scene/gui/color_rect.h" -#include "scene/gui/dialogs.h" -#include "scene/gui/label.h" -#include "scene/gui/option_button.h" -#include "scene/gui/tab_container.h" -#include "scene/gui/tree.h" - -enum InputType { - INPUT_KEY = 1, - INPUT_MOUSE_BUTTON = 2, - INPUT_JOY_BUTTON = 4, - INPUT_JOY_MOTION = 8 -}; - -class EventListenerLineEdit : public LineEdit { - GDCLASS(EventListenerLineEdit, LineEdit) - - int allowed_input_types = INPUT_KEY | INPUT_MOUSE_BUTTON | INPUT_JOY_BUTTON | INPUT_JOY_MOTION; - bool ignore = true; - bool share_keycodes = false; - Ref<InputEvent> event; - - bool _is_event_allowed(const Ref<InputEvent> &p_event) const; - - void gui_input(const Ref<InputEvent> &p_event) override; - void _on_text_changed(const String &p_text); - - void _on_focus(); - void _on_unfocus(); - -protected: - void _notification(int p_what); - static void _bind_methods(); - -public: - Ref<InputEvent> get_event() const; - void clear_event(); - - void set_allowed_input_types(int input_types); - int get_allowed_input_types() const; - -public: - EventListenerLineEdit(); -}; - -// Confirmation Dialog used when configuring an input event. -// Separate from ActionMapEditor for code cleanliness and separation of responsibilities. -class InputEventConfigurationDialog : public ConfirmationDialog { - GDCLASS(InputEventConfigurationDialog, ConfirmationDialog) -private: - struct IconCache { - Ref<Texture2D> keyboard; - Ref<Texture2D> mouse; - Ref<Texture2D> joypad_button; - Ref<Texture2D> joypad_axis; - } icon_cache; - - Ref<InputEvent> event = Ref<InputEvent>(); - - // Listening for input - EventListenerLineEdit *event_listener = nullptr; - Label *event_as_text = nullptr; - - // List of All Key/Mouse/Joypad input options. - int allowed_input_types; - Tree *input_list_tree = nullptr; - LineEdit *input_list_search = nullptr; - - // Additional Options, shown depending on event selected - VBoxContainer *additional_options_container = nullptr; - - HBoxContainer *device_container = nullptr; - OptionButton *device_id_option = nullptr; - - HBoxContainer *mod_container = nullptr; // Contains the subcontainer and the store command checkbox. - - enum ModCheckbox { - MOD_ALT, - MOD_SHIFT, - MOD_CTRL, - MOD_META, - MOD_MAX - }; -#if defined(MACOS_ENABLED) - String mods[MOD_MAX] = { "Option", "Shift", "Ctrl", "Command" }; -#elif defined(WINDOWS_ENABLED) - String mods[MOD_MAX] = { "Alt", "Shift", "Ctrl", "Windows" }; -#else - String mods[MOD_MAX] = { "Alt", "Shift", "Ctrl", "Meta" }; -#endif - String mods_tip[MOD_MAX] = { "Alt or Option key", "Shift key", "Control key", "Meta/Windows or Command key" }; - - CheckBox *mod_checkboxes[MOD_MAX]; - CheckBox *autoremap_command_or_control_checkbox = nullptr; - - CheckBox *physical_key_checkbox = nullptr; - - void _set_event(const Ref<InputEvent> &p_event, bool p_update_input_list_selection = true); - void _on_listen_input_changed(const Ref<InputEvent> &p_event); - void _on_listen_focus_changed(); - - void _search_term_updated(const String &p_term); - void _update_input_list(); - void _input_list_item_selected(); - - void _mod_toggled(bool p_checked, int p_index); - void _autoremap_command_or_control_toggled(bool p_checked); - void _physical_keycode_toggled(bool p_checked); - - void _device_selection_changed(int p_option_button_index); - void _set_current_device(int p_device); - int _get_current_device() const; - String _get_device_string(int p_device) const; - -protected: - void _notification(int p_what); - -public: - // Pass an existing event to configure it. Alternatively, pass no event to start with a blank configuration. - void popup_and_configure(const Ref<InputEvent> &p_event = Ref<InputEvent>()); - Ref<InputEvent> get_event() const; - String get_event_text(const Ref<InputEvent> &p_event, bool p_include_device) const; - - void set_allowed_input_types(int p_type_masks); - - InputEventConfigurationDialog(); -}; +#include "scene/gui/control.h" + +class Button; +class HBoxContainer; +class EventListenerLineEdit; +class LineEdit; +class CheckButton; +class AcceptDialog; +class InputEventConfigurationDialog; +class Tree; class ActionMapEditor : public Control { GDCLASS(ActionMapEditor, Control); diff --git a/editor/editor_settings_dialog.cpp b/editor/editor_settings_dialog.cpp index b5147bb4eb..db501aac03 100644 --- a/editor/editor_settings_dialog.cpp +++ b/editor/editor_settings_dialog.cpp @@ -41,6 +41,8 @@ #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_undo_redo_manager.h" +#include "editor/event_listener_line_edit.h" +#include "editor/input_event_configuration_dialog.h" #include "scene/gui/margin_container.h" void EditorSettingsDialog::ok_pressed() { diff --git a/editor/event_listener_line_edit.cpp b/editor/event_listener_line_edit.cpp new file mode 100644 index 0000000000..14e482432a --- /dev/null +++ b/editor/event_listener_line_edit.cpp @@ -0,0 +1,131 @@ +/*************************************************************************/ +/* event_listener_line_edit.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor/event_listener_line_edit.h" + +bool EventListenerLineEdit::_is_event_allowed(const Ref<InputEvent> &p_event) const { + const Ref<InputEventMouseButton> mb = p_event; + const Ref<InputEventKey> k = p_event; + const Ref<InputEventJoypadButton> jb = p_event; + const Ref<InputEventJoypadMotion> jm = p_event; + + return (mb.is_valid() && (allowed_input_types & INPUT_MOUSE_BUTTON)) || + (k.is_valid() && (allowed_input_types & INPUT_KEY)) || + (jb.is_valid() && (allowed_input_types & INPUT_JOY_BUTTON)) || + (jm.is_valid() && (allowed_input_types & INPUT_JOY_MOTION)); +} + +void EventListenerLineEdit::gui_input(const Ref<InputEvent> &p_event) { + const Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + LineEdit::gui_input(p_event); + return; + } + + // Allow mouse button click on the clear button without being treated as an event. + const Ref<InputEventMouseButton> b = p_event; + if (b.is_valid() && _is_over_clear_button(b->get_position())) { + LineEdit::gui_input(p_event); + return; + } + + // First event will be an event which is used to focus this control - i.e. a mouse click, or a tab press. + // Ignore the first one so that clicking into the LineEdit does not override the current event. + // Ignore is reset to true when the control is unfocused. + if (ignore) { + ignore = false; + return; + } + + accept_event(); + if (!p_event->is_pressed() || p_event->is_echo() || p_event->is_match(event) || !_is_event_allowed(p_event)) { + return; + } + + event = p_event; + set_text(event->as_text()); + emit_signal("event_changed", event); +} + +void EventListenerLineEdit::_on_text_changed(const String &p_text) { + if (p_text.is_empty()) { + clear_event(); + } +} + +void EventListenerLineEdit::_on_focus() { + set_placeholder(TTR("Listening for input...")); +} + +void EventListenerLineEdit::_on_unfocus() { + ignore = true; + set_placeholder(TTR("Filter by event...")); +} + +Ref<InputEvent> EventListenerLineEdit::get_event() const { + return event; +} + +void EventListenerLineEdit::clear_event() { + if (event.is_valid()) { + event = Ref<InputEvent>(); + set_text(""); + emit_signal("event_changed", event); + } +} + +void EventListenerLineEdit::set_allowed_input_types(int input_types) { + allowed_input_types = input_types; +} + +int EventListenerLineEdit::get_allowed_input_types() const { + return allowed_input_types; +} + +void EventListenerLineEdit::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + connect("text_changed", callable_mp(this, &EventListenerLineEdit::_on_text_changed)); + connect("focus_entered", callable_mp(this, &EventListenerLineEdit::_on_focus)); + connect("focus_exited", callable_mp(this, &EventListenerLineEdit::_on_unfocus)); + set_right_icon(get_theme_icon(SNAME("Keyboard"), SNAME("EditorIcons"))); + set_clear_button_enabled(true); + } break; + } +} + +void EventListenerLineEdit::_bind_methods() { + ADD_SIGNAL(MethodInfo("event_changed", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"))); +} + +EventListenerLineEdit::EventListenerLineEdit() { + set_caret_blink_enabled(false); + set_placeholder(TTR("Filter by event...")); +} diff --git a/editor/event_listener_line_edit.h b/editor/event_listener_line_edit.h new file mode 100644 index 0000000000..5b415e8a2d --- /dev/null +++ b/editor/event_listener_line_edit.h @@ -0,0 +1,74 @@ +/*************************************************************************/ +/* event_listener_line_edit.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EVENT_LISTENER_LINE_EDIT_H +#define EVENT_LISTENER_LINE_EDIT_H + +#include "scene/gui/line_edit.h" + +enum InputType { + INPUT_KEY = 1, + INPUT_MOUSE_BUTTON = 2, + INPUT_JOY_BUTTON = 4, + INPUT_JOY_MOTION = 8 +}; + +class EventListenerLineEdit : public LineEdit { + GDCLASS(EventListenerLineEdit, LineEdit) + + int allowed_input_types = INPUT_KEY | INPUT_MOUSE_BUTTON | INPUT_JOY_BUTTON | INPUT_JOY_MOTION; + bool ignore = true; + bool share_keycodes = false; + Ref<InputEvent> event; + + bool _is_event_allowed(const Ref<InputEvent> &p_event) const; + + void gui_input(const Ref<InputEvent> &p_event) override; + void _on_text_changed(const String &p_text); + + void _on_focus(); + void _on_unfocus(); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + Ref<InputEvent> get_event() const; + void clear_event(); + + void set_allowed_input_types(int input_types); + int get_allowed_input_types() const; + +public: + EventListenerLineEdit(); +}; + +#endif // EVENT_LISTENER_LINE_EDIT_H diff --git a/editor/input_event_configuration_dialog.cpp b/editor/input_event_configuration_dialog.cpp new file mode 100644 index 0000000000..ed9a5898fb --- /dev/null +++ b/editor/input_event_configuration_dialog.cpp @@ -0,0 +1,700 @@ +/*************************************************************************/ +/* input_event_configuration_dialog.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor/input_event_configuration_dialog.h" +#include "core/input/input_map.h" +#include "editor/editor_scale.h" +#include "editor/event_listener_line_edit.h" +#include "scene/gui/check_box.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/option_button.h" +#include "scene/gui/separator.h" +#include "scene/gui/tree.h" + +// Maps to 2*axis if value is neg, or 2*axis+1 if value is pos. +static const char *_joy_axis_descriptions[(size_t)JoyAxis::MAX * 2] = { + TTRC("Left Stick Left, Joystick 0 Left"), + TTRC("Left Stick Right, Joystick 0 Right"), + TTRC("Left Stick Up, Joystick 0 Up"), + TTRC("Left Stick Down, Joystick 0 Down"), + TTRC("Right Stick Left, Joystick 1 Left"), + TTRC("Right Stick Right, Joystick 1 Right"), + TTRC("Right Stick Up, Joystick 1 Up"), + TTRC("Right Stick Down, Joystick 1 Down"), + TTRC("Joystick 2 Left"), + TTRC("Left Trigger, Sony L2, Xbox LT, Joystick 2 Right"), + TTRC("Joystick 2 Up"), + TTRC("Right Trigger, Sony R2, Xbox RT, Joystick 2 Down"), + TTRC("Joystick 3 Left"), + TTRC("Joystick 3 Right"), + TTRC("Joystick 3 Up"), + TTRC("Joystick 3 Down"), + TTRC("Joystick 4 Left"), + TTRC("Joystick 4 Right"), + TTRC("Joystick 4 Up"), + TTRC("Joystick 4 Down"), +}; + +String InputEventConfigurationDialog::get_event_text(const Ref<InputEvent> &p_event, bool p_include_device) const { + ERR_FAIL_COND_V_MSG(p_event.is_null(), String(), "Provided event is not a valid instance of InputEvent"); + + String text = p_event->as_text(); + + Ref<InputEventKey> key = p_event; + if (key.is_valid() && key->is_command_or_control_autoremap()) { +#ifdef MACOS_ENABLED + text = text.replace("Command", "Command/Ctrl"); +#else + text = text.replace("Ctrl", "Command/Ctrl"); +#endif + } + Ref<InputEventMouse> mouse = p_event; + Ref<InputEventJoypadMotion> jp_motion = p_event; + Ref<InputEventJoypadButton> jp_button = p_event; + if (jp_motion.is_valid()) { + // Joypad motion events will display slightly differently than what the event->as_text() provides. See #43660. + String desc = TTR("Unknown Joypad Axis"); + if (jp_motion->get_axis() < JoyAxis::MAX) { + desc = RTR(_joy_axis_descriptions[2 * (size_t)jp_motion->get_axis() + (jp_motion->get_axis_value() < 0 ? 0 : 1)]); + } + + text = vformat("Joypad Axis %s %s (%s)", itos((int64_t)jp_motion->get_axis()), jp_motion->get_axis_value() < 0 ? "-" : "+", desc); + } + if (p_include_device && (mouse.is_valid() || jp_button.is_valid() || jp_motion.is_valid())) { + String device_string = _get_device_string(p_event->get_device()); + text += vformat(" - %s", device_string); + } + + return text; +} + +void InputEventConfigurationDialog::_set_event(const Ref<InputEvent> &p_event, bool p_update_input_list_selection) { + if (p_event.is_valid()) { + event = p_event; + + // If the event is changed to something which is not the same as the listener, + // clear out the event from the listener text box to avoid confusion. + const Ref<InputEvent> listener_event = event_listener->get_event(); + if (listener_event.is_valid() && !listener_event->is_match(p_event)) { + event_listener->clear_event(); + } + + // Update Label + event_as_text->set_text(get_event_text(event, true)); + + Ref<InputEventKey> k = p_event; + Ref<InputEventMouseButton> mb = p_event; + Ref<InputEventJoypadButton> joyb = p_event; + Ref<InputEventJoypadMotion> joym = p_event; + Ref<InputEventWithModifiers> mod = p_event; + + // Update option values and visibility + bool show_mods = false; + bool show_device = false; + bool show_phys_key = false; + + if (mod.is_valid()) { + show_mods = true; + mod_checkboxes[MOD_ALT]->set_pressed(mod->is_alt_pressed()); + mod_checkboxes[MOD_SHIFT]->set_pressed(mod->is_shift_pressed()); + mod_checkboxes[MOD_CTRL]->set_pressed(mod->is_ctrl_pressed()); + mod_checkboxes[MOD_META]->set_pressed(mod->is_meta_pressed()); + + autoremap_command_or_control_checkbox->set_pressed(mod->is_command_or_control_autoremap()); + } + + if (k.is_valid()) { + show_phys_key = true; + physical_key_checkbox->set_pressed(k->get_physical_keycode() != Key::NONE && k->get_keycode() == Key::NONE); + + } else if (joyb.is_valid() || joym.is_valid() || mb.is_valid()) { + show_device = true; + _set_current_device(event->get_device()); + } + + mod_container->set_visible(show_mods); + device_container->set_visible(show_device); + physical_key_checkbox->set_visible(show_phys_key); + additional_options_container->show(); + + // Update selected item in input list. + if (p_update_input_list_selection && (k.is_valid() || joyb.is_valid() || joym.is_valid() || mb.is_valid())) { + TreeItem *category = input_list_tree->get_root()->get_first_child(); + while (category) { + TreeItem *input_item = category->get_first_child(); + + if (input_item != nullptr) { + // input_type should always be > 0, unless the tree structure has been misconfigured. + int input_type = input_item->get_parent()->get_meta("__type", 0); + if (input_type == 0) { + return; + } + + // If event type matches input types of this category. + if ((k.is_valid() && input_type == INPUT_KEY) || (joyb.is_valid() && input_type == INPUT_JOY_BUTTON) || (joym.is_valid() && input_type == INPUT_JOY_MOTION) || (mb.is_valid() && input_type == INPUT_MOUSE_BUTTON)) { + // Loop through all items of this category until one matches. + while (input_item) { + bool key_match = k.is_valid() && (Variant(k->get_keycode()) == input_item->get_meta("__keycode") || Variant(k->get_physical_keycode()) == input_item->get_meta("__keycode")); + bool joyb_match = joyb.is_valid() && Variant(joyb->get_button_index()) == input_item->get_meta("__index"); + bool joym_match = joym.is_valid() && Variant(joym->get_axis()) == input_item->get_meta("__axis") && joym->get_axis_value() == (float)input_item->get_meta("__value"); + bool mb_match = mb.is_valid() && Variant(mb->get_button_index()) == input_item->get_meta("__index"); + if (key_match || joyb_match || joym_match || mb_match) { + category->set_collapsed(false); + input_item->select(0); + input_list_tree->ensure_cursor_is_visible(); + return; + } + input_item = input_item->get_next(); + } + } + } + + category->set_collapsed(true); // Event not in this category, so collapse; + category = category->get_next(); + } + } + } else { + // Event is not valid, reset dialog + event = p_event; + event_listener->clear_event(); + event_as_text->set_text(TTR("No Event Configured")); + + additional_options_container->hide(); + input_list_tree->deselect_all(); + _update_input_list(); + } +} + +void InputEventConfigurationDialog::_on_listen_input_changed(const Ref<InputEvent> &p_event) { + // Ignore if invalid, echo or not pressed + if (p_event.is_null() || p_event->is_echo() || !p_event->is_pressed()) { + return; + } + + // Create an editable reference + Ref<InputEvent> received_event = p_event; + // Check what the type is and if it is allowed. + Ref<InputEventKey> k = received_event; + Ref<InputEventJoypadButton> joyb = received_event; + Ref<InputEventJoypadMotion> joym = received_event; + Ref<InputEventMouseButton> mb = received_event; + + int type = 0; + if (k.is_valid()) { + type = INPUT_KEY; + } else if (joyb.is_valid()) { + type = INPUT_JOY_BUTTON; + } else if (joym.is_valid()) { + type = INPUT_JOY_MOTION; + } else if (mb.is_valid()) { + type = INPUT_MOUSE_BUTTON; + } + + if (!(allowed_input_types & type)) { + return; + } + + if (joym.is_valid()) { + float axis_value = joym->get_axis_value(); + if (ABS(axis_value) < 0.9) { + // Ignore motion below 0.9 magnitude to avoid accidental touches + return; + } else { + // Always make the value 1 or -1 for display consistency + joym->set_axis_value(SIGN(axis_value)); + } + } + + if (k.is_valid()) { + k->set_pressed(false); // To avoid serialisation of 'pressed' property - doesn't matter for actions anyway. + // Maintain physical keycode option state + if (physical_key_checkbox->is_pressed()) { + k->set_keycode(Key::NONE); + } else { + k->set_physical_keycode(Key::NONE); + } + } + + Ref<InputEventWithModifiers> mod = received_event; + if (mod.is_valid()) { + mod->set_window_id(0); + } + + // Maintain device selection. + received_event->set_device(_get_current_device()); + + _set_event(received_event); +} + +void InputEventConfigurationDialog::_on_listen_focus_changed() { + if (event_listener->has_focus()) { + set_close_on_escape(false); + } else { + set_close_on_escape(true); + } +} + +void InputEventConfigurationDialog::_search_term_updated(const String &) { + _update_input_list(); +} + +void InputEventConfigurationDialog::_update_input_list() { + input_list_tree->clear(); + + TreeItem *root = input_list_tree->create_item(); + String search_term = input_list_search->get_text(); + + bool collapse = input_list_search->get_text().is_empty(); + + if (allowed_input_types & INPUT_KEY) { + TreeItem *kb_root = input_list_tree->create_item(root); + kb_root->set_text(0, TTR("Keyboard Keys")); + kb_root->set_icon(0, icon_cache.keyboard); + kb_root->set_collapsed(collapse); + kb_root->set_meta("__type", INPUT_KEY); + + for (int i = 0; i < keycode_get_count(); i++) { + String name = keycode_get_name_by_index(i); + + if (!search_term.is_empty() && name.findn(search_term) == -1) { + continue; + } + + TreeItem *item = input_list_tree->create_item(kb_root); + item->set_text(0, name); + item->set_meta("__keycode", keycode_get_value_by_index(i)); + } + } + + if (allowed_input_types & INPUT_MOUSE_BUTTON) { + TreeItem *mouse_root = input_list_tree->create_item(root); + mouse_root->set_text(0, TTR("Mouse Buttons")); + mouse_root->set_icon(0, icon_cache.mouse); + mouse_root->set_collapsed(collapse); + mouse_root->set_meta("__type", INPUT_MOUSE_BUTTON); + + MouseButton mouse_buttons[9] = { MouseButton::LEFT, MouseButton::RIGHT, MouseButton::MIDDLE, MouseButton::WHEEL_UP, MouseButton::WHEEL_DOWN, MouseButton::WHEEL_LEFT, MouseButton::WHEEL_RIGHT, MouseButton::MB_XBUTTON1, MouseButton::MB_XBUTTON2 }; + for (int i = 0; i < 9; i++) { + Ref<InputEventMouseButton> mb; + mb.instantiate(); + mb->set_button_index(mouse_buttons[i]); + String desc = get_event_text(mb, false); + + if (!search_term.is_empty() && desc.findn(search_term) == -1) { + continue; + } + + TreeItem *item = input_list_tree->create_item(mouse_root); + item->set_text(0, desc); + item->set_meta("__index", mouse_buttons[i]); + } + } + + if (allowed_input_types & INPUT_JOY_BUTTON) { + TreeItem *joyb_root = input_list_tree->create_item(root); + joyb_root->set_text(0, TTR("Joypad Buttons")); + joyb_root->set_icon(0, icon_cache.joypad_button); + joyb_root->set_collapsed(collapse); + joyb_root->set_meta("__type", INPUT_JOY_BUTTON); + + for (int i = 0; i < (int)JoyButton::MAX; i++) { + Ref<InputEventJoypadButton> joyb; + joyb.instantiate(); + joyb->set_button_index((JoyButton)i); + String desc = get_event_text(joyb, false); + + if (!search_term.is_empty() && desc.findn(search_term) == -1) { + continue; + } + + TreeItem *item = input_list_tree->create_item(joyb_root); + item->set_text(0, desc); + item->set_meta("__index", i); + } + } + + if (allowed_input_types & INPUT_JOY_MOTION) { + TreeItem *joya_root = input_list_tree->create_item(root); + joya_root->set_text(0, TTR("Joypad Axes")); + joya_root->set_icon(0, icon_cache.joypad_axis); + joya_root->set_collapsed(collapse); + joya_root->set_meta("__type", INPUT_JOY_MOTION); + + for (int i = 0; i < (int)JoyAxis::MAX * 2; i++) { + int axis = i / 2; + int direction = (i & 1) ? 1 : -1; + Ref<InputEventJoypadMotion> joym; + joym.instantiate(); + joym->set_axis((JoyAxis)axis); + joym->set_axis_value(direction); + String desc = get_event_text(joym, false); + + if (!search_term.is_empty() && desc.findn(search_term) == -1) { + continue; + } + + TreeItem *item = input_list_tree->create_item(joya_root); + item->set_text(0, desc); + item->set_meta("__axis", i >> 1); + item->set_meta("__value", (i & 1) ? 1 : -1); + } + } +} + +void InputEventConfigurationDialog::_mod_toggled(bool p_checked, int p_index) { + Ref<InputEventWithModifiers> ie = event; + + // Not event with modifiers + if (ie.is_null()) { + return; + } + + if (p_index == 0) { + ie->set_alt_pressed(p_checked); + } else if (p_index == 1) { + ie->set_shift_pressed(p_checked); + } else if (p_index == 2) { + if (!autoremap_command_or_control_checkbox->is_pressed()) { + ie->set_ctrl_pressed(p_checked); + } + } else if (p_index == 3) { + if (!autoremap_command_or_control_checkbox->is_pressed()) { + ie->set_meta_pressed(p_checked); + } + } + + _set_event(ie); +} + +void InputEventConfigurationDialog::_autoremap_command_or_control_toggled(bool p_checked) { + Ref<InputEventWithModifiers> ie = event; + if (ie.is_valid()) { + ie->set_command_or_control_autoremap(p_checked); + _set_event(ie); + } + + if (p_checked) { + mod_checkboxes[MOD_META]->hide(); + mod_checkboxes[MOD_CTRL]->hide(); + } else { + mod_checkboxes[MOD_META]->show(); + mod_checkboxes[MOD_CTRL]->show(); + } +} + +void InputEventConfigurationDialog::_physical_keycode_toggled(bool p_checked) { + Ref<InputEventKey> k = event; + + if (k.is_null()) { + return; + } + + if (p_checked) { + k->set_physical_keycode(k->get_keycode()); + k->set_keycode(Key::NONE); + } else { + k->set_keycode((Key)k->get_physical_keycode()); + k->set_physical_keycode(Key::NONE); + } + + _set_event(k); +} + +void InputEventConfigurationDialog::_input_list_item_selected() { + TreeItem *selected = input_list_tree->get_selected(); + + // Invalid tree selection - type only exists on the "category" items, which are not a valid selection. + if (selected->has_meta("__type")) { + return; + } + + InputType input_type = (InputType)(int)selected->get_parent()->get_meta("__type"); + + switch (input_type) { + case INPUT_KEY: { + Key keycode = (Key)(int)selected->get_meta("__keycode"); + Ref<InputEventKey> k; + k.instantiate(); + + if (physical_key_checkbox->is_pressed()) { + k->set_physical_keycode(keycode); + k->set_keycode(Key::NONE); + } else { + k->set_physical_keycode(Key::NONE); + k->set_keycode(keycode); + } + + // Maintain modifier state from checkboxes + k->set_alt_pressed(mod_checkboxes[MOD_ALT]->is_pressed()); + k->set_shift_pressed(mod_checkboxes[MOD_SHIFT]->is_pressed()); + if (autoremap_command_or_control_checkbox->is_pressed()) { + k->set_command_or_control_autoremap(true); + } else { + k->set_ctrl_pressed(mod_checkboxes[MOD_CTRL]->is_pressed()); + k->set_meta_pressed(mod_checkboxes[MOD_META]->is_pressed()); + } + + _set_event(k, false); + } break; + case INPUT_MOUSE_BUTTON: { + MouseButton idx = (MouseButton)(int)selected->get_meta("__index"); + Ref<InputEventMouseButton> mb; + mb.instantiate(); + mb->set_button_index(idx); + // Maintain modifier state from checkboxes + mb->set_alt_pressed(mod_checkboxes[MOD_ALT]->is_pressed()); + mb->set_shift_pressed(mod_checkboxes[MOD_SHIFT]->is_pressed()); + if (autoremap_command_or_control_checkbox->is_pressed()) { + mb->set_command_or_control_autoremap(true); + } else { + mb->set_ctrl_pressed(mod_checkboxes[MOD_CTRL]->is_pressed()); + mb->set_meta_pressed(mod_checkboxes[MOD_META]->is_pressed()); + } + + // Maintain selected device + mb->set_device(_get_current_device()); + + _set_event(mb, false); + } break; + case INPUT_JOY_BUTTON: { + JoyButton idx = (JoyButton)(int)selected->get_meta("__index"); + Ref<InputEventJoypadButton> jb = InputEventJoypadButton::create_reference(idx); + + // Maintain selected device + jb->set_device(_get_current_device()); + + _set_event(jb, false); + } break; + case INPUT_JOY_MOTION: { + JoyAxis axis = (JoyAxis)(int)selected->get_meta("__axis"); + int value = selected->get_meta("__value"); + + Ref<InputEventJoypadMotion> jm; + jm.instantiate(); + jm->set_axis(axis); + jm->set_axis_value(value); + + // Maintain selected device + jm->set_device(_get_current_device()); + + _set_event(jm, false); + } break; + } +} + +void InputEventConfigurationDialog::_device_selection_changed(int p_option_button_index) { + // Subtract 1 as option index 0 corresponds to "All Devices" (value of -1) + // and option index 1 corresponds to device 0, etc... + event->set_device(p_option_button_index - 1); + event_as_text->set_text(get_event_text(event, true)); +} + +void InputEventConfigurationDialog::_set_current_device(int p_device) { + device_id_option->select(p_device + 1); +} + +int InputEventConfigurationDialog::_get_current_device() const { + return device_id_option->get_selected() - 1; +} + +String InputEventConfigurationDialog::_get_device_string(int p_device) const { + if (p_device == InputMap::ALL_DEVICES) { + return TTR("All Devices"); + } + return TTR("Device") + " " + itos(p_device); +} + +void InputEventConfigurationDialog::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: { + input_list_search->set_right_icon(input_list_search->get_theme_icon(SNAME("Search"), SNAME("EditorIcons"))); + + physical_key_checkbox->set_icon(get_theme_icon(SNAME("KeyboardPhysical"), SNAME("EditorIcons"))); + + icon_cache.keyboard = get_theme_icon(SNAME("Keyboard"), SNAME("EditorIcons")); + icon_cache.mouse = get_theme_icon(SNAME("Mouse"), SNAME("EditorIcons")); + icon_cache.joypad_button = get_theme_icon(SNAME("JoyButton"), SNAME("EditorIcons")); + icon_cache.joypad_axis = get_theme_icon(SNAME("JoyAxis"), SNAME("EditorIcons")); + + _update_input_list(); + } break; + } +} + +void InputEventConfigurationDialog::popup_and_configure(const Ref<InputEvent> &p_event) { + if (p_event.is_valid()) { + _set_event(p_event); + } else { + // Clear Event + _set_event(p_event); + + // Clear Checkbox Values + for (int i = 0; i < MOD_MAX; i++) { + mod_checkboxes[i]->set_pressed(false); + } + + // Enable the Physical Key checkbox by default to encourage its use. + // Physical Key should be used for most game inputs as it allows keys to work + // on non-QWERTY layouts out of the box. + // This is especially important for WASD movement layouts. + physical_key_checkbox->set_pressed(true); + + autoremap_command_or_control_checkbox->set_pressed(false); + + // Select "All Devices" by default. + device_id_option->select(0); + } + + popup_centered(Size2(0, 400) * EDSCALE); +} + +Ref<InputEvent> InputEventConfigurationDialog::get_event() const { + return event; +} + +void InputEventConfigurationDialog::set_allowed_input_types(int p_type_masks) { + allowed_input_types = p_type_masks; +} + +InputEventConfigurationDialog::InputEventConfigurationDialog() { + allowed_input_types = INPUT_KEY | INPUT_MOUSE_BUTTON | INPUT_JOY_BUTTON | INPUT_JOY_MOTION; + + set_title(TTR("Event Configuration")); + set_min_size(Size2i(550 * EDSCALE, 0)); // Min width + + VBoxContainer *main_vbox = memnew(VBoxContainer); + add_child(main_vbox); + + event_as_text = memnew(Label); + event_as_text->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); + event_as_text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); + event_as_text->add_theme_font_override("font", get_theme_font(SNAME("bold"), SNAME("EditorFonts"))); + event_as_text->add_theme_font_size_override("font_size", 18 * EDSCALE); + main_vbox->add_child(event_as_text); + + event_listener = memnew(EventListenerLineEdit); + event_listener->set_h_size_flags(Control::SIZE_EXPAND_FILL); + event_listener->set_stretch_ratio(0.75); + event_listener->connect("event_changed", callable_mp(this, &InputEventConfigurationDialog::_on_listen_input_changed)); + event_listener->connect("focus_entered", callable_mp(this, &InputEventConfigurationDialog::_on_listen_focus_changed)); + event_listener->connect("focus_exited", callable_mp(this, &InputEventConfigurationDialog::_on_listen_focus_changed)); + main_vbox->add_child(event_listener); + + main_vbox->add_child(memnew(HSeparator)); + + // List of all input options to manually select from. + VBoxContainer *manual_vbox = memnew(VBoxContainer); + manual_vbox->set_name(TTR("Manual Selection")); + manual_vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL); + main_vbox->add_child(manual_vbox); + + input_list_search = memnew(LineEdit); + input_list_search->set_h_size_flags(Control::SIZE_EXPAND_FILL); + input_list_search->set_placeholder(TTR("Filter Inputs")); + input_list_search->set_clear_button_enabled(true); + input_list_search->connect("text_changed", callable_mp(this, &InputEventConfigurationDialog::_search_term_updated)); + manual_vbox->add_child(input_list_search); + + input_list_tree = memnew(Tree); + input_list_tree->set_custom_minimum_size(Size2(0, 100 * EDSCALE)); // Min height for tree + input_list_tree->connect("item_selected", callable_mp(this, &InputEventConfigurationDialog::_input_list_item_selected)); + input_list_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); + manual_vbox->add_child(input_list_tree); + + input_list_tree->set_hide_root(true); + input_list_tree->set_columns(1); + + _update_input_list(); + + // Additional Options + additional_options_container = memnew(VBoxContainer); + additional_options_container->hide(); + + Label *opts_label = memnew(Label); + opts_label->set_theme_type_variation("HeaderSmall"); + opts_label->set_text(TTR("Additional Options")); + additional_options_container->add_child(opts_label); + + // Device Selection + device_container = memnew(HBoxContainer); + device_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); + + Label *device_label = memnew(Label); + device_label->set_theme_type_variation("HeaderSmall"); + device_label->set_text(TTR("Device:")); + device_container->add_child(device_label); + + device_id_option = memnew(OptionButton); + device_id_option->set_h_size_flags(Control::SIZE_EXPAND_FILL); + for (int i = -1; i < 8; i++) { + device_id_option->add_item(_get_device_string(i)); + } + device_id_option->connect("item_selected", callable_mp(this, &InputEventConfigurationDialog::_device_selection_changed)); + _set_current_device(InputMap::ALL_DEVICES); + device_container->add_child(device_id_option); + + device_container->hide(); + additional_options_container->add_child(device_container); + + // Modifier Selection + mod_container = memnew(HBoxContainer); + for (int i = 0; i < MOD_MAX; i++) { + String name = mods[i]; + mod_checkboxes[i] = memnew(CheckBox); + mod_checkboxes[i]->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_mod_toggled).bind(i)); + mod_checkboxes[i]->set_text(name); + mod_checkboxes[i]->set_tooltip_text(TTR(mods_tip[i])); + mod_container->add_child(mod_checkboxes[i]); + } + + mod_container->add_child(memnew(VSeparator)); + + autoremap_command_or_control_checkbox = memnew(CheckBox); + autoremap_command_or_control_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_autoremap_command_or_control_toggled)); + autoremap_command_or_control_checkbox->set_pressed(false); + autoremap_command_or_control_checkbox->set_text(TTR("Command / Control (auto)")); + autoremap_command_or_control_checkbox->set_tooltip_text(TTR("Automatically remaps between 'Meta' ('Command') and 'Control' depending on current platform.")); + mod_container->add_child(autoremap_command_or_control_checkbox); + + mod_container->hide(); + additional_options_container->add_child(mod_container); + + // Physical Key Checkbox + + physical_key_checkbox = memnew(CheckBox); + physical_key_checkbox->set_text(TTR("Use Physical Keycode")); + physical_key_checkbox->set_tooltip_text(TTR("Stores the physical position of the key on the keyboard rather than the key's value. Used for compatibility with non-latin layouts.\nThis should generally be enabled for most game shortcuts, but not in non-game applications.")); + physical_key_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_physical_keycode_toggled)); + physical_key_checkbox->hide(); + additional_options_container->add_child(physical_key_checkbox); + + main_vbox->add_child(additional_options_container); +} diff --git a/editor/input_event_configuration_dialog.h b/editor/input_event_configuration_dialog.h new file mode 100644 index 0000000000..fc590cba26 --- /dev/null +++ b/editor/input_event_configuration_dialog.h @@ -0,0 +1,124 @@ +/*************************************************************************/ +/* input_event_configuration_dialog.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef INPUT_EVENT_CONFIGURATION_DIALOG_H +#define INPUT_EVENT_CONFIGURATION_DIALOG_H + +#include "scene/gui/dialogs.h" + +class OptionButton; +class Tree; +class EventListenerLineEdit; +class CheckBox; + +// Confirmation Dialog used when configuring an input event. +// Separate from ActionMapEditor for code cleanliness and separation of responsibilities. +class InputEventConfigurationDialog : public ConfirmationDialog { + GDCLASS(InputEventConfigurationDialog, ConfirmationDialog) +private: + struct IconCache { + Ref<Texture2D> keyboard; + Ref<Texture2D> mouse; + Ref<Texture2D> joypad_button; + Ref<Texture2D> joypad_axis; + } icon_cache; + + Ref<InputEvent> event = Ref<InputEvent>(); + + // Listening for input + EventListenerLineEdit *event_listener = nullptr; + Label *event_as_text = nullptr; + + // List of All Key/Mouse/Joypad input options. + int allowed_input_types; + Tree *input_list_tree = nullptr; + LineEdit *input_list_search = nullptr; + + // Additional Options, shown depending on event selected + VBoxContainer *additional_options_container = nullptr; + + HBoxContainer *device_container = nullptr; + OptionButton *device_id_option = nullptr; + + HBoxContainer *mod_container = nullptr; // Contains the subcontainer and the store command checkbox. + + enum ModCheckbox { + MOD_ALT, + MOD_SHIFT, + MOD_CTRL, + MOD_META, + MOD_MAX + }; +#if defined(MACOS_ENABLED) + String mods[MOD_MAX] = { "Option", "Shift", "Ctrl", "Command" }; +#elif defined(WINDOWS_ENABLED) + String mods[MOD_MAX] = { "Alt", "Shift", "Ctrl", "Windows" }; +#else + String mods[MOD_MAX] = { "Alt", "Shift", "Ctrl", "Meta" }; +#endif + String mods_tip[MOD_MAX] = { "Alt or Option key", "Shift key", "Control key", "Meta/Windows or Command key" }; + + CheckBox *mod_checkboxes[MOD_MAX]; + CheckBox *autoremap_command_or_control_checkbox = nullptr; + + CheckBox *physical_key_checkbox = nullptr; + + void _set_event(const Ref<InputEvent> &p_event, bool p_update_input_list_selection = true); + void _on_listen_input_changed(const Ref<InputEvent> &p_event); + void _on_listen_focus_changed(); + + void _search_term_updated(const String &p_term); + void _update_input_list(); + void _input_list_item_selected(); + + void _mod_toggled(bool p_checked, int p_index); + void _autoremap_command_or_control_toggled(bool p_checked); + void _physical_keycode_toggled(bool p_checked); + + void _device_selection_changed(int p_option_button_index); + void _set_current_device(int p_device); + int _get_current_device() const; + String _get_device_string(int p_device) const; + +protected: + void _notification(int p_what); + +public: + // Pass an existing event to configure it. Alternatively, pass no event to start with a blank configuration. + void popup_and_configure(const Ref<InputEvent> &p_event = Ref<InputEvent>()); + Ref<InputEvent> get_event() const; + String get_event_text(const Ref<InputEvent> &p_event, bool p_include_device) const; + + void set_allowed_input_types(int p_type_masks); + + InputEventConfigurationDialog(); +}; + +#endif // INPUT_EVENT_CONFIGURATION_DIALOG_H diff --git a/editor/plugins/input_event_editor_plugin.cpp b/editor/plugins/input_event_editor_plugin.cpp index e28c1a4777..b67f0f6ad2 100644 --- a/editor/plugins/input_event_editor_plugin.cpp +++ b/editor/plugins/input_event_editor_plugin.cpp @@ -30,6 +30,9 @@ #include "input_event_editor_plugin.h" +#include "editor/event_listener_line_edit.h" +#include "editor/input_event_configuration_dialog.h" + void InputEventConfigContainer::_bind_methods() { } diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index 2da49f11cc..81f37eaeec 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -36,6 +36,7 @@ #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_undo_redo_manager.h" +#include "scene/gui/check_button.h" #include "servers/movie_writer/movie_writer.h" ProjectSettingsEditor *ProjectSettingsEditor::singleton = nullptr; diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index d1dc188be9..967334f183 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -1187,6 +1187,8 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { } break; default: { + _filter_option_selected(p_tool); + if (p_tool >= EDIT_SUBRESOURCE_BASE) { int idx = p_tool - EDIT_SUBRESOURCE_BASE; @@ -2883,11 +2885,83 @@ void SceneTreeDock::_tree_rmb(const Vector2 &p_menu_pos) { void SceneTreeDock::_update_tree_menu() { PopupMenu *tree_menu = button_tree_menu->get_popup(); - tree_menu->set_item_checked(tree_menu->get_item_idx_from_text(TTR("Auto Expand to Selected")), EditorSettings::get_singleton()->get("docks/scene_tree/auto_expand_to_selected")); + tree_menu->clear(); + + _append_filter_options_to(tree_menu); + + tree_menu->add_separator(); + tree_menu->add_check_item(TTR("Auto Expand to Selected"), TOOL_AUTO_EXPAND); + tree_menu->set_item_checked(tree_menu->get_item_index(TOOL_AUTO_EXPAND), EditorSettings::get_singleton()->get("docks/scene_tree/auto_expand_to_selected")); +} + +void SceneTreeDock::_update_filter_menu() { + _append_filter_options_to(filter->get_menu()); } void SceneTreeDock::_filter_changed(const String &p_filter) { scene_tree->set_filter(p_filter); + + String warning = scene_tree->get_filter_term_warning(); + if (!warning.is_empty()) { + filter->add_theme_icon_override(SNAME("clear"), get_theme_icon(SNAME("NodeWarning"), SNAME("EditorIcons"))); + filter->set_tooltip_text(warning); + } else { + filter->remove_theme_icon_override(SNAME("clear")); + filter->set_tooltip_text(""); + } +} + +void SceneTreeDock::_filter_gui_input(const Ref<InputEvent> &p_event) { + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_null()) { + return; + } + + if (mb->is_pressed() && mb->get_button_index() == MouseButton::MIDDLE) { + filter_quick_menu->clear(); + + _append_filter_options_to(filter_quick_menu, false); + filter_quick_menu->set_position(get_screen_position() + get_local_mouse_position()); + filter_quick_menu->reset_size(); + filter_quick_menu->popup(); + filter_quick_menu->grab_focus(); + accept_event(); + } +} + +void SceneTreeDock::_filter_option_selected(int p_option) { + String filter_parameter; + switch (p_option) { + case FILTER_BY_TYPE: { + filter_parameter = "type"; + } break; + case FILTER_BY_GROUP: { + filter_parameter = "group"; + } break; + } + + if (!filter_parameter.is_empty()) { + set_filter((get_filter() + " " + filter_parameter + ":").strip_edges()); + filter->set_caret_column(filter->get_text().length()); + filter->grab_focus(); + } +} + +void SceneTreeDock::_append_filter_options_to(PopupMenu *p_menu, bool p_include_separator) { + if (p_include_separator) { + p_menu->add_separator(); + + p_menu->set_item_text(-1, TTR("Filters")); + p_menu->set_item_icon(-1, get_theme_icon(SNAME("Search"), SNAME("EditorIcons"))); + p_menu->set_item_indent(-1, -2); + } + + p_menu->add_item(TTR("Filter by Type"), FILTER_BY_TYPE); + p_menu->add_item(TTR("Filter by Group"), FILTER_BY_GROUP); + p_menu->set_item_icon(p_menu->get_item_index(FILTER_BY_TYPE), get_theme_icon(SNAME("Node"), SNAME("EditorIcons"))); + p_menu->set_item_icon(p_menu->get_item_index(FILTER_BY_GROUP), get_theme_icon(SNAME("Groups"), SNAME("EditorIcons"))); + p_menu->set_item_tooltip(p_menu->get_item_index(FILTER_BY_TYPE), TTR("Selects all Nodes of the given type.")); + p_menu->set_item_tooltip(p_menu->get_item_index(FILTER_BY_GROUP), TTR("Selects all Nodes belonging to the given group.\nIf empty, selects any Node belonging to any group.")); } String SceneTreeDock::get_filter() { @@ -3399,14 +3473,22 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec button_instance->set_tooltip_text(TTR("Instantiate a scene file as a Node. Creates an inherited scene if no root node exists.")); button_instance->set_shortcut(ED_GET_SHORTCUT("scene_tree/instance_scene")); filter_hbc->add_child(button_instance); - vbc->add_child(filter_hbc); + + // The "Filter Nodes" text input above the Scene Tree Editor. filter = memnew(LineEdit); filter->set_h_size_flags(SIZE_EXPAND_FILL); filter->set_placeholder(TTR("Filter Nodes")); filter_hbc->add_child(filter); filter->add_theme_constant_override("minimum_character_width", 0); filter->connect("text_changed", callable_mp(this, &SceneTreeDock::_filter_changed)); + filter->connect("gui_input", callable_mp(this, &SceneTreeDock::_filter_gui_input)); + filter->get_menu()->connect("about_to_popup", callable_mp(this, &SceneTreeDock::_update_filter_menu)); + filter->get_menu()->connect("id_pressed", callable_mp(this, &SceneTreeDock::_filter_option_selected)); + + filter_quick_menu = memnew(PopupMenu); + filter_quick_menu->connect("id_pressed", callable_mp(this, &SceneTreeDock::_filter_option_selected)); + filter->add_child(filter_quick_menu); button_create_script = memnew(Button); button_create_script->set_flat(true); @@ -3430,7 +3512,6 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec filter_hbc->add_child(button_tree_menu); PopupMenu *tree_menu = button_tree_menu->get_popup(); - tree_menu->add_check_item(TTR("Auto Expand to Selected"), TOOL_AUTO_EXPAND); tree_menu->connect("id_pressed", callable_mp(this, &SceneTreeDock::_tool_selected).bind(false)); button_hb = memnew(HBoxContainer); diff --git a/editor/scene_tree_dock.h b/editor/scene_tree_dock.h index e48b518252..04bb4d93e7 100644 --- a/editor/scene_tree_dock.h +++ b/editor/scene_tree_dock.h @@ -160,7 +160,13 @@ class SceneTreeDock : public VBoxContainer { EditorQuickOpen *quick_open = nullptr; EditorFileDialog *new_scene_from_dialog = nullptr; + enum FilterMenuItems { + FILTER_BY_TYPE = 64, // Used in the same menus as the Tool enum. + FILTER_BY_GROUP, + }; + LineEdit *filter = nullptr; + PopupMenu *filter_quick_menu = nullptr; TextureRect *filter_icon = nullptr; PopupMenu *menu = nullptr; @@ -243,8 +249,12 @@ class SceneTreeDock : public VBoxContainer { void _tree_rmb(const Vector2 &p_menu_pos); void _update_tree_menu(); + void _update_filter_menu(); void _filter_changed(const String &p_filter); + void _filter_gui_input(const Ref<InputEvent> &p_event); + void _filter_option_selected(int option); + void _append_filter_options_to(PopupMenu *p_menu, bool p_include_separator = true); void _perform_instantiate_scenes(const Vector<String> &p_files, Node *parent, int p_pos); void _replace_with_branch_scene(const String &p_file, Node *base); diff --git a/editor/scene_tree_editor.cpp b/editor/scene_tree_editor.cpp index 689570bcf6..abebd306e6 100644 --- a/editor/scene_tree_editor.cpp +++ b/editor/scene_tree_editor.cpp @@ -612,6 +612,7 @@ void SceneTreeEditor::_update_tree(bool p_scroll_to_selected) { bool SceneTreeEditor::_update_filter(TreeItem *p_parent, bool p_scroll_to_selected) { if (!p_parent) { p_parent = tree->get_root(); + filter_term_warning.clear(); } if (!p_parent) { @@ -704,8 +705,8 @@ bool SceneTreeEditor::_item_matches_all_terms(TreeItem *p_item, PackedStringArra return false; } } - } else { - WARN_PRINT(vformat(TTR("Special Node filter \"%s\" is not recognised. Available filters include \"type\" and \"group\"."), parameter)); + } else if (filter_term_warning.is_empty()) { + filter_term_warning = vformat(TTR("\"%s\" is not a known filter."), parameter); continue; } } else { @@ -1029,6 +1030,10 @@ String SceneTreeEditor::get_filter() const { return filter; } +String SceneTreeEditor::get_filter_term_warning() { + return filter_term_warning; +} + void SceneTreeEditor::set_undo_redo(Ref<EditorUndoRedoManager> p_undo_redo) { undo_redo = p_undo_redo; } diff --git a/editor/scene_tree_editor.h b/editor/scene_tree_editor.h index 8fbc3ab6d6..dcdfead885 100644 --- a/editor/scene_tree_editor.h +++ b/editor/scene_tree_editor.h @@ -62,6 +62,7 @@ class SceneTreeEditor : public Control { ObjectID instance_node; String filter; + String filter_term_warning; AcceptDialog *error = nullptr; AcceptDialog *warning = nullptr; @@ -142,6 +143,7 @@ public: void set_filter(const String &p_filter); String get_filter() const; + String get_filter_term_warning(); void set_undo_redo(Ref<EditorUndoRedoManager> p_undo_redo); void set_display_foreign_nodes(bool p_display); |