summaryrefslogtreecommitdiffstats
path: root/src/ui/dialog/command-palette.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
commitcca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch)
tree146f39ded1c938019e1ed42d30923c2ac9e86789 /src/ui/dialog/command-palette.cpp
parentInitial commit. (diff)
downloadinkscape-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.cpp1636
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 &param)
+{
+ /* 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 :