// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * Dialog for adding a live path effect. * * Author: * Abhay Raj Singh * * Copyright (C) 2020 Authors * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "command-palette.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "actions/actions-extra-data.h" #include "file.h" #include "gc-anchored.h" #include "include/glibmm_version.h" #include "inkscape-application.h" #include "inkscape-window.h" #include "inkscape.h" #include "io/resource.h" #include "message-context.h" #include "message-stack.h" #include "object/uri.h" #include "preferences.h" #include "ui/interface.h" #include "xml/repr.h" namespace Inkscape { class MessageStack; namespace UI { namespace Dialog { namespace { template void debug_print(T variable) { std::cerr << variable << std::endl; } } // namespace // constructor CommandPalette::CommandPalette() { // setup _builder { auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-main.glade"); try { _builder = Gtk::Builder::create_from_file(gladefile); } catch (const Glib::Error &ex) { g_warning("Glade file loading failed for command palette dialog"); return; } } // Setup Base UI Components _builder->get_widget("CPBase", _CPBase); _builder->get_widget("CPHeader", _CPHeader); _builder->get_widget("CPListBase", _CPListBase); _builder->get_widget("CPSearchBar", _CPSearchBar); _builder->get_widget("CPFilter", _CPFilter); _builder->get_widget("CPSuggestions", _CPSuggestions); _builder->get_widget("CPHistory", _CPHistory); _builder->get_widget("CPSuggestionsScroll", _CPSuggestionsScroll); _builder->get_widget("CPHistoryScroll", _CPHistoryScroll); _CPBase->add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::KEY_PRESS_MASK); // TODO: Customise on user language RTL, LTR or better user preference _CPBase->set_halign(Gtk::ALIGN_CENTER); _CPBase->set_valign(Gtk::ALIGN_START); _CPFilter->signal_key_press_event().connect(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape), false); _CPSuggestions->signal_key_press_event().connect(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape), false); _CPHistory->signal_key_press_event().connect(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape), false); set_mode(CPMode::SEARCH); _CPSuggestions->set_activate_on_single_click(); _CPSuggestions->set_selection_mode(Gtk::SELECTION_SINGLE); // Setup operations [actions, extensions] { // setup recent files { //TODO: refactor this ============================== // this code is repeated in menubar.cpp auto recent_manager = Gtk::RecentManager::get_default(); auto recent_files = recent_manager->get_items(); // all recent files not necessarily inkscape only int max_files = Inkscape::Preferences::get()->getInt("/options/maxrecentdocuments/value"); for (auto const &recent_file : recent_files) { // check if given was generated by inkscape bool valid_file = recent_file->has_application(g_get_prgname()) or recent_file->has_application("org.inkscape.Inkscape") or recent_file->has_application("inkscape") or recent_file->has_application("inkscape.exe"); valid_file = valid_file and recent_file->exists(); if (not valid_file) { continue; } if (max_files-- <= 0) { break; } append_recent_file_operation(recent_file->get_uri_display(), true, false); // open - second param true to append in _CPSuggestions append_recent_file_operation(recent_file->get_uri_display(), true, true); // import - last param true for import operation } // ================================================== } } // History management { const auto history = _history_xml.get_operation_history(); for (const auto &page : history) { // second params false to append in history switch (page.history_type) { case HistoryType::ACTION: generate_action_operation(get_action_ptr_name(page.data), false); break; case HistoryType::IMPORT_FILE: append_recent_file_operation(page.data, false, true); break; case HistoryType::OPEN_FILE: append_recent_file_operation(page.data, false, false); break; default: continue; } } } // for `enter to execute` feature _CPSuggestions->signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated)); } void CommandPalette::open() { if (not _win_doc_actions_loaded) { // loading actions can be very slow load_app_actions(); // win doc don't exist at construction so loading at first time opening Command Palette load_win_doc_actions(); _win_doc_actions_loaded = true; } _CPBase->show_all(); _CPFilter->grab_focus(); _is_open = true; } void CommandPalette::close() { _CPBase->hide(); // Reset filtering - show all suggestions _CPFilter->set_text(""); _CPSuggestions->invalidate_filter(); set_mode(CPMode::SEARCH); _is_open = false; } void CommandPalette::toggle() { if (not _is_open) { open(); return; } close(); } void CommandPalette::append_recent_file_operation(const Glib::ustring &path, bool is_suggestion, bool is_import) { static const auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-operation.glade"); Glib::RefPtr operation_builder; try { operation_builder = Gtk::Builder::create_from_file(gladefile); } catch (const Glib::Error &ex) { g_warning("Glade file loading failed for Command Palette operation dialog"); } // declaring required widgets pointers Gtk::EventBox *CPOperation; Gtk::Box *CPSynapseBox; Gtk::Label *CPGroup; Gtk::Label *CPName; Gtk::Label *CPShortcut; Gtk::Button *CPActionFullName; Gtk::Label *CPDescription; // Reading widgets operation_builder->get_widget("CPOperation", CPOperation); operation_builder->get_widget("CPSynapseBox", CPSynapseBox); operation_builder->get_widget("CPGroup", CPGroup); operation_builder->get_widget("CPName", CPName); operation_builder->get_widget("CPShortcut", CPShortcut); operation_builder->get_widget("CPActionFullName", CPActionFullName); operation_builder->get_widget("CPDescription", CPDescription); const auto file = Gio::File::create_for_path(path); if (file->query_exists()) { const Glib::ustring file_name = file->get_basename(); if (is_import) { // Used for Activate row signal of listbox and not CPGroup->set_text("import"); CPActionFullName->set_label("import"); // For filtering only } else { CPGroup->set_text("open"); CPActionFullName->set_label("open"); // For filtering only } // Hide for recent_file, not required CPActionFullName->set_no_show_all(); CPActionFullName->hide(); CPName->set_text((is_import ? _("Import") : _("Open")) + (": " + file_name)); CPName->set_tooltip_text((is_import ? ("Import") : ("Open")) + (": " + file_name)); // Tooltip_text are not translatable CPDescription->set_text(path); CPDescription->set_tooltip_text(path); { Glib::DateTime mod_time; #if GLIBMM_CHECK_VERSION(2, 62, 0) mod_time = file->query_info()->get_modification_date_time(); // Using this to reduce instead of ActionFullName widget because fullname is searched #else mod_time.create_now_local(file->query_info()->modification_time()); #endif CPShortcut->set_text(mod_time.format("%d %b %R")); } // Add to suggestions if (is_suggestion) { _CPSuggestions->append(*CPOperation); } else { _CPHistory->append(*CPOperation); } } } bool CommandPalette::generate_action_operation(const ActionPtrName &action_ptr_name, bool is_suggestion) { static const auto app = InkscapeApplication::instance(); static const auto gapp = app->gtk_app(); static InkActionExtraData &action_data = app->get_action_extra_data(); static const bool show_full_action_name = Inkscape::Preferences::get()->getBool("/options/commandpalette/showfullactionname/value"); static const auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-operation.glade"); Glib::RefPtr operation_builder; try { operation_builder = Gtk::Builder::create_from_file(gladefile); } catch (const Glib::Error &ex) { g_warning("Glade file loading failed for Command Palette operation dialog"); return false; } // declaring required widgets pointers Gtk::EventBox *CPOperation; Gtk::Box *CPSynapseBox; Gtk::Label *CPGroup; Gtk::Label *CPName; Gtk::Label *CPShortcut; Gtk::Label *CPDescription; Gtk::Button *CPActionFullName; // Reading widgets operation_builder->get_widget("CPOperation", CPOperation); operation_builder->get_widget("CPSynapseBox", CPSynapseBox); operation_builder->get_widget("CPGroup", CPGroup); operation_builder->get_widget("CPName", CPName); operation_builder->get_widget("CPShortcut", CPShortcut); operation_builder->get_widget("CPActionFullName", CPActionFullName); operation_builder->get_widget("CPDescription", CPDescription); CPGroup->set_text(action_data.get_section_for_action(Glib::ustring(action_ptr_name.second))); // Setting CPName { auto name = action_data.get_label_for_action(action_ptr_name.second); auto untranslated_name = action_data.get_label_for_action(action_ptr_name.second, false); if (name.empty()) { // If action doesn't have a label, set the name = full action name name = action_ptr_name.second; untranslated_name = action_ptr_name.second; } CPName->set_text(name); CPName->set_tooltip_text(untranslated_name); } { CPActionFullName->set_label(action_ptr_name.second); if (not show_full_action_name) { CPActionFullName->set_no_show_all(); CPActionFullName->hide(); } else { CPActionFullName->signal_clicked().connect( sigc::bind(sigc::mem_fun(*this, &CommandPalette::on_action_fullname_clicked), action_ptr_name.second), false); } } { std::vector accels = gapp->get_accels_for_action(action_ptr_name.second); std::stringstream ss; for (const auto &accel : accels) { ss << accel << ','; } std::string accel_label = ss.str(); if (not accel_label.empty()) { accel_label.pop_back(); CPShortcut->set_text(accel_label); } else { CPShortcut->set_no_show_all(); CPShortcut->hide(); } } CPDescription->set_text(action_data.get_tooltip_for_action(action_ptr_name.second)); CPDescription->set_tooltip_text(action_data.get_tooltip_for_action(action_ptr_name.second, false)); // Add to suggestions if (is_suggestion) { _CPSuggestions->append(*CPOperation); } else { _CPHistory->append(*CPOperation); } return true; } void CommandPalette::on_search() { _CPSuggestions->unset_sort_func(); _CPSuggestions->set_sort_func(sigc::mem_fun(*this, &CommandPalette::on_sort)); _search_text = _CPFilter->get_text(); _CPSuggestions->invalidate_filter(); // Remove old filter constraint and apply new one if (auto top_row = _CPSuggestions->get_row_at_y(0); top_row) { _CPSuggestions->select_row(*top_row); // select top row } } bool CommandPalette::on_filter_full_action_name(Gtk::ListBoxRow *child) { if (auto CPActionFullName = get_full_action_name(child); CPActionFullName and _search_text == CPActionFullName->get_label()) { return true; } return false; } bool CommandPalette::on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import) { auto CPActionFullName = get_full_action_name(child); if (is_import) { if (CPActionFullName and CPActionFullName->get_label() == "import") { auto [CPName, CPDescription] = get_name_desc(child); if (CPDescription && CPDescription->get_text() == _search_text) { return true; } } return false; } if (CPActionFullName and CPActionFullName->get_label() == "open") { auto [CPName, CPDescription] = get_name_desc(child); if (CPDescription && CPDescription->get_text() == _search_text) { return true; } } return false; } bool CommandPalette::on_key_press_cpfilter_escape(GdkEventKey *evt) { if (evt->keyval == GDK_KEY_Escape || evt->keyval == GDK_KEY_question) { close(); return true; // stop propagation of key press, not needed anymore } return false; // Pass the key event which are not used } bool CommandPalette::on_key_press_cpfilter_search_mode(GdkEventKey *evt) { auto key = evt->keyval; if (key == GDK_KEY_Return or key == GDK_KEY_Linefeed) { if (auto selected_row = _CPSuggestions->get_selected_row(); selected_row) { selected_row->activate(); } return true; } else if (key == GDK_KEY_Up) { if (not _CPHistory->get_children().empty()) { set_mode(CPMode::HISTORY); return true; } } return false; } bool CommandPalette::on_key_press_cpfilter_history_mode(GdkEventKey *evt) { if (evt->keyval == GDK_KEY_BackSpace) { return true; } return false; } /** * Executes action when enter pressed */ bool CommandPalette::on_key_press_cpfilter_input_mode(GdkEventKey *evt, const ActionPtrName &action_ptr_name) { switch (evt->keyval) { case GDK_KEY_Return: [[fallthrough]]; case GDK_KEY_Linefeed: execute_action(action_ptr_name, _CPFilter->get_text()); close(); return true; } return false; } void CommandPalette::hide_suggestions() { _CPBase->set_size_request(-1, 10); _CPListBase->hide(); } void CommandPalette::show_suggestions() { _CPBase->set_size_request(-1, _max_height_requestable); _CPListBase->show_all(); } void CommandPalette::on_action_fullname_clicked(const Glib::ustring &action_fullname) { static auto clipboard = Gtk::Clipboard::get(); clipboard->set_text(action_fullname); clipboard->store(); } void CommandPalette::on_row_activated(Gtk::ListBoxRow *activated_row) { // this is set to import/export or full action name const auto full_action_name = get_full_action_name(activated_row)->get_label(); if (full_action_name == "import" or full_action_name == "open") { const auto [name, description] = get_name_desc(activated_row); operate_recent_file(description->get_text(), full_action_name == "import"); } else { ask_action_parameter(get_action_ptr_name(full_action_name)); // this is an action } } void CommandPalette::on_history_selection_changed(Gtk::ListBoxRow *lb) { // set the search box text to current selection if (const auto name_label = get_name_desc(lb).first; name_label) { _CPFilter->set_text(name_label->get_text()); } } bool CommandPalette::operate_recent_file(Glib::ustring const &uri, bool const import) { static auto prefs = Inkscape::Preferences::get(); bool write_to_history = true; // if the last element in CPHistory is already this, don't update history file if (not _CPHistory->get_children().empty()) { if (const auto last_operation = _history_xml.get_last_operation(); last_operation.has_value()) { if (uri == last_operation->data) { bool last_operation_was_import = last_operation->history_type == HistoryType::IMPORT_FILE; // As previous uri is verfied to be the same as current uri we can write to history if current and // previous operation are not the same. // For example: if we want to import and previous operation was import (with same uri) we should not // write ot history, similarly if current is open and previous was open to then dont WTH. // But in case previous operation was open and current is import and vice-versa we should write to // history. if (not(import xor last_operation_was_import)) { write_to_history = false; } } } } if (import) { prefs->setBool("/options/onimport", true); file_import(SP_ACTIVE_DOCUMENT, uri, nullptr); prefs->setBool("/options/onimport", true); if (write_to_history) { _history_xml.add_import(uri); } close(); return true; } // open { get_action_ptr_name("app.file-open").first->activate(uri); if (write_to_history) { _history_xml.add_open(uri); } } close(); return true; } // namespace Dialog /** * Maybe replaced by: Temporary arrangement may be replaced by snippets * This can help us provide parameters for multiple argument function * whose actions take a string as param */ bool CommandPalette::ask_action_parameter(const ActionPtrName &action_ptr_name) { // Avoid writing same last action again // TODO: Merge the if else parts if (const auto last_of_history = _history_xml.get_last_operation(); last_of_history.has_value()) { // operation history is not empty const auto last_full_action_name = last_of_history->data; if (last_full_action_name != action_ptr_name.second) { // last action is not the same so write this one _history_xml.add_action(action_ptr_name.second); // to history file generate_action_operation(action_ptr_name, false); // to _CPHistory } } else { // History is empty so no need to check _history_xml.add_action(action_ptr_name.second); // to history file generate_action_operation(action_ptr_name, false); // to _CPHistory } // Checking if action has handleable parameter type TypeOfVariant action_param_type = get_action_variant_type(action_ptr_name.first); if (action_param_type == TypeOfVariant::UNKNOWN) { std::cerr << "CommandPalette::ask_action_parameter: unhandled action value type (Unknown Type) " << action_ptr_name.second << std::endl; return false; } if (action_param_type != TypeOfVariant::NONE) { set_mode(CPMode::INPUT); _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect( sigc::bind(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_input_mode), action_ptr_name), false); // get type string NOTE: Temporary should be replaced by adding some data to InkActionExtraDataj Glib::ustring type_string; switch (action_param_type) { case TypeOfVariant::BOOL: type_string = "bool"; break; case TypeOfVariant::INT: type_string = "integer"; break; case TypeOfVariant::DOUBLE: type_string = "double"; break; case TypeOfVariant::STRING: type_string = "string"; break; case TypeOfVariant::TUPLE_DD: type_string = "pair of doubles"; break; default: break; } const auto app = InkscapeApplication::instance(); InkActionHintData &action_hint_data = app->get_action_hint_data(); auto action_hint = action_hint_data.get_tooltip_hint_for_action(action_ptr_name.second, false); // Indicate user about what to enter FIXME Dialog generation if (action_hint.length()) { _CPFilter->set_placeholder_text(action_hint); _CPFilter->set_tooltip_text(action_hint); } else { _CPFilter->set_placeholder_text("Enter a " + type_string + "..."); _CPFilter->set_tooltip_text("Enter a " + type_string + "..."); } return true; } execute_action(action_ptr_name, ""); close(); return true; } /** * Color removal */ void CommandPalette::remove_color(Gtk::Label *label, const Glib::ustring &subject, bool tooltip) { if (tooltip) { label->set_tooltip_text(subject); } else if (label->get_use_markup()) { label->set_text(subject); } } /** * Color addition */ Glib::ustring make_bold(const Glib::ustring &search) { // TODO: Add a CSS class that changes the color of the search return "" + search + ""; } void CommandPalette::add_color(Gtk::Label *label, const Glib::ustring &search, const Glib::ustring &subject, bool tooltip) { Glib::ustring text = ""; Glib::ustring subject_string = subject.lowercase(); Glib::ustring search_string = search.lowercase(); int j = 0; if (search_string.length() > 7) { for (gunichar i : search_string) { if (i == ' ') { continue; } while (j < subject_string.length()) { if (i == subject_string[j]) { text += make_bold(Glib::Markup::escape_text(subject.substr(j, 1))); j++; break; } else { text += subject[j]; } j++; } } if (j < subject.length()) { text += Glib::Markup::escape_text(subject.substr(j)); } } else { std::map search_string_character; for (const auto &character : search_string) { search_string_character[character]++; } int subject_length = subject_string.length(); for (int i = 0; i < subject_length; i++) { if (search_string_character[subject_string[i]]--) { text += make_bold(Glib::Markup::escape_text(subject.substr(i, 1))); } else { text += subject[i]; } } } if (tooltip) { label->set_tooltip_markup(text); } else { label->set_markup(text); } } /** * Color addition for description text * Coloring complete consecutive search text in the description text */ void CommandPalette::add_color_description(Gtk::Label *label, const Glib::ustring &search) { Glib::ustring subject = label->get_text(); Glib::ustring const subject_normalize = subject.lowercase().normalize(); Glib::ustring const search_normalize = search.lowercase().normalize(); auto const position = subject_normalize.find(search_normalize); auto const search_length = search_normalize.size(); subject = Glib::Markup::escape_text(subject.substr(0, position)) + make_bold(Glib::Markup::escape_text(subject.substr(position, search_length))) + Glib::Markup::escape_text(subject.substr(position + search_length)); label->set_markup(subject); } /** * The Searching algorithm consists of fuzzy search and fuzzy points. * * Ever search of the label can contain up to three subjects to search * CPName text,CPName tooltip text,CPDescription text * * Fuzzy search searches the search text in these subjects and returns a boolean * Searching of a search text as a subsequence of the subject * * Fuzzy points give an integer of a particular search text concerning a particular subject. * Less the fuzzy point more is the precedence. * * Special case for CPDescription text search by searching text as a substring of the subject * * TODO: Adding more conditions in fuzzy points and fuzzy search for creating better user experience */ bool CommandPalette::fuzzy_tolerance_search(const Glib::ustring &subject, const Glib::ustring &search) { Glib::ustring subject_string = subject.lowercase(); Glib::ustring search_string = search.lowercase(); std::map subject_string_character, search_string_character; for (const auto &character : subject_string) { subject_string_character[character]++; } for (const auto &character : search_string) { search_string_character[character]++; } for (const auto &character : search_string_character) { auto [alphabet, occurrence] = character; if (subject_string_character[alphabet] < occurrence) { return false; } } return true; } bool CommandPalette::fuzzy_search(const Glib::ustring &subject, const Glib::ustring &search) { Glib::ustring subject_string = subject.lowercase(); Glib::ustring search_string = search.lowercase(); for (int j = 0, i = 0; i < search_string.length(); i++) { bool alphabet_present = false; while (j < subject_string.length()) { if (search_string[i] == subject_string[j]) { alphabet_present = true; j++; break; } j++; } if (!alphabet_present) { return false; // If not present } } return true; } /** * Searching the full search_text in the subject string * used for CPDescription text */ bool CommandPalette::normal_search(const Glib::ustring &subject, const Glib::ustring &search) { if (subject.lowercase().find(search.lowercase()) != -1) { return true; } return false; } /** * Calculates the fuzzy_point */ int CommandPalette::fuzzy_points(const Glib::ustring &subject, const Glib::ustring &search) { int fuzzy_cost = 100; // Taking initial fuzzy_cost as 100 constexpr int SEQUENTIAL_BONUS = -15; // bonus for adjacent matches constexpr int SEPARATOR_BONUS = -30; // bonus if search occurs after a separator constexpr int CAMEL_BONUS = -30; // bonus if search is uppercase and subject is lower constexpr int FIRST_LETTET_BONUS = -15; // bonus if the first letter is matched constexpr int LEADING_LETTER_PENALTY = +5; // penalty applied for every letter in subject before the first match constexpr int MAX_LEADING_LETTER_PENALTY = +15; // maximum penalty for leading letters constexpr int UNMATCHED_LETTER_PENALTY = +1; // penalty for every letter that doesn't matter Glib::ustring subject_string = subject.lowercase(); Glib::ustring search_string = search.lowercase(); bool sequential_compare = false; bool leading_letter = true; int total_leading_letter_penalty = 0; int j = 0, i = 0; while (i < search_string.length() && j < subject_string.length()) { if (search_string[i] != subject_string[j]) { j++; sequential_compare = false; fuzzy_cost += UNMATCHED_LETTER_PENALTY; if (leading_letter) { if (total_leading_letter_penalty < MAX_LEADING_LETTER_PENALTY) { fuzzy_cost += LEADING_LETTER_PENALTY; total_leading_letter_penalty += LEADING_LETTER_PENALTY; } } continue; } if (search_string[i] == subject_string[j]) { leading_letter = false; if (j > 0 && subject_string[j - 1] == ' ') { fuzzy_cost += SEPARATOR_BONUS; } if (i == 0 && j == 0) { fuzzy_cost += FIRST_LETTET_BONUS; } if (search[i] == subject_string[j]) { fuzzy_cost += CAMEL_BONUS; } if (sequential_compare) { fuzzy_cost += SEQUENTIAL_BONUS; } sequential_compare = true; i++; } } return fuzzy_cost; } int CommandPalette::fuzzy_tolerance_points(const Glib::ustring &subject, const Glib::ustring &search) { int fuzzy_cost = 200; // Taking initial fuzzy_cost as 200 constexpr int FIRST_LETTET_BONUS = -15; // bonus if the first letter is matched Glib::ustring subject_string = subject.lowercase(); Glib::ustring search_string = search.lowercase(); std::map search_string_character; for (const auto &character : search_string) { search_string_character[character]++; } for (const auto &character : search_string_character) { auto [alphabet, occurrence] = character; for (int i = 0; i < subject_string.length() && occurrence; i++) { if (subject_string[i] == alphabet) { if (i == 0) fuzzy_cost += FIRST_LETTET_BONUS; fuzzy_cost += i; occurrence--; } } } return fuzzy_cost; } int CommandPalette::on_filter_general(Gtk::ListBoxRow *child) { auto [CPName, CPDescription] = get_name_desc(child); if (CPName) { remove_color(CPName, CPName->get_text()); remove_color(CPName, CPName->get_tooltip_text(), true); } if (CPDescription) { remove_color(CPDescription, CPDescription->get_text()); } if (_search_text.empty()) { return 1; } // Every operation is visible if search text is empty if (CPName) { if (fuzzy_search(CPName->get_text(), _search_text)) { add_color(CPName, _search_text, CPName->get_text()); return fuzzy_points(CPName->get_text(), _search_text); } if (fuzzy_search(CPName->get_tooltip_text(), _search_text)) { add_color(CPName, _search_text, CPName->get_tooltip_text(), true); return fuzzy_points(CPName->get_tooltip_text(), _search_text); } if (fuzzy_tolerance_search(CPName->get_text(), _search_text)) { add_color(CPName, _search_text, CPName->get_text()); return fuzzy_tolerance_points(CPName->get_text(), _search_text); } if (fuzzy_tolerance_search(CPName->get_tooltip_text(), _search_text)) { add_color(CPName, _search_text, CPName->get_tooltip_text(), true); return fuzzy_tolerance_points(CPName->get_tooltip_text(), _search_text); } } if (CPDescription && normal_search(CPDescription->get_text(), _search_text)) { add_color_description(CPDescription, _search_text); return fuzzy_points(CPDescription->get_text(), _search_text); } return 0; } int CommandPalette::fuzzy_points_compare(int fuzzy_points_count_1, int fuzzy_points_count_2, int text_len_1, int text_len_2) { if (fuzzy_points_count_1 && fuzzy_points_count_2) { if (fuzzy_points_count_1 < fuzzy_points_count_2) { return -1; } else if (fuzzy_points_count_1 == fuzzy_points_count_2) { if (text_len_1 > text_len_2) { return 1; } else { return -1; } } else { return 1; } } if (fuzzy_points_count_1 == 0 && fuzzy_points_count_2) { return 1; } if (fuzzy_points_count_2 == 0 && fuzzy_points_count_1) { return -1; } return 0; } /** * compare different rows for order of display * priority of comparison * 1) CPName->get_text() * 2) CPName->get_tooltip_text() * 3) CPDescription->get_text() */ int CommandPalette::on_sort(Gtk::ListBoxRow *row1, Gtk::ListBoxRow *row2) { // tests for fuzzy_search assert(fuzzy_search("Export background", "ebo") == true); assert(fuzzy_search("Query y", "qyy") == true); assert(fuzzy_search("window close", "qt") == false); // tests for fuzzy_points assert(fuzzy_points("Export background", "ebo") == -22); assert(fuzzy_points("Query y", "qyy") == -16); assert(fuzzy_points("window close", "wc") == 2); // tests for fuzzy_tolerance_search assert(fuzzy_tolerance_search("object to path", "ebo") == true); assert(fuzzy_tolerance_search("execute verb", "qyy") == false); assert(fuzzy_tolerance_search("color mode", "moco") == true); // tests for fuzzy_tolerance_points assert(fuzzy_tolerance_points("object to path", "ebo") == 189); assert(fuzzy_tolerance_points("execute verb", "vec") == 196); assert(fuzzy_tolerance_points("color mode", "moco") == 195); if (_search_text.empty()) { return -1; } // No change in the order auto [cp_name_1, cp_description_1] = get_name_desc(row1); auto [cp_name_2, cp_description_2] = get_name_desc(row2); int fuzzy_points_count_1 = 0, fuzzy_points_count_2 = 0; int text_len_1 = 0, text_len_2 = 0; int points_compare = 0; constexpr int TOOLTIP_PENALTY = 100; constexpr int DESCRIPTION_PENALTY = 500; if (cp_name_1 && cp_name_2) { if (fuzzy_search(cp_name_1->get_text(), _search_text)) { text_len_1 = cp_name_1->get_text().length(); fuzzy_points_count_1 = fuzzy_points(cp_name_1->get_text(), _search_text); } if (fuzzy_search(cp_name_2->get_text(), _search_text)) { text_len_2 = cp_name_2->get_text().length(); fuzzy_points_count_2 = fuzzy_points(cp_name_2->get_text(), _search_text); } points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2); if (points_compare != 0) { return points_compare; } if (fuzzy_tolerance_search(cp_name_1->get_text(), _search_text)) { text_len_1 = cp_name_1->get_text().length(); fuzzy_points_count_1 = fuzzy_tolerance_points(cp_name_1->get_text(), _search_text); } if (fuzzy_tolerance_search(cp_name_2->get_text(), _search_text)) { text_len_2 = cp_name_2->get_text().length(); fuzzy_points_count_2 = fuzzy_tolerance_points(cp_name_2->get_text(), _search_text); } points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2); if (points_compare != 0) { return points_compare; } if (fuzzy_search(cp_name_1->get_tooltip_text(), _search_text)) { text_len_1 = cp_name_1->get_tooltip_text().length(); fuzzy_points_count_1 = fuzzy_points(cp_name_1->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY; } if (fuzzy_search(cp_name_2->get_tooltip_text(), _search_text)) { text_len_2 = cp_name_2->get_tooltip_text().length(); fuzzy_points_count_2 = fuzzy_points(cp_name_2->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY; } points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2); if (points_compare != 0) { return points_compare; } if (fuzzy_tolerance_search(cp_name_1->get_tooltip_text(), _search_text)) { text_len_1 = cp_name_1->get_tooltip_text().length(); fuzzy_points_count_1 = fuzzy_tolerance_points(cp_name_1->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence } if (fuzzy_tolerance_search(cp_name_2->get_tooltip_text(), _search_text)) { text_len_2 = cp_name_2->get_tooltip_text().length(); fuzzy_points_count_2 = fuzzy_tolerance_points(cp_name_2->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence } points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2); if (points_compare != 0) { return points_compare; } } if (cp_description_1 && normal_search(cp_description_1->get_text(), _search_text)) { text_len_1 = cp_description_1->get_text().length(); fuzzy_points_count_1 = fuzzy_points(cp_description_1->get_text(), _search_text) + DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence } if (cp_description_2 && normal_search(cp_description_2->get_text(), _search_text)) { text_len_2 = cp_description_2->get_text().length(); fuzzy_points_count_2 = fuzzy_points(cp_description_2->get_text(), _search_text) + DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence } points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2); if (points_compare != 0) { return points_compare; } return 0; } void CommandPalette::set_mode(CPMode mode) { switch (mode) { case CPMode::SEARCH: if (_mode == CPMode::SEARCH) { return; } _CPFilter->set_text(""); _CPFilter->set_icon_from_icon_name("edit-find-symbolic"); _CPFilter->set_placeholder_text("Search operation..."); _CPFilter->set_tooltip_text("Search operation..."); show_suggestions(); // Show Suggestions instead of history _CPHistoryScroll->set_no_show_all(); _CPHistoryScroll->hide(); _CPSuggestionsScroll->set_no_show_all(false); _CPSuggestionsScroll->show_all(); _CPSuggestions->unset_filter_func(); _CPSuggestions->set_filter_func(sigc::mem_fun(*this, &CommandPalette::on_filter_general)); _cpfilter_search_connection.disconnect(); // to be sure _cpfilter_key_press_connection.disconnect(); _cpfilter_search_connection = _CPFilter->signal_search_changed().connect(sigc::mem_fun(*this, &CommandPalette::on_search)); _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect( sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_search_mode), false); _search_text = ""; _CPSuggestions->invalidate_filter(); break; case CPMode::INPUT: if (_mode == CPMode::INPUT) { return; } _cpfilter_search_connection.disconnect(); _cpfilter_key_press_connection.disconnect(); hide_suggestions(); _CPFilter->set_text(""); _CPFilter->grab_focus(); _CPFilter->set_icon_from_icon_name("input-keyboard"); _CPFilter->set_placeholder_text("Enter action argument"); _CPFilter->set_tooltip_text("Enter action argument"); break; case CPMode::SHELL: if (_mode == CPMode::SHELL) { return; } hide_suggestions(); _CPFilter->set_icon_from_icon_name("gtk-search"); _cpfilter_search_connection.disconnect(); _cpfilter_key_press_connection.disconnect(); break; case CPMode::HISTORY: if (_mode == CPMode::HISTORY) { return; } if (_CPHistory->get_children().empty()) { return; } // Show history instead of suggestions _CPSuggestionsScroll->set_no_show_all(); _CPHistoryScroll->set_no_show_all(false); _CPSuggestionsScroll->hide(); _CPHistoryScroll->show_all(); _CPFilter->set_icon_from_icon_name("format-justify-fill"); _CPFilter->set_icon_tooltip_text(N_("History mode")); _cpfilter_search_connection.disconnect(); _cpfilter_key_press_connection.disconnect(); _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect( sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_history_mode), false); _CPHistory->signal_row_selected().connect( sigc::mem_fun(*this, &CommandPalette::on_history_selection_changed)); _CPHistory->signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated)); { // select last row const auto last_row = _CPHistory->get_row_at_index(_CPHistory->get_children().size() - 1); _CPHistory->select_row(*last_row); last_row->grab_focus(); } { // FIXME: scroll to bottom const auto adjustment = _CPHistoryScroll->get_vadjustment(); adjustment->set_value(adjustment->get_upper()); } break; } _mode = mode; } /** * Calls actions with parameters */ CommandPalette::ActionPtrName CommandPalette::get_action_ptr_name(const Glib::ustring &full_action_name) { static auto gapp = InkscapeApplication::instance()->gtk_app(); // TODO: Optimisation: only try to assign if null, make static const auto win = InkscapeApplication::instance()->get_active_window(); const auto doc = InkscapeApplication::instance()->get_active_document(); auto action_domain_string = full_action_name.substr(0, full_action_name.find('.')); // app, win, doc auto action_name = full_action_name.substr(full_action_name.find('.') + 1); ActionPtr action_ptr; if (action_domain_string == "app") { action_ptr = gapp->lookup_action(action_name); } else if (action_domain_string == "win" and win) { action_ptr = win->lookup_action(action_name); } else if (action_domain_string == "doc" and doc) { if (const auto map = doc->getActionGroup(); map) { action_ptr = map->lookup_action(action_name); } } return {action_ptr, full_action_name}; } bool CommandPalette::execute_action(const ActionPtrName &action_ptr_name, const Glib::ustring &value) { if (not value.empty()) { _history_xml.add_action_parameter(action_ptr_name.second, value); } auto [action_ptr, action_name] = action_ptr_name; switch (get_action_variant_type(action_ptr)) { case TypeOfVariant::BOOL: if (value == "1" || value == "t" || value == "true" || value.empty()) { action_ptr->activate(Glib::Variant::create(true)); } else if (value == "0" || value == "f" || value == "false") { action_ptr->activate(Glib::Variant::create(false)); } else { std::cerr << "CommandPalette::execute_action: Invalid boolean value: " << action_name << ":" << value << std::endl; } break; case TypeOfVariant::INT: try { action_ptr->activate(Glib::Variant::create(std::stoi(value))); } catch (...) { if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) { dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter an integer number.")); } } break; case TypeOfVariant::DOUBLE: try { action_ptr->activate(Glib::Variant::create(std::stod(value))); } catch (...) { if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) { dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter a decimal number.")); } } break; case TypeOfVariant::STRING: action_ptr->activate(Glib::Variant::create(value)); break; case TypeOfVariant::TUPLE_DD: try { double d0 = 0; double d1 = 0; std::vector tokens = Glib::Regex::split_simple("\\s*,\\s*", value); try { if (tokens.size() != 2) { throw std::invalid_argument("requires two numbers"); } } catch (...) { throw; } try { d0 = std::stod(tokens[0]); d1 = std::stod(tokens[1]); } catch (...) { throw; } auto variant = Glib::Variant>::create(std::tuple(d0, d1)); action_ptr->activate(variant); } catch (...) { if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) { dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter two comma separated numbers.")); } } break; case TypeOfVariant::UNKNOWN: std::cerr << "CommandPalette::execute_action: unhandled action value type (Unknown Type) " << action_name << std::endl; break; case TypeOfVariant::NONE: default: action_ptr->activate(); break; } return false; } TypeOfVariant CommandPalette::get_action_variant_type(const ActionPtr &action_ptr) { const GVariantType *gtype = g_action_get_parameter_type(action_ptr->gobj()); if (gtype) { Glib::VariantType type = action_ptr->get_parameter_type(); if (type.get_string() == "b") { return TypeOfVariant::BOOL; } else if (type.get_string() == "i") { return TypeOfVariant::INT; } else if (type.get_string() == "d") { return TypeOfVariant::DOUBLE; } else if (type.get_string() == "s") { return TypeOfVariant::STRING; } else if (type.get_string() == "(dd)") { return TypeOfVariant::TUPLE_DD; } else { std::cerr << "CommandPalette::get_action_variant_type: unknown variant type: " << type.get_string() << std::endl; return TypeOfVariant::UNKNOWN; } } // With value. return TypeOfVariant::NONE; } std::pair CommandPalette::get_name_desc(Gtk::ListBoxRow *child) { auto event_box = dynamic_cast(child->get_child()); if (event_box) { // NOTE: These variables have same name as in the glade file command-palette-operation.glade // FIXME: When structure of Gladefile of CPOperation changes, refactor this auto CPSynapseBox = dynamic_cast(event_box->get_child()); if (CPSynapseBox) { auto synapse_children = CPSynapseBox->get_children(); auto CPName = dynamic_cast(synapse_children[0]); auto CPDescription = dynamic_cast(synapse_children[1]); return std::pair(CPName, CPDescription); } } return std::pair(nullptr, nullptr); } Gtk::Button *CommandPalette::get_full_action_name(Gtk::ListBoxRow *child) { auto event_box = dynamic_cast(child->get_child()); if (event_box) { auto CPSynapseBox = dynamic_cast(event_box->get_child()); if (CPSynapseBox) { auto synapse_children = CPSynapseBox->get_children(); auto CPActionFullName = dynamic_cast(synapse_children[2]); return CPActionFullName; } } return nullptr; } void CommandPalette::load_app_actions() { auto gapp = InkscapeApplication::instance()->gtk_app(); std::vector all_actions_info; std::vector actions = gapp->list_actions(); for (const auto &action : actions) { generate_action_operation(get_action_ptr_name("app." + action), true); } } void CommandPalette::load_win_doc_actions() { if (auto window = InkscapeApplication::instance()->get_active_window(); window) { std::vector actions = window->list_actions(); for (auto action : actions) { generate_action_operation(get_action_ptr_name("win." + action), true); } if (auto document = window->get_document(); document) { auto map = document->getActionGroup(); if (map) { std::vector actions = map->list_actions(); for (auto action : actions) { generate_action_operation(get_action_ptr_name("doc." + action), true); } } else { std::cerr << "CommandPalette::load_win_doc_actions: No document map!" << std::endl; } } } } Gtk::Box *CommandPalette::get_base_widget() { return _CPBase; } // CPHistoryXML --------------------------------------------------------------- CPHistoryXML::CPHistoryXML() : _file_path(IO::Resource::profile_path("cphistory.xml")) { _xml_doc = sp_repr_read_file(_file_path.c_str(), nullptr); if (not _xml_doc) { _xml_doc = sp_repr_document_new("cphistory"); /* STRUCTURE EXAMPLE ------------------ Illustration 1 full.action_name uri uri 30 23.5 */ // Just a pointer, we don't own it, don't free/release/delete auto root = _xml_doc->root(); // add operation history in this element auto operations = _xml_doc->createElement("operations"); root->appendChild(operations); // add param history in this element auto params = _xml_doc->createElement("params"); root->appendChild(params); // This was created by allocated Inkscape::GC::release(operations); Inkscape::GC::release(params); // only save if created new save(); } // Only two children :) check and ensure Illustration 1 _operations = _xml_doc->root()->firstChild(); _params = _xml_doc->root()->lastChild(); } CPHistoryXML::~CPHistoryXML() { Inkscape::GC::release(_xml_doc); } void CPHistoryXML::add_action(const std::string &full_action_name) { add_operation(HistoryType::ACTION, full_action_name); } void CPHistoryXML::add_import(const std::string &uri) { add_operation(HistoryType::IMPORT_FILE, uri); } void CPHistoryXML::add_open(const std::string &uri) { add_operation(HistoryType::OPEN_FILE, uri); } void CPHistoryXML::add_action_parameter(const std::string &full_action_name, const std::string ¶m) { /* Creates * * +1 * + 30 * + 60 * + 90 * +1 * * * + : generally creates * +1: creates once */ const auto parameter_node = _xml_doc->createElement("param"); const auto parameter_text = _xml_doc->createTextNode(param.c_str()); parameter_node->appendChild(parameter_text); Inkscape::GC::release(parameter_text); for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->next()) { // If this action's node already exists if (full_action_name == action_iter->attribute("name")) { // If the last parameter was the same don't do anything, inner text is also a node hence 2 times last // child if (action_iter->lastChild()->lastChild() && action_iter->lastChild()->lastChild()->content() == param) { Inkscape::GC::release(parameter_node); return; } // If last current than parameter is different, add current action_iter->appendChild(parameter_node); Inkscape::GC::release(parameter_node); save(); return; } } // only encountered when the actions element doesn't already exists,so we create that action's element const auto action_node = _xml_doc->createElement("action"); action_node->setAttribute("name", full_action_name.c_str()); action_node->appendChild(parameter_node); _params->appendChild(action_node); save(); Inkscape::GC::release(action_node); Inkscape::GC::release(parameter_node); } std::optional CPHistoryXML::get_last_operation() { auto last_child = _operations->lastChild(); if (last_child) { if (const auto operation_type = _get_operation_type(last_child); operation_type.has_value()) { // inner text is a text Node thus last child return History{*operation_type, last_child->lastChild()->content()}; } } return std::nullopt; } std::vector CPHistoryXML::get_operation_history() const { // TODO: add max items in history std::vector history; for (auto operation_iter = _operations->firstChild(); operation_iter; operation_iter = operation_iter->next()) { if (const auto operation_type = _get_operation_type(operation_iter); operation_type.has_value()) { history.emplace_back(*operation_type, operation_iter->firstChild()->content()); } } return history; } std::vector CPHistoryXML::get_action_parameter_history(const std::string &full_action_name) const { std::vector params; for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->prev()) { // If this action's node already exists if (full_action_name == action_iter->attribute("name")) { // lastChild and prev for LIFO order for (auto param_iter = _params->lastChild(); param_iter; param_iter = param_iter->prev()) { params.emplace_back(param_iter->content()); } return params; } } // action not used previously so no params; return {}; } void CPHistoryXML::save() const { sp_repr_save_file(_xml_doc, _file_path.c_str()); } void CPHistoryXML::add_operation(const HistoryType history_type, const std::string &data) { std::string operation_type_name; switch (history_type) { // see Illustration 1 case HistoryType::ACTION: operation_type_name = "action"; break; case HistoryType::IMPORT_FILE: operation_type_name = "import"; break; case HistoryType::OPEN_FILE: operation_type_name = "open"; break; default: return; } auto operation_to_add = _xml_doc->createElement(operation_type_name.c_str()); // action, import, open auto operation_data = _xml_doc->createTextNode(data.c_str()); operation_data->setContent(data.c_str()); operation_to_add->appendChild(operation_data); _operations->appendChild(operation_to_add); Inkscape::GC::release(operation_data); Inkscape::GC::release(operation_to_add); save(); } std::optional CPHistoryXML::_get_operation_type(Inkscape::XML::Node *operation) { const std::string operation_type_name = operation->name(); if (operation_type_name == "action") { return HistoryType::ACTION; } else if (operation_type_name == "import") { return HistoryType::IMPORT_FILE; } else if (operation_type_name == "open") { return HistoryType::OPEN_FILE; } else { return std::nullopt; // unknown HistoryType } } } // namespace Dialog } // namespace UI } // namespace Inkscape /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :