Path: blob/master/editor/settings/editor_command_palette.cpp
9896 views
/**************************************************************************/1/* editor_command_palette.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "editor_command_palette.h"3132#include "core/os/keyboard.h"33#include "editor/editor_node.h"34#include "editor/editor_string_names.h"35#include "editor/gui/editor_toaster.h"36#include "editor/settings/editor_settings.h"37#include "editor/themes/editor_scale.h"38#include "scene/gui/control.h"39#include "scene/gui/line_edit.h"40#include "scene/gui/margin_container.h"41#include "scene/gui/tree.h"4243EditorCommandPalette *EditorCommandPalette::singleton = nullptr;4445static Rect2i prev_rect = Rect2i();46static bool was_showed = false;4748float EditorCommandPalette::_score_path(const String &p_search, const String &p_path) {49float score = 0.9f + .1f * (p_search.length() / (float)p_path.length());5051// Positive bias for matches close to the beginning of the file name.52int pos = p_path.findn(p_search);53if (pos != -1) {54return score * (1.0f - 0.1f * (float(pos) / p_path.length()));55}5657// Positive bias for matches close to the end of the path.58pos = p_path.rfindn(p_search);59if (pos != -1) {60return score * (0.8f - 0.1f * (float(p_path.length() - pos) / p_path.length()));61}6263// Remaining results belong to the same class of results.64return score * 0.69f;65}6667void EditorCommandPalette::_update_command_search(const String &search_text) {68ERR_FAIL_COND(commands.is_empty());6970HashMap<String, TreeItem *> sections;71TreeItem *first_section = nullptr;7273// Filter possible candidates.74Vector<CommandEntry> entries;75for (const KeyValue<String, Command> &E : commands) {76CommandEntry r;77r.key_name = E.key;78r.display_name = E.value.name;79r.shortcut_text = E.value.shortcut_text;80r.last_used = E.value.last_used;8182bool is_subsequence_of_key_name = search_text.is_subsequence_ofn(r.key_name);83bool is_subsequence_of_display_name = search_text.is_subsequence_ofn(r.display_name);8485if (is_subsequence_of_key_name || is_subsequence_of_display_name) {86if (!search_text.is_empty()) {87float key_name_score = is_subsequence_of_key_name ? _score_path(search_text, r.key_name.to_lower()) : .0f;88float display_name_score = is_subsequence_of_display_name ? _score_path(search_text, r.display_name.to_lower()) : .0f;8990r.score = MAX(key_name_score, display_name_score);91}9293entries.push_back(r);94}95}9697TreeItem *root = search_options->get_root();98root->clear_children();99100if (entries.is_empty()) {101get_ok_button()->set_disabled(true);102103return;104}105106if (!search_text.is_empty()) {107SortArray<CommandEntry, CommandEntryComparator> sorter;108sorter.sort(entries.ptrw(), entries.size());109} else {110SortArray<CommandEntry, CommandHistoryComparator> sorter;111sorter.sort(entries.ptrw(), entries.size());112}113114const int entry_limit = MIN(entries.size(), 300);115for (int i = 0; i < entry_limit; i++) {116String section_name = entries[i].key_name.get_slicec('/', 0);117TreeItem *section;118119if (sections.has(section_name)) {120section = sections[section_name];121} else {122section = search_options->create_item(root);123124if (!first_section) {125first_section = section;126}127128String item_name = section_name.capitalize();129section->set_text(0, item_name);130section->set_selectable(0, false);131section->set_selectable(1, false);132section->set_custom_bg_color(0, get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor)));133section->set_custom_bg_color(1, get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor)));134135sections[section_name] = section;136}137138TreeItem *ti = search_options->create_item(section);139String shortcut_text = entries[i].shortcut_text == "None" ? "" : entries[i].shortcut_text;140ti->set_text(0, entries[i].display_name);141ti->set_metadata(0, entries[i].key_name);142ti->set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT);143ti->set_text(1, shortcut_text);144Color c = get_theme_color(SceneStringName(font_color), EditorStringName(Editor)) * Color(1, 1, 1, 0.5);145ti->set_custom_color(1, c);146}147148TreeItem *to_select = first_section->get_first_child();149to_select->select(0);150to_select->set_as_cursor(0);151search_options->ensure_cursor_is_visible();152}153154void EditorCommandPalette::_bind_methods() {155ClassDB::bind_method(D_METHOD("add_command", "command_name", "key_name", "binded_callable", "shortcut_text"), &EditorCommandPalette::_add_command, DEFVAL("None"));156ClassDB::bind_method(D_METHOD("remove_command", "key_name"), &EditorCommandPalette::remove_command);157}158159void EditorCommandPalette::_notification(int p_what) {160switch (p_what) {161case NOTIFICATION_VISIBILITY_CHANGED: {162if (!is_visible()) {163prev_rect = Rect2i(get_position(), get_size());164was_showed = true;165}166} break;167168case NOTIFICATION_THEME_CHANGED: {169command_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search")));170} break;171172case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {173if (!EditorSettings::get_singleton()->check_changed_settings_in_group("shortcuts")) {174break;175}176177for (KeyValue<String, Command> &kv : commands) {178Command &c = kv.value;179if (c.shortcut.is_valid()) {180c.shortcut_text = c.shortcut->get_as_text();181}182}183} break;184}185}186187void EditorCommandPalette::_sbox_input(const Ref<InputEvent> &p_event) {188// Redirect navigational key events to the tree.189Ref<InputEventKey> key = p_event;190if (key.is_valid()) {191if (key->is_action("ui_up", true) || key->is_action("ui_down", true) || key->is_action("ui_page_up") || key->is_action("ui_page_down")) {192search_options->gui_input(key);193command_search_box->accept_event();194}195}196}197198void EditorCommandPalette::_confirmed() {199TreeItem *selected_option = search_options->get_selected();200const String command_key = selected_option != nullptr ? selected_option->get_metadata(0) : "";201if (!command_key.is_empty()) {202hide();203callable_mp(this, &EditorCommandPalette::execute_command).call_deferred(command_key);204}205}206207void EditorCommandPalette::open_popup() {208if (was_showed) {209popup(prev_rect);210} else {211_update_command_search(String());212popup_centered_clamped(Size2(600, 440) * EDSCALE, 0.8f);213}214215command_search_box->clear();216command_search_box->grab_focus();217218search_options->scroll_to_item(search_options->get_root());219}220221void EditorCommandPalette::get_actions_list(List<String> *p_list) const {222for (const KeyValue<String, Command> &E : commands) {223p_list->push_back(E.key);224}225}226227void EditorCommandPalette::remove_command(String p_key_name) {228ERR_FAIL_COND_MSG(!commands.has(p_key_name), "The Command '" + String(p_key_name) + "' doesn't exists. Unable to remove it.");229230commands.erase(p_key_name);231}232233void EditorCommandPalette::add_command(String p_command_name, String p_key_name, Callable p_action, Vector<Variant> arguments, const Ref<Shortcut> &p_shortcut) {234ERR_FAIL_COND_MSG(commands.has(p_key_name), "The Command '" + String(p_command_name) + "' already exists. Unable to add it.");235236const Variant **argptrs = (const Variant **)alloca(sizeof(Variant *) * arguments.size());237for (int i = 0; i < arguments.size(); i++) {238argptrs[i] = &arguments[i];239}240Command command;241command.name = p_command_name;242command.callable = p_action.bindp(argptrs, arguments.size());243if (p_shortcut.is_null()) {244command.shortcut_text = "None";245} else {246command.shortcut = p_shortcut;247command.shortcut_text = p_shortcut->get_as_text();248}249250commands[p_key_name] = command;251}252253void EditorCommandPalette::_add_command(String p_command_name, String p_key_name, Callable p_binded_action, String p_shortcut_text) {254ERR_FAIL_COND_MSG(commands.has(p_key_name), "The Command '" + String(p_command_name) + "' already exists. Unable to add it.");255256Command command;257command.name = p_command_name;258command.callable = p_binded_action;259command.shortcut_text = p_shortcut_text;260261// Commands added from plugins don't exist yet when the history is loaded, so we assign the last use time here if it was recorded.262Dictionary command_history = EditorSettings::get_singleton()->get_project_metadata("command_palette", "command_history", Dictionary());263if (command_history.has(p_key_name)) {264command.last_used = command_history[p_key_name];265}266267commands[p_key_name] = command;268}269270void EditorCommandPalette::execute_command(const String &p_command_key) {271ERR_FAIL_COND_MSG(!commands.has(p_command_key), p_command_key + " not found.");272commands[p_command_key].last_used = OS::get_singleton()->get_unix_time();273_save_history();274275Variant ret;276Callable::CallError ce;277const Callable &callable = commands[p_command_key].callable;278callable.callp(nullptr, 0, ret, ce);279280if (ce.error != Callable::CallError::CALL_OK) {281EditorToaster::get_singleton()->popup_str(vformat(TTR("Failed to execute command \"%s\":\n%s."), p_command_key, Variant::get_callable_error_text(callable, nullptr, 0, ce)), EditorToaster::SEVERITY_ERROR);282}283}284285void EditorCommandPalette::register_shortcuts_as_command() {286for (const KeyValue<String, Pair<String, Ref<Shortcut>>> &E : unregistered_shortcuts) {287String command_name = E.value.first;288Ref<Shortcut> shortcut = E.value.second;289Ref<InputEventShortcut> ev;290ev.instantiate();291ev->set_shortcut(shortcut);292add_command(command_name, E.key, callable_mp(EditorNode::get_singleton()->get_viewport(), &Viewport::push_input), varray(ev, false), shortcut);293}294unregistered_shortcuts.clear();295296// Load command use history.297Dictionary command_history = EditorSettings::get_singleton()->get_project_metadata("command_palette", "command_history", Dictionary());298for (const KeyValue<Variant, Variant> &history_kv : command_history) {299const String &history_key = history_kv.key;300if (commands.has(history_key)) {301commands[history_key].last_used = history_kv.value;302}303}304}305306Ref<Shortcut> EditorCommandPalette::add_shortcut_command(const String &p_command, const String &p_key, Ref<Shortcut> p_shortcut) {307if (is_inside_tree()) {308Ref<InputEventShortcut> ev;309ev.instantiate();310ev->set_shortcut(p_shortcut);311add_command(p_command, p_key, callable_mp(EditorNode::get_singleton()->get_viewport(), &Viewport::push_input), varray(ev, false), p_shortcut);312} else {313const String key_name = String(p_key);314const String command_name = String(p_command);315Pair pair = Pair(command_name, p_shortcut);316unregistered_shortcuts[key_name] = pair;317}318return p_shortcut;319}320321void EditorCommandPalette::_save_history() const {322Dictionary command_history;323324for (const KeyValue<String, Command> &E : commands) {325if (E.value.last_used > 0) {326command_history[E.key] = E.value.last_used;327}328}329EditorSettings::get_singleton()->set_project_metadata("command_palette", "command_history", command_history);330}331332EditorCommandPalette *EditorCommandPalette::get_singleton() {333if (singleton == nullptr) {334singleton = memnew(EditorCommandPalette);335}336return singleton;337}338339EditorCommandPalette::EditorCommandPalette() {340set_hide_on_ok(false);341connect(SceneStringName(confirmed), callable_mp(this, &EditorCommandPalette::_confirmed));342343VBoxContainer *vbc = memnew(VBoxContainer);344add_child(vbc);345346command_search_box = memnew(LineEdit);347command_search_box->set_placeholder(TTR("Filter Commands"));348command_search_box->set_accessibility_name(TTRC("Filter Commands"));349command_search_box->connect(SceneStringName(gui_input), callable_mp(this, &EditorCommandPalette::_sbox_input));350command_search_box->connect(SceneStringName(text_changed), callable_mp(this, &EditorCommandPalette::_update_command_search));351command_search_box->set_v_size_flags(Control::SIZE_EXPAND_FILL);352command_search_box->set_clear_button_enabled(true);353MarginContainer *margin_container_csb = memnew(MarginContainer);354margin_container_csb->add_child(command_search_box);355vbc->add_child(margin_container_csb);356register_text_enter(command_search_box);357358search_options = memnew(Tree);359search_options->connect("item_activated", callable_mp(this, &EditorCommandPalette::_confirmed));360search_options->connect(SceneStringName(item_selected), callable_mp((BaseButton *)get_ok_button(), &BaseButton::set_disabled).bind(false));361search_options->connect("nothing_selected", callable_mp((BaseButton *)get_ok_button(), &BaseButton::set_disabled).bind(true));362search_options->create_item();363search_options->set_hide_root(true);364search_options->set_columns(2);365search_options->set_v_size_flags(Control::SIZE_EXPAND_FILL);366search_options->set_h_size_flags(Control::SIZE_EXPAND_FILL);367search_options->set_column_custom_minimum_width(0, int(8 * EDSCALE));368vbc->add_child(search_options, true);369}370371Ref<Shortcut> ED_SHORTCUT_AND_COMMAND(const String &p_path, const String &p_name, Key p_keycode, String p_command_name) {372if (p_command_name.is_empty()) {373p_command_name = p_name;374}375376Ref<Shortcut> shortcut = ED_SHORTCUT(p_path, p_name, p_keycode);377EditorCommandPalette::get_singleton()->add_shortcut_command(p_command_name, p_path, shortcut);378return shortcut;379}380381Ref<Shortcut> ED_SHORTCUT_ARRAY_AND_COMMAND(const String &p_path, const String &p_name, const PackedInt32Array &p_keycodes, String p_command_name) {382if (p_command_name.is_empty()) {383p_command_name = p_name;384}385386Ref<Shortcut> shortcut = ED_SHORTCUT_ARRAY(p_path, p_name, p_keycodes);387EditorCommandPalette::get_singleton()->add_shortcut_command(p_command_name, p_path, shortcut);388return shortcut;389}390391392