diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/ui/dialog/command-palette.cpp | |
parent | Initial commit. (diff) | |
download | inkscape-upstream/1.2.2.tar.xz inkscape-upstream/1.2.2.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/ui/dialog/command-palette.cpp | 1636 |
1 files changed, 1636 insertions, 0 deletions
diff --git a/src/ui/dialog/command-palette.cpp b/src/ui/dialog/command-palette.cpp new file mode 100644 index 0000000..32d2ca9 --- /dev/null +++ b/src/ui/dialog/command-palette.cpp @@ -0,0 +1,1636 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for adding a live path effect. + * + * Author: + * Abhay Raj Singh <abhayonlyone@gmail.com> + * + * Copyright (C) 2020 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "command-palette.h" + +#include <cstddef> +#include <cstring> +#include <ctime> +#include <gdk/gdkkeysyms.h> +#include <giomm/action.h> +#include <giomm/application.h> +#include <giomm/file.h> +#include <giomm/fileinfo.h> +#include <glib/gi18n.h> +#include <glibconfig.h> +#include <glibmm/convert.h> +#include <glibmm/date.h> +#include <glibmm/error.h> +#include <glibmm/i18n.h> +#include <glibmm/markup.h> +#include <glibmm/ustring.h> +#include <gtkmm/application.h> +#include <gtkmm/box.h> +#include <gtkmm/enums.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/label.h> +#include <gtkmm/messagedialog.h> +#include <gtkmm/recentinfo.h> +#include <iostream> +#include <iterator> +#include <memory> +#include <optional> +#include <ostream> +#include <sigc++/adaptors/bind.h> +#include <sigc++/functors/mem_fun.h> +#include <string> + +#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 <typename T> +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<Gtk::Builder> 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<Gtk::Builder> 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<Glib::ustring>(sigc::mem_fun(*this, &CommandPalette::on_action_fullname_clicked), + action_ptr_name.second), + false); + } + } + + { + std::vector<Glib::ustring> 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<ActionPtrName>(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 "<span weight=\"bold\">" + search + "</span>"; +} + +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<gunichar, int> 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<gunichar, int> 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<gunichar, int> 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<bool>::create(true)); + } else if (value == "0" || value == "f" || value == "false") { + action_ptr->activate(Glib::Variant<bool>::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<int>::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<double>::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<Glib::ustring>::create(value)); + break; + case TypeOfVariant::TUPLE_DD: + try { + double d0 = 0; + double d1 = 0; + std::vector<Glib::ustring> 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<std::tuple<double, double>>::create(std::tuple<double, double>(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<Gtk::Label *, Gtk::Label *> CommandPalette::get_name_desc(Gtk::ListBoxRow *child) +{ + auto event_box = dynamic_cast<Gtk::EventBox *>(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<Gtk::Box *>(event_box->get_child()); + if (CPSynapseBox) { + auto synapse_children = CPSynapseBox->get_children(); + auto CPName = dynamic_cast<Gtk::Label *>(synapse_children[0]); + auto CPDescription = dynamic_cast<Gtk::Label *>(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<Gtk::EventBox *>(child->get_child()); + if (event_box) { + auto CPSynapseBox = dynamic_cast<Gtk::Box *>(event_box->get_child()); + if (CPSynapseBox) { + auto synapse_children = CPSynapseBox->get_children(); + auto CPActionFullName = dynamic_cast<Gtk::Button *>(synapse_children[2]); + + return CPActionFullName; + } + } + + return nullptr; +} + +void CommandPalette::load_app_actions() +{ + auto gapp = InkscapeApplication::instance()->gtk_app(); + std::vector<ActionPtrName> all_actions_info; + + std::vector<Glib::ustring> 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<Glib::ustring> 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<Glib::ustring> 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 + <cphistory> + <operations> + <action> full.action_name </action> + <import> uri </import> + <export> uri </export> + </operations> + <params> + <action name="app.transfor-rotate"> + <param> 30 </param> + <param> 23.5 </param> + </action> + </params> + </cphistory> + */ + + // 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 + * <params> + * +1 <action name="full.action-name"> + * + <param>30</param> + * + <param>60</param> + * + <param>90</param> + * +1 <action name="full.action-name"> + * <params> + * + * + : 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<History> 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<History> CPHistoryXML::get_operation_history() const +{ + // TODO: add max items in history + std::vector<History> 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<std::string> CPHistoryXML::get_action_parameter_history(const std::string &full_action_name) const +{ + std::vector<std::string> 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<HistoryType> 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 : |