Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
command-palette.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
5/* Author:
6 * Abhay Raj Singh <abhayonlyone@gmail.com>
7 *
8 * Copyright (C) 2020 Authors
9 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
10 */
11
12#include "command-palette.h"
13
14#include <glibmm/i18n.h>
15#include <gtkmm/accelerator.h>
16#include <gtkmm/adjustment.h>
17#include <gtkmm/eventcontrollerfocus.h>
18#include <gtkmm/eventcontrollerkey.h>
19#include <gtkmm/listbox.h>
20#include <gtkmm/recentmanager.h>
21#include <gtkmm/scrolledwindow.h>
22#include <gtkmm/searchentry2.h>
23
24#include "desktop.h"
25#include "document.h"
26#include "file.h"
27#include "inkscape.h"
28#include "preferences.h"
30#include "inkscape-window.h"
31#include "io/resource.h"
32#include "ui/builder-utils.h"
33#include "ui/shortcuts.h"
34#include "ui/util.h"
35
38
39namespace Inkscape::UI::Dialog {
40
42 : _builder(create_builder("command-palette-main.glade"))
43 , _CPBase (get_widget<Gtk::Box>(_builder, "CPBase"))
44 , _CPListBase (get_widget<Gtk::Box>(_builder, "CPListBase"))
45 , _CPFilter (get_widget<Gtk::SearchEntry2>(_builder, "CPFilter"))
46 , _CPSuggestions (get_widget<Gtk::ListBox>(_builder, "CPSuggestions"))
47 , _CPHistory (get_widget<Gtk::ListBox>(_builder, "CPHistory"))
48 , _CPSuggestionsScroll (get_widget<Gtk::ScrolledWindow>(_builder, "CPSuggestionsScroll"))
49 , _CPHistoryScroll (get_widget<Gtk::ScrolledWindow>(_builder, "CPHistoryScroll"))
50{
51 // TODO: Move to a test program.
52 test_sort();
53
54 // TODO: Customise on user language RTL, LTR or better user preference
55 _CPBase.set_halign(Gtk::Align::CENTER);
56 _CPBase.set_valign(Gtk::Align::START);
57
58 // Close the CommandPalette when the toplevel Window receives an Escape key press.
59 // & also when the focused widget of said window is no longer a descendent of the Palette.
60 auto const key = Gtk::EventControllerKey::create();
61 key->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
62 key->signal_key_pressed().connect(sigc::mem_fun(*this, &CommandPalette::on_key_pressed), true);
63 _CPBase.add_controller(key);
64
65 auto focus = Gtk::EventControllerFocus::create();
66 focus->property_contains_focus().signal_changed().connect([this, &focus = *focus] {
67 if (!focus.contains_focus()) {
68 close();
69 }
70 });
71 _CPBase.add_controller(focus);
72
73 // Fixme (GTKmm): signal_activate() not wrapped.
74 g_signal_connect(_CPFilter.gobj(), "activate", G_CALLBACK(+[] (GtkSearchEntry *self, void *data) {
75 auto cp = reinterpret_cast<CommandPalette *>(data);
77 }), this);
78
79 auto const entry_key = Gtk::EventControllerKey::create();
80 entry_key->signal_key_pressed().connect([this](auto &&...args) { return on_entry_keypress(args...); }, true);
81 _CPFilter.add_controller(entry_key);
82
84
85 _CPSuggestions.set_activate_on_single_click();
86 _CPSuggestions.set_selection_mode(Gtk::SelectionMode::SINGLE);
87
88 // Setup operations [actions, extensions]
89 {
90 // setup recent files
91 {
92 //TODO: refactor this ==============================
93 // this code is repeated in menubar.cpp
94 auto recent_manager = Gtk::RecentManager::get_default();
95 auto recent_files = recent_manager->get_items(); // all recent files not necessarily inkscape only
96 int max_files = Inkscape::Preferences::get()->getInt("/options/maxrecentdocuments/value");
97 Glib::ustring const prgname = g_get_prgname();
98
99 for (auto const &recent_file : recent_files) {
100 // check if given was generated by inkscape
101 bool valid_file = recent_file->has_application(prgname) or
102 recent_file->has_application("org.inkscape.Inkscape") or
103 recent_file->has_application("inkscape") or
104 recent_file->has_application("inkscape.exe");
105
106 // Note: Do not check if the file exists, to avoid long delays. See https://gitlab.com/inkscape/inkscape/-/issues/2348 .
107
108 if (!valid_file) {
109 continue;
110 }
111
112 if (max_files-- <= 0) {
113 break;
114 }
115
116 append_recent_file_operation(recent_file->get_uri_display(), true,
117 false); // open - second param true to append in _CPSuggestions
118 append_recent_file_operation(recent_file->get_uri_display(), true,
119 true); // import - last param true for import operation
120 }
121 // ==================================================
122 }
123 }
124
125 // History management
126 {
127 const auto history = _history_xml.get_operation_history();
128
129 for (const auto &page : history) {
130 // second params false to append in history
131 switch (page.history_type) {
134 break;
136 append_recent_file_operation(page.data, false, true);
137 break;
139 append_recent_file_operation(page.data, false, false);
140 break;
141 default:
142 continue;
143 }
144 }
145 }
146 // for `enter to execute` feature
147 _CPSuggestions.signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated));
148}
149
151{
153 // loading actions can be very slow
155 // win doc don't exist at construction so loading at first time opening Command Palette
158 }
159
160 _CPBase.set_visible(true);
161
162 _CPFilter.grab_focus();
163
164 _is_open = true;
165}
166
168{
169 _CPBase.set_visible(false);
170
171 // Reset filtering - show all suggestions
172 _CPFilter.set_text("");
173 _CPSuggestions.invalidate_filter();
174
176
177 _is_open = false;
178}
179
181{
182 _is_open ? close() : open();
183}
184
185void CommandPalette::append_recent_file_operation(const Glib::ustring &path, bool is_suggestion, bool is_import)
186{
187 auto operation_builder = create_builder("command-palette-operation.glade");
188 auto &CPOperation (get_widget<Gtk::Box> (operation_builder, "CPOperation"));
189 auto &CPGroup (get_widget<Gtk::Label> (operation_builder, "CPGroup"));
190 auto &CPName (get_widget<Gtk::Label> (operation_builder, "CPName"));
191 auto &CPActionFullButton (get_widget<Gtk::Button>(operation_builder, "CPActionFullButton"));
192 auto &CPActionFullLabel (get_widget<Gtk::Label> (operation_builder, "CPActionFullLabel"));
193 auto &CPDescription (get_widget<Gtk::Label> (operation_builder, "CPDescription"));
194
195 const auto file = Gio::File::create_for_path(path);
196 const Glib::ustring file_name = file->get_basename();
197
198 if (is_import) {
199 // Used for Activate row signal of listbox and not
200 CPGroup.set_text("import");
201 CPActionFullLabel.set_text("import"); // For filtering only
202
203 } else {
204 CPGroup.set_text("open");
205 CPActionFullLabel.set_text("open"); // For filtering only
206 }
207
208 // Hide for recent_file, not required
209 CPActionFullButton.set_visible(false);
210
211 CPName.set_text((is_import ? _("Import") : _("Open")) + (": " + file_name));
212 CPName.set_tooltip_text((is_import ? ("Import") : ("Open")) + (": " + file_name)); // Tooltip_text are not translatable
213 CPDescription.set_text(path);
214 CPDescription.set_tooltip_text(path);
215
216 // Add to suggestions
217 if (is_suggestion) {
218 _CPSuggestions.append(CPOperation);
219 } else {
220 _CPHistory.append(CPOperation);
221 }
222}
223
224bool CommandPalette::generate_action_operation(const ActionPtrName &action_ptr_name, bool is_suggestion)
225{
226 static const auto app = InkscapeApplication::instance();
227 static const InkActionExtraData &action_data = app->get_action_extra_data();
228 static const bool show_full_action_name =
229 Inkscape::Preferences::get()->getBool("/options/commandpalette/showfullactionname/value");
230
231 auto operation_builder = create_builder("command-palette-operation.glade");
232 auto &CPOperation (get_widget<Gtk::Box> (operation_builder, "CPOperation"));
233 auto &CPGroup (get_widget<Gtk::Label> (operation_builder, "CPGroup"));
234 auto &CPName (get_widget<Gtk::Label> (operation_builder, "CPName"));
235 auto &CPShortcut (get_widget<Gtk::Label> (operation_builder, "CPShortcut"));
236 auto &CPActionFullButton (get_widget<Gtk::Button>(operation_builder, "CPActionFullButton"));
237 auto &CPActionFullLabel (get_widget<Gtk::Label> (operation_builder, "CPActionFullLabel"));
238 auto &CPDescription (get_widget<Gtk::Label> (operation_builder, "CPDescription"));
239
240 CPGroup.set_text(action_data.get_section_for_action(action_ptr_name.second));
241
242 // Setting CPName
243 {
244 auto name = action_data.get_label_for_action(action_ptr_name.second);
245 auto untranslated_name = action_data.get_label_for_action(action_ptr_name.second, false);
246 if (name.empty()) {
247 // If action doesn't have a label, set the name = full action name
248 name = action_ptr_name.second;
249 untranslated_name = action_ptr_name.second;
250 }
251
252 CPName.set_text(name);
253 CPName.set_tooltip_text(untranslated_name);
254 }
255
256 CPActionFullLabel.set_text(action_ptr_name.second);
257
258 if (not show_full_action_name) {
259 CPActionFullButton.set_visible(false);
260 } else {
261 CPActionFullButton.signal_clicked().connect(
262 sigc::bind(sigc::mem_fun(*this, &CommandPalette::on_action_fullname_clicked),
263 action_ptr_name.second),
264 false);
265 }
266
267 {
268 auto const &accels = Shortcuts::getInstance().get_triggers(action_ptr_name.second);
269 std::string accel_label;
270 for (const auto &accel : accels) {
271 guint key = 0;
272 Gdk::ModifierType mods;
273 Gtk::Accelerator::parse(accel, key, mods);
274 Glib::ustring label = Gtk::Accelerator::get_label(key, mods);
275 accel_label.append(label.raw()).append(1, ' ');
276 }
277
278 if (not accel_label.empty()) {
279 accel_label.pop_back();
280 CPShortcut.set_text(accel_label);
281 } else {
282 CPShortcut.set_visible(false);
283 }
284 }
285
286 CPDescription.set_text(action_data.get_tooltip_for_action(action_ptr_name.second));
287 CPDescription.set_tooltip_text(action_data.get_tooltip_for_action(action_ptr_name.second, false));
288
289 // Add to suggestions
290 if (is_suggestion) {
291 _CPSuggestions.append(CPOperation);
292 } else {
293 _CPHistory.append(CPOperation);
294 }
295
296 return true;
297}
298
300{
301 // TODO: Why is this done here? It seems very wasteful! But I didnʼt get anything else to work,
302 // for example by setting it elsewhere and then only invalidating it here. Ponder more...
303 // Although in saying that, TODO: GTK4: Consider porting this whole thing to GtkListView.
304 _CPSuggestions.unset_sort_func();
305 _CPSuggestions.set_sort_func(sigc::mem_fun(*this, &CommandPalette::on_sort));
306
307 _search_text = _CPFilter.get_text();
308
309 _CPSuggestions.invalidate_filter(); // Remove old filter constraint and apply new one
310
311 if (auto top_row = _CPSuggestions.get_row_at_y(0); top_row) {
312 _CPSuggestions.select_row(*top_row); // select top row
313 }
314
315 _CPSuggestionsScroll.get_vadjustment()->set_value(0);
316}
317
319{
320 auto CPActionFullLabel = get_full_action_name(child);
321 return CPActionFullLabel && _search_text == CPActionFullLabel->get_text();
322}
323
324bool CommandPalette::on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import)
325{
326 auto CPActionFullLabel = get_full_action_name(child);
327 if (is_import) {
328 if (CPActionFullLabel && CPActionFullLabel->get_text() == "import") {
329 auto [_, CPDescription] = get_name_desc(child);
330 if (CPDescription && CPDescription->get_text() == _search_text) {
331 return true;
332 }
333 }
334 return false;
335 }
336 if (CPActionFullLabel && CPActionFullLabel->get_text() == "open") {
337 auto [_, CPDescription] = get_name_desc(child);
338 if (CPDescription && CPDescription->get_text() == _search_text) {
339 return true;
340 }
341 }
342 return false;
343}
344
345bool CommandPalette::on_key_pressed(unsigned keyval, unsigned /*keycode*/, Gdk::ModifierType /*state*/)
346{
347 g_return_val_if_fail(_is_open, false);
348
349 if (keyval == GDK_KEY_Escape || keyval == GDK_KEY_question) {
350 close();
351 return true; // stop propagation of key press, not needed anymore
352 }
353
354 return false; // Pass the key event which are not used
355}
356
358{
359 if (_mode == CPMode::SEARCH) {
360 if (auto selected_row = _CPSuggestions.get_selected_row()) {
361 selected_row->activate();
362 }
363 } else if (_mode == CPMode::INPUT) {
364 execute_action(_ask_action_ptr_name.value(), _CPFilter.get_text());
365 _ask_action_ptr_name.reset();
366 close();
367 }
368}
369
370bool CommandPalette::on_entry_keypress(unsigned keyval, unsigned, Gdk::ModifierType)
371{
372 if (_mode != CPMode::SEARCH) return false;
373
374 if (keyval == GDK_KEY_Up) {
376 return true;
377 } else if (keyval == GDK_KEY_Down) {
378 if (auto row = _CPSuggestions.get_row_at_index(0)) {
379 // Go to first row of suggestions.
380 _CPSuggestions.select_row(*row);
381 row->grab_focus();
382 return true;
383 }
384 }
385
386 return false;
387}
388
390{
391 _CPBase.set_size_request(-1, 10);
392 _CPListBase.set_visible(false);
393}
394
396{
397 _CPBase.set_size_request(-1, _max_height_requestable);
398 _CPListBase.set_visible(true);
399}
400
401void CommandPalette::on_action_fullname_clicked(const Glib::ustring &action_fullname)
402{
403 auto clipboard = Gdk::Display::get_default()->get_clipboard();
404 clipboard->set_text(action_fullname);
405}
406
407void CommandPalette::on_row_activated(Gtk::ListBoxRow *activated_row)
408{
409 // this is set to import/export or full action name
410 auto full_action_name = get_full_action_name(activated_row)->get_label();
411 if (full_action_name == "import" or full_action_name == "open") {
412 const auto [name, description] = get_name_desc(activated_row);
413 operate_recent_file(description->get_text(), full_action_name == "import");
414 } else {
415 ask_action_parameter(get_action_ptr_name(std::move(full_action_name)));
416 // this is an action
417 }
418}
419
421{
422 // set the search box text to current selection
423 if (const auto name_label = get_name_desc(lb).first; name_label) {
424 _CPFilter.set_text(name_label->get_text());
425 }
426}
427
428bool CommandPalette::operate_recent_file(Glib::ustring const &uri, bool const import)
429{
430 bool write_to_history = true;
431
432 // if the last element in CPHistory is already this, don't update history file
433 if (!UI::get_children(_CPHistory).empty()) {
434 if (const auto last_operation = _history_xml.get_last_operation(); last_operation.has_value()) {
435 if (uri.raw() == last_operation->data) {
436 bool last_operation_was_import = last_operation->history_type == HistoryType::IMPORT_FILE;
437 // As previous uri is verfied to be the same as current uri we can write to history if current and
438 // previous operation are not the same.
439 // For example: if we want to import and previous operation was import (with same uri) we should not
440 // write ot history, similarly if current is open and previous was open to then dont WTH.
441 // But in case previous operation was open and current is import and vice-versa we should write to
442 // history.
443 if (not(import xor last_operation_was_import)) {
444 write_to_history = false;
445 }
446 }
447 }
448 }
449
450 if (import) {
451 file_import(SP_ACTIVE_DOCUMENT, uri, nullptr);
452
453 if (write_to_history) {
455 }
456
457 close();
458 return true;
459 }
460
461 // open
462 {
463 get_action_ptr_name("app.file-open-window").first->activate(uri);
464 if (write_to_history) {
466 }
467 }
468
469 close();
470 return true;
471}
472
473static void set_hint_texts(Gtk::SearchEntry2 &entry, Glib::ustring const &text)
474{
475 entry.set_placeholder_text(text);
476 entry.set_tooltip_text (text);
477}
478
485{
486 _ask_action_ptr_name.emplace(action_ptr_name);
487
488 // Avoid writing same last action again
489 // TODO: Merge the if else parts
490 if (const auto last_of_history = _history_xml.get_last_operation(); last_of_history.has_value()) {
491 // operation history is not empty
492 const auto last_full_action_name = last_of_history->data;
493 if (last_full_action_name != action_ptr_name.second.raw()) {
494 // last action is not the same so write this one
495 _history_xml.add_action(action_ptr_name.second); // to history file
496 generate_action_operation(action_ptr_name, false); // to _CPHistory
497 }
498 } else {
499 // History is empty so no need to check
500 _history_xml.add_action(action_ptr_name.second); // to history file
501 generate_action_operation(action_ptr_name, false); // to _CPHistory
502 }
503
504 // Checking if action has handleable parameter type
505 TypeOfVariant action_param_type = get_action_variant_type(action_ptr_name.first);
506 if (action_param_type == TypeOfVariant::UNKNOWN) {
507 std::cerr << "CommandPalette::ask_action_parameter: unhandled action value type (Unknown Type) "
508 << action_ptr_name.second.raw() << std::endl;
509 return false;
510 }
511
512 if (action_param_type != TypeOfVariant::NONE) {
514
515 // get type string NOTE: Temporary should be replaced by adding some data to InkActionExtraDataj
516 Glib::ustring type_string;
517 switch (action_param_type) {
519 type_string = _("boolean");
520 break;
522 type_string = _("whole number");
523 break;
525 type_string = _("decimal number");
526 break;
528 type_string = _("text string");
529 break;
531 type_string = _("pair of decimal numbers");
532 break;
533 default:
534 break;
535 }
536
537 const auto app = InkscapeApplication::instance();
538 InkActionHintData &action_hint_data = app->get_action_hint_data();
539 auto action_hint = action_hint_data.get_tooltip_hint_for_action(action_ptr_name.second, false);
540
541 // Indicate user about what to enter FIXME Dialog generation
542 if (action_hint.empty()) {
543 /* TRANSLATORS: %1 will be replaced with the type of parameter
544 * expected by the action, e.g., “whole number”. */
545 action_hint = Glib::ustring::compose(_("Enter a %1..."), type_string);
546 }
547 set_hint_texts(_CPFilter, action_hint);
548
549 return true;
550 }
551
552 execute_action(action_ptr_name, "");
553 close();
554
555 return true;
556}
557
561void CommandPalette::remove_color(Gtk::Label *label, const Glib::ustring &subject, bool tooltip)
562{
563 /* if (tooltip) {
564 label->set_tooltip_text(subject);
565 } else if (label->get_use_markup()) {
566 label->set_text(subject);
567 } */
568}
569
573Glib::ustring make_bold(const Glib::ustring &search)
574{
575 // TODO: Add a CSS class that changes the color of the search
576 return "<span weight=\"bold\">" + search + "</span>";
577}
578
579void CommandPalette::add_color(Gtk::Label *label, const Glib::ustring &search, const Glib::ustring &subject, bool tooltip)
580{
581 //is no working on master fill all chars so I comment to speedup
582 /* Glib::ustring text = "";
583 Glib::ustring subject_string = subject.lowercase();
584 Glib::ustring search_string = search.lowercase();
585 int j = 0;
586
587 if (search_string.length() > 7) {
588 for (gunichar i : search_string) {
589 if (i == ' ') {
590 continue;
591 }
592 while (j < subject_string.length()) {
593 if (i == subject_string[j]) {
594 text += make_bold(Glib::Markup::escape_text(subject.substr(j, 1)));
595 j++;
596 break;
597 } else {
598 text += Glib::Markup::escape_text(subject.substr(j, 1));
599 }
600 j++;
601 }
602 }
603 if (j < subject.length()) {
604 text += Glib::Markup::escape_text(subject.substr(j));
605 }
606 } else {
607 std::map<gunichar, int> search_string_character;
608
609 for (const auto &character : search_string) {
610 search_string_character[character]++;
611 }
612
613 int subject_length = subject_string.length();
614
615 for (int i = 0; i < subject_length; i++) {
616 if (search_string_character[subject_string[i]]--) {
617 text += make_bold(Glib::Markup::escape_text(subject.substr(i, 1)));
618 } else {
619 text += Glib::Markup::escape_text(subject.substr(i, 1));
620 }
621 }
622 }
623
624 if (tooltip) {
625 label->set_tooltip_markup(text);
626 } else {
627 label->set_markup(text);
628 } */
629}
630
635void CommandPalette::add_color_description(Gtk::Label *label, const Glib::ustring &search)
636{
637 /* Glib::ustring subject = label->get_text();
638
639 Glib::ustring const subject_normalize = subject.lowercase().normalize();
640 Glib::ustring const search_normalize = search.lowercase().normalize();
641
642 auto const position = subject_normalize.find(search_normalize);
643 auto const search_length = search_normalize.size();
644
645 subject = Glib::Markup::escape_text(subject.substr(0, position)) +
646 make_bold(Glib::Markup::escape_text(subject.substr(position, search_length))) +
647 Glib::Markup::escape_text(subject.substr(position + search_length));
648
649 label->set_markup(subject); */
650}
651
669bool CommandPalette::fuzzy_tolerance_search(const Glib::ustring &subject, const Glib::ustring &search)
670{
671 Glib::ustring subject_string = subject.lowercase();
672 Glib::ustring search_string = search.lowercase();
673 std::map<gunichar, int> subject_string_character, search_string_character;
674 for (const auto &character : subject_string) {
675 subject_string_character[character]++;
676 }
677 for (const auto &character : search_string) {
678 search_string_character[character]++;
679 }
680 for (const auto &character : search_string_character) {
681 const auto &[alphabet, occurrence] = character;
682 if (subject_string_character[alphabet] < occurrence) {
683 return false;
684 }
685 }
686 return true;
687}
688
689bool CommandPalette::fuzzy_search(const Glib::ustring &subject, const Glib::ustring &search)
690{
691 Glib::ustring subject_string = subject.lowercase();
692 Glib::ustring search_string = search.lowercase();
693
694 for (int j = 0, i = 0; i < search_string.length(); i++) {
695 bool alphabet_present = false;
696
697 while (j < subject_string.length()) {
698 if (search_string[i] == subject_string[j]) {
699 alphabet_present = true;
700 j++;
701 break;
702 }
703 j++;
704 }
705
706 if (!alphabet_present) {
707 return false; // If not present
708 }
709 }
710
711 return true;
712}
713
718bool CommandPalette::normal_search(const Glib::ustring &subject, const Glib::ustring &search)
719{
720 if (subject.lowercase().find(search.lowercase()) != -1) {
721 return true;
722 }
723 return false;
724}
725
729int CommandPalette::fuzzy_points(const Glib::ustring &subject, const Glib::ustring &search)
730{
731 int fuzzy_cost = 100; // Taking initial fuzzy_cost as 100
732
733 constexpr int SEQUENTIAL_BONUS = -15; // bonus for adjacent matches
734 constexpr int SEPARATOR_BONUS = -30; // bonus if search occurs after a separator
735 constexpr int CAMEL_BONUS = -30; // bonus if search is uppercase and subject is lower
736 constexpr int FIRST_LETTER_BONUS = -15; // bonus if the first letter is matched
737 constexpr int LEADING_LETTER_PENALTY = +5; // penalty applied for every letter in subject before the first match
738 constexpr int MAX_LEADING_LETTER_PENALTY = +15; // maximum penalty for leading letters
739 constexpr int UNMATCHED_LETTER_PENALTY = +1; // penalty for every letter that doesn't matter
740
741 Glib::ustring subject_string = subject.lowercase();
742 Glib::ustring search_string = search.lowercase();
743
744 bool sequential_compare = false;
745 bool leading_letter = true;
746 int total_leading_letter_penalty = 0;
747 int j = 0, i = 0;
748
749 while (i < search_string.length() && j < subject_string.length()) {
750 if (search_string[i] != subject_string[j]) {
751 j++;
752 sequential_compare = false;
753 fuzzy_cost += UNMATCHED_LETTER_PENALTY;
754
755 if (leading_letter) {
756 if (total_leading_letter_penalty < MAX_LEADING_LETTER_PENALTY) {
757 fuzzy_cost += LEADING_LETTER_PENALTY;
758 total_leading_letter_penalty += LEADING_LETTER_PENALTY;
759 }
760 }
761
762 continue;
763 }
764
765 if (search_string[i] == subject_string[j]) {
766 leading_letter = false;
767
768 if (j > 0 && subject_string[j - 1] == ' ') {
769 fuzzy_cost += SEPARATOR_BONUS;
770 }
771
772 if (i == 0 && j == 0) {
773 fuzzy_cost += FIRST_LETTER_BONUS;
774 }
775
776 if (search[i] == subject_string[j]) {
777 fuzzy_cost += CAMEL_BONUS;
778 }
779
780 if (sequential_compare) {
781 fuzzy_cost += SEQUENTIAL_BONUS;
782 }
783
784 sequential_compare = true;
785 i++;
786 }
787 }
788
789 return fuzzy_cost;
790}
791
792int CommandPalette::fuzzy_tolerance_points(const Glib::ustring &subject, const Glib::ustring &search)
793{
794 int fuzzy_cost = 200; // Taking initial fuzzy_cost as 200
795 constexpr int FIRST_LETTER_BONUS = -15; // bonus if the first letter is matched
796
797 Glib::ustring subject_string = subject.lowercase();
798 Glib::ustring search_string = search.lowercase();
799 std::map<gunichar, int> search_string_character;
800
801 for (const auto &character : search_string) {
802 search_string_character[character]++;
803 }
804
805 for (auto [alphabet, occurrence] : search_string_character) {
806 for (int i = 0; i < subject_string.length() && occurrence; i++) {
807 if (subject_string[i] == alphabet) {
808 if (i == 0)
809 fuzzy_cost += FIRST_LETTER_BONUS;
810 fuzzy_cost += i;
811 occurrence--;
812 }
813 }
814 }
815
816 return fuzzy_cost;
817}
818
820{
821 auto [CPName, CPDescription] = get_name_desc(child);
822
823 /*
824 if (CPName) {
825 remove_color(CPName, CPName->get_text());
826 remove_color(CPName, CPName->get_tooltip_text(), true);
827 }
828 if (CPDescription) {
829 remove_color(CPDescription, CPDescription->get_text());
830 }
831 */
832
833 if (_search_text.empty()) {
834 return 1;
835 } // Every operation is visible if search text is empty
836
837 if (CPName) {
838 auto const &name_text = CPName->get_text();
839
840 if (fuzzy_search(name_text, _search_text)) {
841 // add_color(CPName, _search_text, name_text);
842 return fuzzy_points(name_text, _search_text);
843 }
844
845 auto const &name_tooltip = CPName->get_tooltip_text();
846
847 if (fuzzy_search(name_tooltip, _search_text)) {
848 // add_color(CPName, _search_text, name_tooltip, true);
849 return fuzzy_points(name_tooltip, _search_text);
850 }
851
852 if (fuzzy_tolerance_search(name_text, _search_text)) {
853 // add_color(CPName, _search_text, name_text);
854 return fuzzy_tolerance_points(name_text, _search_text);
855 }
856
857 if (fuzzy_tolerance_search(name_tooltip, _search_text)) {
858 // add_color(CPName, _search_text, name_tooltip, true);
859 return fuzzy_tolerance_points(name_tooltip, _search_text);
860 }
861 }
862
863 if (CPDescription) {
864 auto const &description_text = CPDescription->get_text();
865 if (normal_search(description_text, _search_text)) {
866 // add_color_description(CPDescription, _search_text);
867 return fuzzy_points(description_text, _search_text);
868 }
869 }
870
871 return 0;
872}
873
874int CommandPalette::fuzzy_points_compare(int fuzzy_points_count_1, int fuzzy_points_count_2, int text_len_1,
875 int text_len_2)
876{
877 if (fuzzy_points_count_1 && fuzzy_points_count_2) {
878 if (fuzzy_points_count_1 < fuzzy_points_count_2) {
879 return -1;
880 } else if (fuzzy_points_count_1 == fuzzy_points_count_2) {
881 if (text_len_1 > text_len_2) {
882 return 1;
883 } else {
884 return -1;
885 }
886 } else {
887 return 1;
888 }
889 }
890
891 if (fuzzy_points_count_1 == 0 && fuzzy_points_count_2) {
892 return 1;
893 }
894 if (fuzzy_points_count_2 == 0 && fuzzy_points_count_1) {
895 return -1;
896 }
897
898 return 0;
899}
900
901// TODO: Move to a test program.
903{
904 // tests for fuzzy_search
905 assert(fuzzy_search("Export background", "ebo") == true);
906 assert(fuzzy_search("Query y", "qyy") == true);
907 assert(fuzzy_search("window close", "qt") == false);
908
909 // tests for fuzzy_points
910 assert(fuzzy_points("Export background", "ebo") == -22);
911 assert(fuzzy_points("Query y", "qyy") == -16);
912 assert(fuzzy_points("window close", "wc") == 2);
913
914 // tests for fuzzy_tolerance_search
915 assert(fuzzy_tolerance_search("object to path", "ebo") == true);
916 assert(fuzzy_tolerance_search("execute verb", "qyy") == false);
917 assert(fuzzy_tolerance_search("color mode", "moco") == true);
918
919 // tests for fuzzy_tolerance_points
920 assert(fuzzy_tolerance_points("object to path", "ebo") == 189);
921 assert(fuzzy_tolerance_points("execute verb", "vec") == 196);
922 assert(fuzzy_tolerance_points("color mode", "moco") == 195);
923}
924
932int CommandPalette::on_sort(Gtk::ListBoxRow *row1, Gtk::ListBoxRow *row2)
933{
934 if (_search_text.empty()) {
935 return -1;
936 } // No change in the order
937
938 auto [cp_name_1, cp_description_1] = get_name_desc(row1);
939 auto [cp_name_2, cp_description_2] = get_name_desc(row2);
940
941 int fuzzy_points_count_1 = 0, fuzzy_points_count_2 = 0;
942 int text_len_1 = 0, text_len_2 = 0;
943 int points_compare = 0;
944
945 constexpr int TOOLTIP_PENALTY = 100;
946 constexpr int DESCRIPTION_PENALTY = 500;
947
948 if (cp_name_1 && cp_name_2) {
949 auto const &name_1_text = cp_name_1->get_text();
950 auto const &name_2_text = cp_name_2->get_text();
951
952 if (fuzzy_search(name_1_text, _search_text)) {
953 text_len_1 = name_1_text.length();
954 fuzzy_points_count_1 = fuzzy_points(name_1_text, _search_text);
955 }
956 if (fuzzy_search(name_2_text, _search_text)) {
957 text_len_2 = name_2_text.length();
958 fuzzy_points_count_2 = fuzzy_points(name_2_text, _search_text);
959 }
960
961 points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
962 if (points_compare != 0) {
963 return points_compare;
964 }
965
966 if (fuzzy_tolerance_search(name_1_text, _search_text)) {
967 text_len_1 = name_1_text.length();
968 fuzzy_points_count_1 = fuzzy_tolerance_points(name_1_text, _search_text);
969 }
970 if (fuzzy_tolerance_search(name_2_text, _search_text)) {
971 text_len_2 = name_2_text.length();
972 fuzzy_points_count_2 = fuzzy_tolerance_points(name_2_text, _search_text);
973 }
974
975 points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
976 if (points_compare != 0) {
977 return points_compare;
978 }
979
980 auto const &name_1_tooltip = cp_name_1->get_tooltip_text();
981 auto const &name_2_tooltip = cp_name_2->get_tooltip_text();
982
983 if (fuzzy_search(name_1_tooltip, _search_text)) {
984 text_len_1 = name_1_tooltip.length();
985 fuzzy_points_count_1 = fuzzy_points(name_1_tooltip, _search_text) + TOOLTIP_PENALTY;
986 }
987 if (fuzzy_search(name_2_tooltip, _search_text)) {
988 text_len_2 = name_2_tooltip.length();
989 fuzzy_points_count_2 = fuzzy_points(name_2_tooltip, _search_text) + TOOLTIP_PENALTY;
990 }
991
992 points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
993 if (points_compare != 0) {
994 return points_compare;
995 }
996
997 if (fuzzy_tolerance_search(name_1_tooltip, _search_text)) {
998 text_len_1 = name_1_tooltip.length();
999 fuzzy_points_count_1 = fuzzy_tolerance_points(name_1_tooltip, _search_text) +
1000 TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence
1001 }
1002 if (fuzzy_tolerance_search(name_2_tooltip, _search_text)) {
1003 text_len_2 = name_2_tooltip.length();
1004 fuzzy_points_count_2 = fuzzy_tolerance_points(name_2_tooltip, _search_text) +
1005 TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence
1006 }
1007 points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
1008 if (points_compare != 0) {
1009 return points_compare;
1010 }
1011 }
1012
1013 auto const &description_1_text = cp_description_1->get_text();
1014 auto const &description_2_text = cp_description_2->get_text();
1015
1016 if (cp_description_1 && normal_search(description_1_text, _search_text)) {
1017 text_len_1 = description_1_text.length();
1018 fuzzy_points_count_1 = fuzzy_points(description_1_text, _search_text) +
1019 DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence
1020 }
1021 if (cp_description_2 && normal_search(description_2_text, _search_text)) {
1022 text_len_2 = description_2_text.length();
1023 fuzzy_points_count_2 = fuzzy_points(description_2_text, _search_text) +
1024 DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence
1025 }
1026
1027 points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
1028 if (points_compare != 0) {
1029 return points_compare;
1030 }
1031 return 0;
1032}
1033
1034// Widget.set_sensitive() made the cursor vanish, so… TODO: GTK4: Check if fixed
1035static void set_sensitive(Gtk::SearchEntry2 &entry, bool const sensitive)
1036{
1037 entry.set_editable(sensitive);
1038}
1039
1041{
1042 if (_mode == mode) {
1043 return;
1044 }
1045
1046 switch (mode) {
1047 case CPMode::SEARCH:
1048 set_sensitive(_CPFilter, true);
1049 _CPFilter.set_text("");
1050 // _CPFilter.set_icon_from_icon_name("edit-find-symbolic"); // Icon not modifiable in GTK4.
1051 set_hint_texts(_CPFilter, _("Enter search term to search for a command"));
1052
1054
1055 // Show Suggestions instead of history
1056 _CPHistoryScroll.set_visible(false);
1057 _CPSuggestionsScroll.set_visible(true);
1058
1059 _CPSuggestions.unset_filter_func();
1060 _CPSuggestions.set_filter_func(sigc::mem_fun(*this, &CommandPalette::on_filter_general));
1061
1062 _cpfilter_search_connection.disconnect(); // to be sure
1063
1065 _CPFilter.signal_search_changed().connect(sigc::mem_fun(*this, &CommandPalette::on_search));
1066
1067 _search_text = "";
1068 _CPSuggestions.invalidate_filter();
1069
1070 break;
1071
1072 case CPMode::INPUT:
1073 _cpfilter_search_connection.disconnect();
1074
1076
1077 set_sensitive(_CPFilter, true);
1078 _CPFilter.set_text("");
1079 _CPFilter.grab_focus();
1080 // _CPFilter.set_icon_from_icon_name("input-keyboard"); // Icon not modifiable in GTK4.
1081 set_hint_texts(_CPFilter, _("Enter action argument"));
1082
1083 break;
1084
1085 case CPMode::SHELL:
1087
1088 set_sensitive(_CPFilter, true);
1089 // _CPFilter.set_icon_from_icon_name("gtk-search"); // Icon not modifiable in GTK4.
1090
1091 _cpfilter_search_connection.disconnect();
1092
1093 break;
1094
1095 case CPMode::HISTORY: {
1096 auto const children = UI::get_children(_CPHistory);
1097 if (children.empty()) {
1098 return;
1099 }
1100
1101 // Show history instead of suggestions
1102 _CPSuggestionsScroll.set_visible(false);
1103 _CPHistoryScroll.set_visible(true);
1104
1105 set_sensitive(_CPFilter, false);
1106 // _CPFilter.set_icon_from_icon_name("format-justify-fill"); // Icon not modifiable in GTK4.
1107 set_hint_texts(_CPFilter, _("History mode"));
1108
1109 _cpfilter_search_connection.disconnect();
1110
1111 _CPHistory.signal_row_selected().connect(
1112 sigc::mem_fun(*this, &CommandPalette::on_history_selection_changed));
1113 _CPHistory.signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated));
1114
1115 {
1116 // select last row
1117 auto const last_row = _CPHistory.get_row_at_index(children.size() - 1);
1118 _CPHistory.select_row(*last_row);
1119 last_row->grab_focus();
1120 }
1121
1122 Glib::signal_idle().connect_once([this]
1123 {
1124 const auto adjustment = _CPHistoryScroll.get_vadjustment();
1125 adjustment->set_value(adjustment->get_upper());
1126 });
1127 }
1128 }
1129
1130 _mode = mode;
1131}
1132
1137{
1138 auto gapp = InkscapeApplication::instance()->gtk_app();
1139 // TODO: Optimisation: only try to assign if null, make static
1142 const auto dot = full_action_name.find('.');
1143 const auto action_domain = std::string_view{full_action_name.c_str(), dot}; // app, win, doc
1144 const auto action_name = full_action_name.substr(dot + 1);
1145
1146 ActionPtr action_ptr;
1147 if (action_domain == "app") {
1148 action_ptr = gapp->lookup_action(action_name);
1149 } else if (win && action_domain == "win") {
1150 action_ptr = win->lookup_action(action_name);
1151 } else if (doc && action_domain == "doc") {
1152 if (const auto map = doc->getActionGroup(); map) {
1153 action_ptr = map->lookup_action(action_name);
1154 }
1155 }
1156
1157 return {std::move(action_ptr), std::move(full_action_name)};
1158}
1159
1160bool CommandPalette::execute_action(const ActionPtrName &action_ptr_name, const Glib::ustring &value)
1161{
1162 if (!value.empty()) {
1163 _history_xml.add_action_parameter(action_ptr_name.second, value);
1164 }
1165
1166 const auto &[action_ptr, action_name] = action_ptr_name;
1167
1168 switch (get_action_variant_type(action_ptr)) {
1170 if (value == "1" || value == "t" || value == "true" || value.empty()) {
1171 action_ptr->activate(Glib::Variant<bool>::create(true));
1172 } else if (value == "0" || value == "f" || value == "false") {
1173 action_ptr->activate(Glib::Variant<bool>::create(false));
1174 } else {
1175 std::cerr << "CommandPalette::execute_action: Invalid boolean value: " << action_name.raw() << ":" << value
1176 << std::endl;
1177 }
1178 break;
1179
1180 case TypeOfVariant::INT:
1181 try {
1182 action_ptr->activate(Glib::Variant<int>::create(std::stoi(value)));
1183 } catch (...) {
1184 if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
1185 dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter an integer number."));
1186 }
1187 }
1188 break;
1189
1191 try {
1192 action_ptr->activate(Glib::Variant<double>::create(std::stod(value)));
1193 } catch (...) {
1194 if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
1195 dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter a decimal number."));
1196 }
1197 }
1198 break;
1199
1201 action_ptr->activate(Glib::Variant<Glib::ustring>::create(value));
1202 break;
1203
1205 try {
1206 double d0 = 0;
1207 double d1 = 0;
1208 std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", value);
1209
1210 try {
1211 if (tokens.size() != 2) {
1212 throw std::invalid_argument("requires two numbers");
1213 }
1214 } catch (...) {
1215 throw;
1216 }
1217
1218 try {
1219 d0 = std::stod(tokens[0]);
1220 d1 = std::stod(tokens[1]);
1221 } catch (...) {
1222 throw;
1223 }
1224
1225 auto variant = Glib::Variant<std::tuple<double, double>>::create(std::tuple<double, double>(d0, d1));
1226 action_ptr->activate(variant);
1227 } catch (...) {
1228 if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
1229 dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter two comma separated numbers."));
1230 }
1231 }
1232 break;
1233
1235 std::cerr << "CommandPalette::execute_action: unhandled action value type (Unknown Type) " << action_name.raw()
1236 << std::endl;
1237 break;
1238
1240 default:
1241 action_ptr->activate();
1242 }
1243 return false;
1244}
1245
1247{
1248 const GVariantType *gtype = g_action_get_parameter_type(action_ptr->gobj());
1249 if (gtype) {
1250 Glib::VariantType type = action_ptr->get_parameter_type();
1251 if (type.get_string() == "b") {
1252 return TypeOfVariant::BOOL;
1253 } else if (type.get_string() == "i") {
1254 return TypeOfVariant::INT;
1255 } else if (type.get_string() == "d") {
1256 return TypeOfVariant::DOUBLE;
1257 } else if (type.get_string() == "s") {
1258 return TypeOfVariant::STRING;
1259 } else if (type.get_string() == "(dd)") {
1261 } else {
1262 std::cerr << "CommandPalette::get_action_variant_type: unknown variant type: " << type.get_string() << std::endl;
1264 }
1265 }
1266 // With value.
1267 return TypeOfVariant::NONE;
1268}
1269
1270std::pair<Gtk::Label *, Gtk::Label *> CommandPalette::get_name_desc(Gtk::ListBoxRow *child)
1271{
1272 auto box = dynamic_cast<Gtk::Box *>(child->get_child());
1273 if (box && (box->get_name() == "CPOperation")) {
1274 // NOTE: These variables have same name as in the glade file command-palette-operation.glade
1275 // FIXME: When structure of Gladefile of CPOperation changes, refactor this
1276 auto const box_children = UI::get_children(*box);
1277 auto CPNameBox = dynamic_cast<Gtk::Box *>(box_children.at(0));
1278 if (CPNameBox) {
1279 auto const name_children = UI::get_children(*CPNameBox);
1280 auto CPName = dynamic_cast<Gtk::Label *>(name_children.at(0));
1281 auto CPDescription = dynamic_cast<Gtk::Label *>(name_children.at(1));
1282 return std::pair(CPName, CPDescription);
1283 }
1284 }
1285 return std::pair(nullptr, nullptr);
1286}
1287
1288Gtk::Label *CommandPalette::get_full_action_name(Gtk::ListBoxRow *child)
1289{
1290 auto box = dynamic_cast<Gtk::Box *>(child->get_child());
1291 if (box && (box->get_name() == "CPOperation")) {
1292 auto const box_children = UI::get_children(*box);
1293 auto CPActionFullButton = dynamic_cast<Gtk::Button *>(box_children.at(1));
1294 if (CPActionFullButton) {
1295 auto const synapse_button = UI::get_children(*CPActionFullButton);
1296 auto CPSynapseButtonBox = dynamic_cast<Gtk::Box *>(synapse_button.at(0));
1297 if (CPSynapseButtonBox) {
1298 auto const synapse_button_content = UI::get_children(*CPSynapseButtonBox);
1299 return dynamic_cast<Gtk::Label *>(synapse_button_content.at(1));
1300 }
1301 }
1302 }
1303 return nullptr;
1304}
1305
1307{
1308 auto gapp = InkscapeApplication::instance()->gtk_app();
1309 for (auto &&action : gapp->list_actions()) {
1310 generate_action_operation(get_action_ptr_name("app." + std::move(action)), true);
1311 }
1312}
1313
1315{
1316 if (auto window = InkscapeApplication::instance()->get_active_window(); window) {
1317 for (auto &&action : window->list_actions()) {
1318 generate_action_operation(get_action_ptr_name("win." + std::move(action)), true);
1319 }
1320
1321 if (auto document = window->get_document(); document) {
1322 auto map = document->getActionGroup();
1323 if (map) {
1324 for (auto &&action : map->list_actions()) {
1325 generate_action_operation(get_action_ptr_name("doc." + std::move(action)), true);
1326 }
1327 } else {
1328 std::cerr << "CommandPalette::load_win_doc_actions: No document map!" << std::endl;
1329 }
1330 }
1331 }
1332}
1333
1334// CPHistoryXML ---------------------------------------------------------------
1336 : _file_path(IO::Resource::profile_path("cphistory.xml"))
1337{
1338 _xml_doc = sp_repr_read_file(_file_path.c_str(), nullptr);
1339 if (!_xml_doc) {
1340 _xml_doc = sp_repr_document_new("cphistory");
1341
1342 /* STRUCTURE EXAMPLE ------------------ Illustration 1
1343 <cphistory>
1344 <operations>
1345 <action> full.action_name </action>
1346 <import> uri </import>
1347 <export> uri </export>
1348 </operations>
1349 <params>
1350 <action name="app.transfor-rotate">
1351 <param> 30 </param>
1352 <param> 23.5 </param>
1353 </action>
1354 </params>
1355 </cphistory>
1356 */
1357
1358 // Just a pointer, we don't own it, don't free/release/delete
1359 auto root = _xml_doc->root();
1360
1361 // add operation history in this element
1362 auto operations = _xml_doc->createElement("operations");
1363 root->appendChild(operations);
1364
1365 // add param history in this element
1366 auto params = _xml_doc->createElement("params");
1367 root->appendChild(params);
1368
1369 // This was created by allocated
1370 Inkscape::GC::release(operations);
1371 Inkscape::GC::release(params);
1372
1373 // only save if created new
1374 save();
1375 }
1376
1377 // Only two children :) check and ensure Illustration 1
1380}
1381
1386
1387void CPHistoryXML::add_action(const std::string &full_action_name)
1388{
1389 add_operation(HistoryType::ACTION, full_action_name);
1390}
1391
1392void CPHistoryXML::add_import(const std::string &uri)
1393{
1395}
1396
1397void CPHistoryXML::add_open(const std::string &uri)
1398{
1400}
1401
1402void CPHistoryXML::add_action_parameter(const std::string &full_action_name, const std::string &param)
1403{
1404 /* Creates
1405 * <params>
1406 * +1 <action name="full.action-name">
1407 * + <param>30</param>
1408 * + <param>60</param>
1409 * + <param>90</param>
1410 * +1 <action name="full.action-name">
1411 * <params>
1412 *
1413 * + : generally creates
1414 * +1: creates once
1415 */
1416 const auto parameter_node = _xml_doc->createElement("param");
1417 const auto parameter_text = _xml_doc->createTextNode(param.c_str());
1418
1419 parameter_node->appendChild(parameter_text);
1420 Inkscape::GC::release(parameter_text);
1421
1422 for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->next()) {
1423 // If this action's node already exists
1424 if (full_action_name == action_iter->attribute("name")) {
1425 // If the last parameter was the same don't do anything, inner text is also a node hence 2 times last
1426 // child
1427 if (action_iter->lastChild()->lastChild() && action_iter->lastChild()->lastChild()->content() == param) {
1428 Inkscape::GC::release(parameter_node);
1429 return;
1430 }
1431
1432 // If last current than parameter is different, add current
1433 action_iter->appendChild(parameter_node);
1434 Inkscape::GC::release(parameter_node);
1435
1436 save();
1437 return;
1438 }
1439 }
1440
1441 // only encountered when the actions element doesn't already exists,so we create that action's element
1442 const auto action_node = _xml_doc->createElement("action");
1443 action_node->setAttribute("name", full_action_name.c_str());
1444 action_node->appendChild(parameter_node);
1445
1446 _params->appendChild(action_node);
1447 save();
1448
1449 Inkscape::GC::release(action_node);
1450 Inkscape::GC::release(parameter_node);
1451}
1452
1453std::optional<History> CPHistoryXML::get_last_operation()
1454{
1455 auto last_child = _operations->lastChild();
1456 if (last_child) {
1457 if (const auto operation_type = _get_operation_type(last_child); operation_type.has_value()) {
1458 // inner text is a text Node thus last child
1459 return History{*operation_type, last_child->lastChild()->content()};
1460 }
1461 }
1462 return std::nullopt;
1463}
1464std::vector<History> CPHistoryXML::get_operation_history() const
1465{
1466 // TODO: add max items in history
1467 std::vector<History> history;
1468 for (auto operation_iter = _operations->firstChild(); operation_iter; operation_iter = operation_iter->next()) {
1469 if (const auto operation_type = _get_operation_type(operation_iter); operation_type.has_value()) {
1470 history.emplace_back(*operation_type, operation_iter->firstChild()->content());
1471 }
1472 }
1473 return history;
1474}
1475
1476std::vector<std::string> CPHistoryXML::get_action_parameter_history(const std::string &full_action_name) const
1477{
1478 std::vector<std::string> params;
1479 for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->prev()) {
1480 // If this action's node already exists
1481 if (full_action_name == action_iter->attribute("name")) {
1482 // lastChild and prev for LIFO order
1483 for (auto param_iter = _params->lastChild(); param_iter; param_iter = param_iter->prev()) {
1484 params.emplace_back(param_iter->content());
1485 }
1486 return params;
1487 }
1488 }
1489 // action not used previously so no params;
1490 return {};
1491}
1492
1494{
1496}
1497
1498void CPHistoryXML::add_operation(const HistoryType history_type, const std::string &data)
1499{
1500 std::string operation_type_name;
1501 switch (history_type) {
1502 // see Illustration 1
1504 operation_type_name = "action";
1505 break;
1507 operation_type_name = "import";
1508 break;
1510 operation_type_name = "open";
1511 break;
1512 default:
1513 return;
1514 }
1515 auto operation_to_add = _xml_doc->createElement(operation_type_name.c_str()); // action, import, open
1516 auto operation_data = _xml_doc->createTextNode(data.c_str());
1517 operation_data->setContent(data.c_str());
1518
1519 operation_to_add->appendChild(operation_data);
1520 _operations->appendChild(operation_to_add);
1521
1522 Inkscape::GC::release(operation_data);
1523 Inkscape::GC::release(operation_to_add);
1524
1525 save();
1526}
1527
1528std::optional<HistoryType> CPHistoryXML::_get_operation_type(Inkscape::XML::Node *operation)
1529{
1530 const std::string operation_type_name = operation->name();
1531
1532 if (operation_type_name == "action") {
1533 return HistoryType::ACTION;
1534 } else if (operation_type_name == "import") {
1536 } else if (operation_type_name == "open") {
1538 } else {
1539 return std::nullopt;
1540 // unknown HistoryType
1541 }
1542}
1543
1544} // namespace Inkscape::UI::Dialog
1545
1546/*
1547 Local Variables:
1548 mode:c++
1549 c-file-style:"stroustrup"
1550 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1551 indent-tabs-mode:nil
1552 fill-column:99
1553 End:
1554*/
1555// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
const char * action_name
Gtk builder utilities.
uint64_t page
Definition canvas.cpp:171
Glib::ustring get_label_for_action(Glib::ustring const &action_name, bool translated=true) const
Glib::ustring get_section_for_action(Glib::ustring const &action_name) const
Glib::ustring get_tooltip_for_action(Glib::ustring const &action_name, bool translated=true, bool expanded=false) const
Glib::ustring get_tooltip_hint_for_action(Glib::ustring const &action_name, bool translated=true)
InkscapeWindow * get_active_window()
SPDocument * get_active_document()
static InkscapeApplication * instance()
Singleton instance.
Gtk::Application * gtk_app()
The Gtk application instance, or NULL if running headless without display.
bool getBool(Glib::ustring const &pref_path, bool def=false)
Retrieve a Boolean value.
static Preferences * get()
Access the singleton Preferences object.
int getInt(Glib::ustring const &pref_path, int def=0)
Retrieve an integer.
static Shortcuts & getInstance(bool init=true)
Definition shortcuts.h:66
std::vector< Glib::ustring > get_triggers(Glib::ustring const &action_name) const
Returns a vector of triggers for a given detailed_action_name.
Inkscape::XML::Document * _xml_doc
void add_import(const std::string &uri)
void add_open(const std::string &uri)
void add_action(const std::string &full_action_name)
std::vector< History > get_operation_history() const
To construct _CPHistory.
std::vector< std::string > get_action_parameter_history(const std::string &full_action_name) const
To get parameter history when an action is selected, LIFO stack like so more recent first.
void add_action_parameter(const std::string &full_action_name, const std::string &param)
Remember parameter for action.
std::optional< History > get_last_operation()
static std::optional< HistoryType > _get_operation_type(Inkscape::XML::Node *operation)
void add_operation(const HistoryType history_type, const std::string &data)
void on_row_activated(Gtk::ListBoxRow *activated_row)
CPMode _mode
Remember the mode we are in helps in unnecessary signal disconnection and reconnection Used by set_mo...
static bool fuzzy_tolerance_search(const Glib::ustring &subject, const Glib::ustring &search)
The Searching algorithm consists of fuzzy search and fuzzy points.
bool operate_recent_file(Glib::ustring const &uri, bool const import)
bool on_filter_full_action_name(Gtk::ListBoxRow *child)
static bool fuzzy_search(const Glib::ustring &subject, const Glib::ustring &search)
Implements text matching logic.
void add_color(Gtk::Label *label, const Glib::ustring &search, const Glib::ustring &subject, bool tooltip=false)
bool generate_action_operation(const ActionPtrName &action_ptr_name, const bool is_suggestion)
static TypeOfVariant get_action_variant_type(const ActionPtr &action_ptr)
int on_filter_general(Gtk::ListBoxRow *child)
static ActionPtrName get_action_ptr_name(Glib::ustring full_action_name)
Calls actions with parameters.
bool on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import)
Gtk::ScrolledWindow & _CPSuggestionsScroll
static int fuzzy_points(const Glib::ustring &subject, const Glib::ustring &search)
Calculates the fuzzy_point.
Glib::RefPtr< Gio::Action > ActionPtr
void on_action_fullname_clicked(const Glib::ustring &action_fullname)
static std::pair< Gtk::Label *, Gtk::Label * > get_name_desc(Gtk::ListBoxRow *child)
sigc::connection _cpfilter_search_connection
Stores the search connection to deactivate when not needed.
void append_recent_file_operation(const Glib::ustring &path, bool is_suggestion, bool is_import=true)
bool ask_action_parameter(const ActionPtrName &action)
Maybe replaced by: Temporary arrangement may be replaced by snippets This can help us provide paramet...
void remove_color(Gtk::Label *label, const Glib::ustring &subject, bool tooltip=false)
Color removal.
static int fuzzy_tolerance_points(const Glib::ustring &subject, const Glib::ustring &search)
bool on_key_pressed(unsigned keyval, unsigned keycode, Gdk::ModifierType state)
Gtk::Label * get_full_action_name(Gtk::ListBoxRow *child)
static bool normal_search(const Glib::ustring &subject, const Glib::ustring &search)
Searching the full search_text in the subject string used for CPDescription text.
std::pair< ActionPtr, Glib::ustring > ActionPtrName
std::optional< ActionPtrName > _ask_action_ptr_name
Stores the most recent ask_action_name for when Entry::activate fires & we are in INPUT mode.
static void add_color_description(Gtk::Label *label, const Glib::ustring &search)
Color addition for description text Coloring complete consecutive search text in the description text...
int on_sort(Gtk::ListBoxRow *row1, Gtk::ListBoxRow *row2)
compare different rows for order of display priority of comparison 1) CPName->get_text() 2) CPName->g...
static int fuzzy_points_compare(int fuzzy_points_count_1, int fuzzy_points_count_2, int text_len_1, int text_len_2)
void on_history_selection_changed(Gtk::ListBoxRow *lb)
bool on_entry_keypress(unsigned keyval, unsigned, Gdk::ModifierType)
bool execute_action(const ActionPtrName &action, const Glib::ustring &value)
Interface for refcounted XML nodes.
Definition node.h:80
virtual Node * prev()=0
virtual void appendChild(Node *child)=0
Append a node as the last child of this node.
virtual char const * name() const =0
Get the name of the element node.
void setAttribute(Util::const_char_ptr key, Util::const_char_ptr value)
Change an attribute of this node.
Definition node.cpp:25
virtual Node * firstChild()=0
Get the first child of this node.
virtual void setContent(char const *value)=0
Set the content of a text or comment node.
virtual Node * lastChild()=0
Get the last child of this node.
virtual Node * root()=0
Get the root node of this node's document.
To do: update description of desktop.
Definition desktop.h:149
CommandPalette: Class providing Command Palette feature.
RootCluster root
std::unordered_map< std::string, std::unique_ptr< SPDocument > > map
std::string file_name
Editable view implementation.
SPObject * file_import(SPDocument *in_doc, const std::string &path, Inkscape::Extension::Extension *key)
Import a resource.
Definition file.cpp:726
Inkscape - An SVG editor.
Glib::ustring label
Definition desktop.h:50
static R & release(R &r)
Decrements the reference count of a anchored object.
Dialog code.
Definition desktop.h:117
static void set_hint_texts(Gtk::SearchEntry2 &entry, Glib::ustring const &text)
static void set_sensitive(Gtk::SearchEntry2 &entry, bool const sensitive)
Glib::ustring make_bold(const Glib::ustring &search)
Color addition.
std::vector< Gtk::Widget * > get_children(Gtk::Widget &widget)
Get a vector of the widgetʼs children, from get_first_child() through each get_next_sibling().
Definition util.cpp:141
W & get_widget(const Glib::RefPtr< Gtk::Builder > &builder, const char *id)
Glib::RefPtr< Gtk::Builder > create_builder(const char *filename)
@ ERROR_MESSAGE
Definition message.h:29
static cairo_user_data_key_t key
int mode
Singleton class to access the preferences file in a convenient way.
Ocnode * child[8]
Definition quantize.cpp:33
Document * sp_repr_read_file(const gchar *filename, const gchar *default_ns, bool xinclude)
Reads XML from a file, and returns the Document.
Definition repr-io.cpp:274
bool sp_repr_save_file(Document *doc, gchar const *const filename, gchar const *default_ns)
Returns true iff file successfully saved.
Definition repr-io.cpp:724
Inkscape::XML::Document * sp_repr_document_new(char const *rootname)
Returns new document having as first child a node named rootname.
Definition repr.cpp:32
Inkscape::IO::Resource - simple resource API.
static const Point data[]
virtual Node * createTextNode(char const *content)=0
virtual Node * createElement(char const *name)=0
void dot(Cairo::RefPtr< Cairo::Context > &cr, double x, double y)
std::unique_ptr< Toolbar >(* create)()
Definition toolbars.cpp:56
Glib::ustring name
Definition toolbars.cpp:55