Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
shortcuts.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Shortcuts
4 *
5 * Copyright (C) 2020 Tavmjong Bah
6 * Rewrite of code (C) MenTalguY and others.
7 *
8 * The contents of this file may be used under the GNU General Public License Version 2 or later.
9 *
10 */
11
12#include "shortcuts.h"
13
14#include <memory>
15#include <numeric>
16#include <iostream>
17#include <iomanip>
18#include <glibmm/convert.h>
19#include <glibmm/i18n.h>
20#include <glibmm/miscutils.h>
21#include <glibmm/regex.h>
22#include <glibmm/variant.h>
23#include <giomm/file.h>
24#include <giomm/simpleaction.h>
25#include <gtkmm/accelerator.h>
26#include <gtkmm/actionable.h>
27#include <gtkmm/application.h>
28#include <gtkmm/eventcontrollerkey.h>
29#include <gtkmm/shortcut.h>
30#include <gtkmm/window.h>
31
32#include "document.h"
34#include "inkscape-window.h"
35#include "preferences.h"
36#include "io/dir-util.h"
37#include "io/resource.h"
38#include "io/sys.h"
39#include "ui/modifiers.h"
40#include "ui/tools/tool-base.h" // For latin keyval
41#include "ui/dialog/choose-file.h" // Importing/exporting shortcut files.
42#include "ui/util.h"
44#include "xml/simple-document.h"
45#include "xml/node.h"
46#include "xml/node-iterators.h"
47
48using namespace Inkscape::IO::Resource;
49using namespace Inkscape::Modifiers;
50
51namespace Inkscape {
52
54 : app{dynamic_cast<Gtk::Application *>(Gio::Application::get_default().get())}
55{
56 if (!app) {
57 std::cerr << "Shortcuts::Shortcuts: No app! Shortcuts cannot be used without a Gtk::Application!" << std::endl;
58 return;
59 }
60
61 // Shared among all Shortcut controllers.
63}
64
66
67void
69 initialized = true;
70
71 // Clear arrays (we may be re-reading).
72 _clear();
73
74 bool success = false; // We've read a shortcut file!
75 std::string path;
76
77 // ------------ Open Inkscape shortcut file ------------
78
79 // Try filename from preferences first.
81
82 path = prefs->getString("/options/kbshortcuts/shortcutfile");
83 if (!path.empty()) {
84 bool absolute = true;
85 if (!Glib::path_is_absolute(path)) {
86 path = get_path_string(SYSTEM, KEYS, path.c_str());
87 absolute = false;
88 }
89
90 Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(path);
91 success = _read(file);
92 if (!success) {
93 std::cerr << "Shortcut::Shortcut: Unable to read shortcut file listed in preferences: " + path << std::endl;
94 }
95
96 // Save relative path to "share/keys" if possible to handle parallel installations of
97 // Inskcape gracefully.
98 if (success && absolute) {
99 auto const relative_path = sp_relative_path_from_path(path, get_path_string(SYSTEM, KEYS));
100 prefs->setString("/options/kbshortcuts/shortcutfile", relative_path.c_str());
101 }
102 }
103
104 if (!success) {
105 // std::cerr << "Shortcut::Shortcut: " << reason << ", trying default.xml" << std::endl;
106
107 Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SYSTEM, KEYS, "default.xml"));
108 success = _read(file);
109 }
110
111 if (!success) {
112 std::cerr << "Shortcut::Shortcut: Failed to read file default.xml, trying inkscape.xml" << std::endl;
113
114 Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SYSTEM, KEYS, "inkscape.xml"));
115 success = _read(file);
116 }
117
118 if (!success) {
119 std::cerr << "Shortcut::Shortcut: Failed to read file inkscape.xml; giving up!" << std::endl;
120 }
121
122 // ------------ Open Shared shortcut file -------------
123 Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SHARED, KEYS, "default.xml"));
124 // Test if file exists before attempting to read to avoid generating warning message.
125 if (file->query_exists()) {
126 _read(file, true);
127 }
128
129 // ------------ Open User shortcut file -------------
130 file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
131 // Test if file exists before attempting to read to avoid generating warning message.
132 if (file->query_exists()) {
133 _read(file, true);
134 }
135
136 // Emit changed signal in case of read-reading (user selects different file).
137 _changed.emit();
138
139 // _dump();
140}
141
142
143// ****** User Shortcuts ******
144
145// Add a user shortcut, updating user's shortcut file if successful.
146bool
147Shortcuts::add_user_shortcut(Glib::ustring const &detailed_action_name,const Gtk::AccelKey& trigger)
148{
149 // Add shortcut, if successful, save to file.
150 // Performance is not critical here. This is only called from the preferences dialog.
151
152 if (_add_shortcut(
153 detailed_action_name,
154 trigger.get_abbrev(),
155 true /* user shortcut */,
156 false /* do not cache action-names */
157 )) {
158 _changed.emit();
159
160 // Save
161 return write_user();
162 }
163
164 std::cerr << "Shortcut::add_user_shortcut: Failed to add: " << detailed_action_name.raw()
165 << " with shortcut " << trigger.get_abbrev().raw() << std::endl;
166 return false;
167};
168
169// Remove a user shortcut, updating user's shortcut file.
170bool
171Shortcuts::remove_user_shortcut(Glib::ustring const &detailed_action_name)
172{
173 // Check if really user shortcut.
174 bool user_shortcut = is_user_set(detailed_action_name);
175
176 if (!user_shortcut) {
177 // We don't allow removing non-user shortcuts.
178 return false;
179 }
180
181 if (_remove_shortcuts(detailed_action_name)) {
182 // Save
183 write_user();
184
185 // Reread to get original shortcut (if any). And emit changes signal.
186 init();
187
188 return true;
189 }
190
191 std::cerr << "Shortcuts::remove_user_shortcut: Failed to remove shortcut for: "
192 << detailed_action_name.raw() << std::endl;
193 return false;
194}
195
196
200bool
202{
203 // Create new empty document and save
204 auto *document = new XML::SimpleDocument();
205 XML::Node * node = document->createElement("keys");
206 node->setAttribute("name", "User Shortcuts");
207 document->appendChild(node);
208 Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
209 sp_repr_save_file(document, file->get_path().c_str(), nullptr);
210 GC::release(document);
211
212 // Re-read everything! And emit changed signal.
213 init();
214 return true;
215}
216
220bool
221Shortcuts::is_user_set(Glib::ustring const &detailed_action_name)
222{
223 auto it = _shortcuts.find(detailed_action_name);
224
225 if (it != _shortcuts.end()) {
226 // We need to test only one entry, as there will be only one if user set.
227 return (it->second.user_set);
228 }
229
230 return false;
231}
232
236bool
238{
239 Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
240 return _write(file, User);
241}
242
243/*
244 * Update text with shortcuts.
245 * Inkscape includes shortcuts in tooltips and in dialog titles. They need to be updated
246 * anytime a tooltip is changed.
247 */
248void
250{
251 if (auto const actionable = dynamic_cast<Gtk::Actionable *>(widget)) {
252 if (auto action = actionable->get_action_name(); !action.empty()) {
253 Glib::ustring variant;
254 if (auto const value = actionable->get_action_target_value()) {
255 auto const type = value.get_type_string();
256 if (type == "s") {
257 variant = static_cast<Glib::Variant<Glib::ustring> const &>(value).get();
258 action += "('" + variant + "')";
259 } else if (type == "i") {
260 variant = std::to_string(static_cast<Glib::Variant<std::int32_t> const &>(value).get());
261 action += "(" + variant + ")";
262 } else {
263 std::cerr << "Shortcuts::update_gui_text_recursive: unhandled variant type: " << type << std::endl;
264 }
265 }
266
267 auto const &triggers = get_triggers(action);
268
269 Glib::ustring tooltip;
270 auto *iapp = InkscapeApplication::instance();
271 if (iapp) {
272 tooltip = iapp->get_action_extra_data().get_tooltip_for_action(action, true, true);
273 }
274
275 // Add new primary accelerator.
276 if (triggers.size() > 0) {
277 // Add space between tooltip and accel if there is a tooltip
278 if (!tooltip.empty()) {
279 tooltip += " ";
280 }
281
282 // Convert to more user friendly notation.
283 unsigned int key = 0;
284 Gdk::ModifierType mod{};
285 Gtk::Accelerator::parse(triggers[0], key, mod);
286 tooltip += "(" + Gtk::Accelerator::get_label(key, mod) + ")";
287 }
288
289 // Update tooltip.
290 widget->set_tooltip_markup(tooltip);
291 }
292 }
293
294 for (auto const child : UI::get_children(*widget)) {
296 }
297}
298
299// ******** Invoke Actions *******
300
302bool
303Shortcuts::invoke_action(Gtk::AccelKey const &shortcut)
304{
305 // This can be simplified in GTK4.
306 Glib::ustring accel = Gtk::Accelerator::name(shortcut.get_key(), shortcut.get_mod());
307
308 auto const actions = get_actions(accel);
309 if (!actions.empty()) {
310 Glib::ustring const &action = actions[0];
311 Glib::ustring action_name;
312 Glib::VariantBase value;
313 Gio::SimpleAction::parse_detailed_name_variant(action.substr(4), action_name, value);
314 if (action.compare(0, 4, "app.") == 0) {
315 app->activate_action(action_name, value);
316 return true;
317 } else {
318 auto window = dynamic_cast<InkscapeWindow *>(app->get_active_window());
319 if (window) {
320 window->activate_action(action, value); // Not action_name in Gtk4!
321 return true;
322 }
323 }
324 }
325 return false;
326}
327
329// Used by Tools
330bool
332{
333 auto const shortcut = get_from_event(event);
334 return invoke_action(shortcut);
335}
336
338// NOT USED CURRENTLY
339bool
340Shortcuts::invoke_action(GtkEventControllerKey const * const controller,
341 unsigned const keyval, unsigned const keycode,
342 GdkModifierType const state)
343{
344 auto const shortcut = get_from(controller, keyval, keycode, state);
345 return invoke_action(shortcut);
346}
347
348// ******* Utility *******
349
353std::vector<Glib::ustring>
354Shortcuts::get_triggers(Glib::ustring const &detailed_action_name) const
355{
356 std::vector<Glib::ustring> triggers;
357 auto matches = _shortcuts.equal_range(detailed_action_name);
358 for (auto it = matches.first; it != matches.second; ++it) {
359 triggers.push_back(it->second.trigger_string);
360 }
361 return triggers;
362}
363
367std::vector<Glib::ustring>
368Shortcuts::get_actions(Glib::ustring const &trigger) const
369{
370 std::vector<Glib::ustring> actions;
371 for (auto const &[detailed_action_name, value] : _shortcuts) {
372 if (trigger == value.trigger_string) {
373 actions.emplace_back(detailed_action_name);
374 }
375 }
376 return actions;
377}
378
379Glib::ustring
380Shortcuts::get_label(const Gtk::AccelKey& shortcut)
381{
382 Glib::ustring label;
383
384 if (!shortcut.is_null()) {
385 // ::get_label shows key pad and numeric keys identically.
386 // TODO: Results in labels like "Numpad Alt+5"
387 if (shortcut.get_abbrev().find("KP") != Glib::ustring::npos) {
388 label += _("Numpad");
389 label += " ";
390 }
391
392 label += Gtk::Accelerator::get_label(shortcut.get_key(), shortcut.get_mod());
393 }
394
395 return label;
396}
397
398static Gtk::AccelKey
399get_from_event_impl(unsigned const event_keyval, unsigned const event_keycode,
400 GdkModifierType const event_state, unsigned const event_group,
401 bool const fix)
402{
403 // MOD2 corresponds to the NumLock key. Masking it out allows
404 // shortcuts to work regardless of its state.
405 auto const default_mod_mask = Gtk::Accelerator::get_default_mod_mask();
406 auto const initial_modifiers = static_cast<Gdk::ModifierType>(event_state) & default_mod_mask;
407
408 auto consumed_modifiers = 0u;
410 event_keyval, event_keycode, event_state, event_group, &consumed_modifiers);
411
412 // If a key value is "convertible", i.e. it has different lower case and upper case versions,
413 // convert to lower case and don't consume the "shift" modifier.
414 bool is_case_convertible = !(gdk_keyval_is_upper(keyval) && gdk_keyval_is_lower(keyval));
415 if (is_case_convertible) {
416 keyval = gdk_keyval_to_lower(keyval);
417 consumed_modifiers &= ~static_cast<unsigned>(Gdk::ModifierType::SHIFT_MASK);
418 }
419
420 // The InkscapePreferences dialog returns an event structure where the Shift modifier is not
421 // set for keys like '('. This causes '(' to be converted to '9' by get_latin_keyval. It also
422 // returns 'Shift-k' for 'K' (instead of 'Shift-K') but this is not a problem.
423 // We fix this by restoring keyval to its original value.
424 if (fix) {
425 keyval = event_keyval;
426 }
427
428 auto const unused_modifiers = Gdk::ModifierType(static_cast<unsigned>(initial_modifiers)
429 & ~consumed_modifiers
430 & GDK_MODIFIER_MASK
431 & ~GDK_LOCK_MASK);
432
433 // std::cout << "Shortcuts::get_from_event: End: "
434 // << " Key: " << std::hex << keyval << " (" << (char)keyval << ")"
435 // << " Mod: " << std::hex << unused_modifiers << std::endl;
436 return (Gtk::AccelKey(keyval, unused_modifiers));
437}
438
454Gtk::AccelKey
455Shortcuts::get_from(GtkEventControllerKey const * const controller,
456 unsigned const keyval, unsigned const keycode, GdkModifierType const state,
457 bool const fix)
458{
459 // TODO: Once controller.h is updated to use gtkmm 4 wrappers, we can get rid of const_cast etc
460 auto const mcontroller = const_cast<GtkEventControllerKey *>(controller);
461 auto const group = controller ? gtk_event_controller_key_get_group(mcontroller) : 0u;
462 return get_from_event_impl(keyval, keycode, state, group, fix);
463}
464
465Gtk::AccelKey
466Shortcuts::get_from(Gtk::EventControllerKey const &controller,
467 unsigned keyval, unsigned keycode, Gdk::ModifierType state, bool fix)
468{
469 return get_from_event_impl(keyval, keycode, static_cast<GdkModifierType>(state), controller.get_group(), fix);
470}
471
472Gtk::AccelKey
473Shortcuts::get_from_event(KeyEvent const &event, bool fix)
474{
475 return get_from_event_impl(event.keyval, event.keycode,
476 static_cast<GdkModifierType>(event.modifiers), event.group, fix);
477}
478
479// Get a list of detailed action names (as defined in action extra data).
480// This is more useful for shortcuts than a list of all actions.
481std::vector<Glib::ustring>
483{
484 auto *iapp = InkscapeApplication::instance();
485 InkActionExtraData& action_data = iapp->get_action_extra_data();
486 return action_data.get_actions();
487}
488
489// Get a list of all actions (application, window, and document), properly prefixed.
490// We need to do this ourselves as Gtk::Application does not have a function for this.
491std::vector<Glib::ustring>
493{
494 std::vector<Glib::ustring> all_actions;
495
496 auto actions = app->list_actions();
497 std::sort(actions.begin(), actions.end());
498 for (auto &&action: std::move(actions)) {
499 all_actions.push_back("app." + std::move(action));
500 }
501
502 auto gwindow = app->get_active_window();
503 auto window = dynamic_cast<InkscapeWindow *>(gwindow);
504 if (window) {
505 actions = window->list_actions();
506 std::sort(actions.begin(), actions.end());
507 for (auto &&action: std::move(actions)) {
508 all_actions.push_back("win." + std::move(action));
509 }
510
511 auto document = window->get_document();
512 if (document) {
513 auto map = document->getActionGroup();
514 if (map) {
515 actions = map->list_actions();
516 std::sort(actions.begin(), actions.end());
517 for (auto &&action: std::move(actions)) {
518 all_actions.push_back("doc." + std::move(action));
519 }
520 } else {
521 std::cerr << "Shortcuts::list_all_actions: No document map!" << std::endl;
522 }
523 }
524 }
525
526 return all_actions;
527}
528
529template <typename T>
530static void append(std::vector<T> &target, std::vector<T> &&source)
531{
532 target.insert(target.end(), std::move_iterator{source.begin()}, std::move_iterator{source.end()});
533}
534
538std::vector<std::pair<Glib::ustring, std::string>>
540{
541 using namespace Inkscape::IO::Resource;
542
543 // Make a list of all key files from System and User. Glib::ustring should be std::string!
544 auto filenames = get_filenames(SYSTEM, KEYS, {".xml"});
545 // Exclude default.xml as it only contains user modifications.
546 append(filenames, get_filenames(SHARED, KEYS, {".xml"}, {"default.xml"}));
547 append(filenames, get_filenames(USER , KEYS, {".xml"}, {"default.xml"}));
548
549 // Check file exists and extract out label if it does.
550 std::vector<std::pair<Glib::ustring, std::string>> names_and_paths;
551 for (auto const &filename : filenames) {
552 Glib::ustring label = Glib::path_get_basename(filename);
553 auto filename_relative = sp_relative_path_from_path(filename, get_path_string(SYSTEM, KEYS));
554
555 XML::Document *document = sp_repr_read_file(filename.c_str(), nullptr, true);
556 if (!document) {
557 std::cerr << "Shortcut::get_file_names: could not parse file: " << filename << std::endl;
558 continue;
559 }
560
561 XML::NodeConstSiblingIterator iter = document->firstChild();
562 for ( ; iter ; ++iter ) { // We iterate in case of comments.
563 if (strcmp(iter->name(), "keys") == 0) {
564 char const * const name = iter->attribute("name");
565 if (name) {
566 label = Glib::ustring::compose("%1 (%2)", name, label);
567 }
568 names_and_paths.emplace_back(std::move(label), std::move(filename_relative));
569 break;
570 }
571 }
572 if (!iter) {
573 std::cerr << "Shortcuts::get_File_names: not a shortcut keys file: " << filename << std::endl;
574 }
575
576 Inkscape::GC::release(document);
577 }
578
579 // Sort by name
580 std::sort(names_and_paths.begin(), names_and_paths.end(),
581 [](auto const &pair1, auto const &pair2) {
582 return pair1.first < pair2.first;
583 });
584 // But default.xml at top
585 auto it_default = std::find_if(names_and_paths.begin(), names_and_paths.end(),
586 [](auto const &pair) {
587 return pair.second == "default.xml";
588 });
589 if (it_default != names_and_paths.end()) {
590 std::rotate(names_and_paths.begin(), it_default, it_default+1);
591 }
592
593 return names_and_paths;
594}
595
596// Dialogs
597
598// Import user shortcuts from a file.
599bool
601 // Users key directory.
602 auto directory = get_path_string(USER, KEYS, {});
603
604 // Create and show the dialog
605 Gtk::Window* window = app->get_active_window();
606 if (!window) {
607 return false;
608 }
609
610 static std::vector<std::pair<Glib::ustring, Glib::ustring>> const filters {
611 {_("Inkscape shortcuts (*.xml)"), "*.xml"}
612 };
613
614 auto file = choose_file_open(_("Select a file to import"),
615 window,
616 filters,
617 directory);
618 if (!file) {
619 return false; // Cancel
620 }
621
622 // Read
623 if (!_read(file, true)) {
624 std::cerr << "Shortcuts::import_shortcuts: Failed to read file!" << std::endl;
625 return false;
626 }
627
628 // Save
629 return write_user();
630};
631
632bool
634 // Users key directory.
635 auto directory = get_path_string(USER, KEYS, {});
636
637 // Create and show the dialog
638 Gtk::Window* window = app->get_active_window();
639 if (!window) {
640 return false;
641 }
642
643 auto file = choose_file_save(_("Select a filename for export"),
644 window,
645 "text/xml", // Mime type
646 "shortcuts.xml", // Initial filename
647 directory); // Initial directory
648
649 if (!file) {
650 return false; // Cancel
651 }
652
653 auto success = _write(file, User);
654 if (!success) {
655 std::cerr << "Shortcuts::export_shortcuts: Failed to save file!" << std::endl;
656 }
657
658 return success;
659};
660
662sigc::connection Shortcuts::connect_changed(sigc::slot<void ()> const &slot)
663{
664 return _changed.connect(slot);
665}
666
667
668// -------- Private --------
669
670[[nodiscard]] static Glib::ustring
671join(std::vector<Glib::ustring> const &accels, char const separator)
672{
673 auto const capacity = std::accumulate(accels.begin(), accels.end(), std::size_t{0},
674 [](std::size_t capacity, auto const &accel){ return capacity += accel.size() + 1; });
675 Glib::ustring result;
676 result.reserve(capacity);
677 for (auto const &accel: accels) {
678 if (!result.empty()) result += separator;
679 result += accel;
680 }
681 return result;
682}
683
684Gdk::ModifierType
685parse_modifier_string(char const * const modifiers_string)
686{
687 Gdk::ModifierType modifiers{};
688 if (modifiers_string) {
689 std::vector<Glib::ustring> mod_vector = Glib::Regex::split_simple("\\s*,\\s*", modifiers_string);
690
691 for (auto const &mod : mod_vector) {
692 if (mod == "Control" || mod == "Ctrl") {
693 modifiers |= Gdk::ModifierType::CONTROL_MASK;
694 } else if (mod == "Shift") {
695 modifiers |= Gdk::ModifierType::SHIFT_MASK;
696 } else if (mod == "Alt") {
697 modifiers |= Gdk::ModifierType::ALT_MASK;
698 } else if (mod == "Super") {
699 modifiers |= Gdk::ModifierType::SUPER_MASK; // Not used
700 } else if (mod == "Hyper") {
701 modifiers |= Gdk::ModifierType::HYPER_MASK; // Not used
702 } else if (mod == "Meta") {
703 modifiers |= Gdk::ModifierType::META_MASK;
704 } else if (mod == "Primary") {
705#ifdef __APPLE__
706 modifiers |= Gdk::ModifierType::META_MASK;
707#else
708 modifiers |= Gdk::ModifierType::CONTROL_MASK;
709#endif
710 } else {
711 std::cerr << "Shortcut::read: Unknown GDK modifier: " << mod.c_str() << std::endl;
712 }
713 }
714 }
715 return modifiers;
716}
717
718// ******* Files *******
719
723bool
724Shortcuts::_read(Glib::RefPtr<Gio::File> const &file, bool const user_set)
725{
726 if (!file->query_exists()) {
727 std::cerr << "Shortcut::read: file does not exist: " << file->get_path() << std::endl;
728 return false;
729 }
730
731 XML::Document *document = sp_repr_read_file(file->get_path().c_str(), nullptr, true);
732 if (!document) {
733 std::cerr << "Shortcut::read: could not parse file: " << file->get_path() << std::endl;
734 return false;
735 }
736
737 XML::NodeConstSiblingIterator iter = document->firstChild();
738 for ( ; iter ; ++iter ) { // We iterate in case of comments.
739 if (strcmp(iter->name(), "keys") == 0) {
740 break;
741 }
742 }
743
744 if (!iter) {
745 std::cerr << "Shortcuts::read: File in wrong format: " << file->get_path() << std::endl;
746 return false;
747 }
748
749 // Loop through the children in <keys> (may have nested keys)
750 _read(*iter, user_set);
751
752 return true;
753}
754
761void
762Shortcuts::_read(XML::Node const &keysnode, bool user_set)
763{
764 bool cache_action_list = false; // see below
766 for ( ; iter ; ++iter ) {
767 if (strcmp(iter->name(), "modifier") == 0) {
768 char const * const mod_name = iter->attribute("action");
769 if (!mod_name) {
770 std::cerr << "Shortcuts::read: Missing modifier for action!" << std::endl;
771 continue;
772 }
773
774 Modifier *mod = Modifier::get(mod_name);
775 if (mod == nullptr) {
776 std::cerr << "Shortcuts::read: Can't find modifier: " << mod_name << std::endl;
777 continue;
778 }
779
780 // If mods isn't specified then it should use default, if it's an empty string
781 // then the modifier is None (i.e. happens all the time without a modifier)
782 KeyMask and_modifier = NOT_SET;
783 char const * const mod_attr = iter->attribute("modifiers");
784 if (mod_attr) {
785 and_modifier = (KeyMask) parse_modifier_string(mod_attr);
786 }
787
788 // Parse not (cold key) modifier
789 KeyMask not_modifier = NOT_SET;
790 char const * const not_attr = iter->attribute("not_modifiers");
791 if (not_attr) {
792 not_modifier = (KeyMask) parse_modifier_string(not_attr);
793 }
794
795 char const * const disabled_attr = iter->attribute("disabled");
796 if (disabled_attr && strcmp(disabled_attr, "true") == 0) {
797 and_modifier = NEVER;
798 }
799
800 if (and_modifier != NOT_SET) {
801 if(user_set) {
802 mod->set_user(and_modifier, not_modifier);
803 } else {
804 mod->set_keys(and_modifier, not_modifier);
805 }
806 }
807 continue;
808 } else if (strcmp(iter->name(), "keys") == 0) {
809 _read(*iter, user_set);
810 continue;
811 } else if (strcmp(iter->name(), "bind") != 0) {
812 // Unknown element, do not complain.
813 continue;
814 }
815
816 // Gio::Action's
817 char const * const gaction = iter->attribute("gaction");
818 char const * const keys = iter->attribute("keys");
819 if (gaction && keys) {
820
821 // Trim leading spaces
822 Glib::ustring Keys = keys;
823 auto p = Keys.find_first_not_of(" ");
824 Keys = Keys.erase(0, p);
825
826 std::vector<Glib::ustring> key_vector = Glib::Regex::split_simple("\\s*,\\s*", Keys);
827 std::reverse(key_vector.begin(), key_vector.end()); // Last key added will appear in menus.
828
829 // Set one shortcut at a time so we can check if it has been previously used.
830 for (auto const &key : key_vector) {
831 // Within this function,
832 // cache_action_list is false for the first call to _add_action,
833 // then true for all further calls until we return.
834 _add_shortcut(gaction, key, user_set,
835 cache_action_list /* on first call, invalidate action list cache */);
836 cache_action_list = true;
837 }
838
839 // Uncomment to see what the cat dragged in.
840 // if (!key_vector.empty()) {
841 // std::cout << "Shortcut::read: gaction: "<< gaction
842 // << ", user set: " << std::boolalpha << user_set << ", ";
843 // for (auto const &key : key_vector) {
844 // std::cout << key << ", ";
845 // }
846 // std::cout << std::endl;
847 // }
848
849 continue;
850 }
851 }
852}
853
854// In principle, we only write User shortcuts. But for debugging, we might want to write something else.
855bool
856Shortcuts::_write(Glib::RefPtr<Gio::File> const &file, What const what)
857{
858 auto *document = new XML::SimpleDocument();
859 XML::Node * node = document->createElement("keys");
860 switch (what) {
861 case User:
862 node->setAttribute("name", "User Shortcuts");
863 break;
864 case System:
865 node->setAttribute("name", "System Shortcuts");
866 break;
867 default:
868 node->setAttribute("name", "Inkscape Shortcuts");
869 }
870
871 document->appendChild(node);
872
873 // Actions: write out all actions with accelerators.
874 for (auto const &action_name : list_all_detailed_action_names()) {
875 bool user_set = is_user_set(action_name.raw());
876 if ( (what == All) ||
877 (what == System && !user_set) ||
878 (what == User && user_set) )
879 {
880 auto const &triggers = get_triggers(action_name);
881 if (!triggers.empty()) {
882 XML::Node * node = document->createElement("bind");
883
884 node->setAttribute("gaction", action_name);
885
886 auto const keys = join(triggers, ',');
887 node->setAttribute("keys", keys);
888
889 document->root()->appendChild(node);
890 }
891 }
892 }
893
894 for(auto modifier: Inkscape::Modifiers::Modifier::getList()) {
895 if (what == User && modifier->is_set_user()) {
896 XML::Node * node = document->createElement("modifier");
897 node->setAttribute("action", modifier->get_id());
898
899 if (modifier->get_config_user_disabled()) {
900 node->setAttribute("disabled", "true");
901 } else {
902 node->setAttribute("modifiers", modifier->get_config_user_and());
903 auto not_mask = modifier->get_config_user_not();
904 if (!not_mask.empty() and not_mask != "-") {
905 node->setAttribute("not_modifiers", not_mask);
906 }
907 }
908
909 document->root()->appendChild(node);
910 }
911 }
912
913 sp_repr_save_file(document, file->get_path().c_str(), nullptr);
914 GC::release(document);
915
916 return true;
917};
918
919// ******* Add/remove shortcuts *******
920
930bool Shortcuts::_add_shortcut(Glib::ustring const &detailed_action_name, Glib::ustring const &trigger_string, bool user,
931 bool cache_action_names)
932{
933 // Format has changed between Gtk3 and Gtk4. Pass through xxx to standardize form.
934 auto str = trigger_string.raw();
935
936#ifdef __APPLE__
937 // map <primary> modifier to <command> modifier on macOS, as gtk4 backend does not do that for us;
938 // this will restore predefined Inkscape shortcuts, so they work like they used to in older versions
939 static std::string const primary = "<primary>";
940 auto const pos = str.find(primary);
941 if (pos != std::string::npos) {
942 str.replace(pos, pos + primary.length(), "<meta>");
943 }
944#endif
945
946 Gtk::AccelKey key(str);
947
948 auto trigger_normalized = key.get_abbrev();
949
950 // Check if action actually exists. Need to compare action names without values...
951 Glib::ustring action_name;
952 Glib::VariantBase target;
953 Gio::SimpleAction::parse_detailed_name_variant(detailed_action_name, action_name, target);
954
955 // Note: Commented out because actions are now installed later, so this check actually breaks all shortcuts,
956 /*if (!_list_action_names(cache_action_names).contains(action_name.raw())) {
957 // Oops, not an action!
958 std::cerr << "Shortcuts::_add_shortcut: No Action for " << detailed_action_name.raw() << std::endl;
959 return false;
960 }*/
961
962 // Remove previous use of trigger.
963 [[maybe_unused]] auto const removed = _remove_shortcut_trigger(trigger_normalized);
964 // if (removed) {
965 // std::cerr << "Shortcut::add_shortcut: duplicate shortcut found for: " << trigger_normalized
966 // << " New: " << detailed_action_name.raw() << " !" << std::endl;
967 // }
968
969 // A user shortcut replaces all others.
970 if (user) {
971 _remove_shortcuts(detailed_action_name);
972 }
973
974 auto const trigger = Gtk::ShortcutTrigger::parse_string(trigger_normalized);
975 g_assert(trigger);
976
977 auto const action = Gtk::NamedAction::create(action_name);
978 g_assert(action);
979
980 auto shortcut = Gtk::Shortcut::create(trigger, action);
981 g_assert(shortcut);
982 if (target) {
983 shortcut->set_arguments(target);
984 }
985
986 _liststore->append(shortcut);
987
988 auto value = ShortcutValue{std::move(trigger_normalized), std::move(shortcut), user};
989 _shortcuts.emplace(detailed_action_name.raw(), std::move(value));
990
991 return true;
992}
993
998bool
999Shortcuts::_remove_shortcut_trigger(Glib::ustring const& trigger)
1000{
1001 bool changed = false;
1002 for (auto it = _shortcuts.begin(); it != _shortcuts.end(); ) {
1003 if (it->second.trigger_string.raw() == trigger.raw()) {
1004 // Liststores are ugly!
1005 auto shortcut = it->second.shortcut;
1006 for (int i = 0; i < _liststore->get_n_items(); ++i) {
1007 if (shortcut == _liststore->get_item(i)) {
1008 _liststore->remove(i);
1009 break;
1010 }
1011 }
1012
1013 it = _shortcuts.erase(it);
1014 changed = true;
1015 } else {
1016 ++it;
1017 }
1018 }
1019
1020 if (changed) {
1021 return true;
1022 }
1023
1024 return false;
1025}
1026
1030bool
1031Shortcuts::_remove_shortcuts(Glib::ustring const &detailed_action_name)
1032{
1033 bool removed = false;
1034 for (auto it = _shortcuts.begin(); it != _shortcuts.end(); ) {
1035 if (it->first == detailed_action_name.raw()) {
1036 auto const &shortcut = it->second.shortcut;
1037 g_assert(shortcut);
1038
1039 // Liststores are ugly!
1040 for (int i = 0; i < _liststore->get_n_items(); ++i) {
1041 if (shortcut == _liststore->get_item(i)) {
1042 _liststore->remove(i);
1043 break;
1044 }
1045 }
1046
1047 removed = true;
1048 it = _shortcuts.erase(it);
1049 } else {
1050 ++it;
1051 }
1052 }
1053
1054 return removed;
1055}
1070const std::set<std::string> &Shortcuts::_list_action_names(bool cached)
1071{
1072 if (!cached) {
1073 // std::cerr << "Shortcuts::_list_action_names: invalidating cache." << std::endl;
1075 for (auto const &action_name_detailed : list_all_detailed_action_names()) {
1076 Glib::ustring action_name_short;
1077 Glib::VariantBase unused;
1078 Gio::SimpleAction::parse_detailed_name_variant(action_name_detailed, action_name_short, unused);
1079 _list_action_names_cache.insert(action_name_short.raw());
1080 }
1081 }
1083}
1084
1088void
1090{
1091 _liststore->remove_all();
1092 _shortcuts.clear();
1093}
1094
1095
1096// For debugging.
1097void
1099 // What shortcuts are being used?
1100 static std::vector<Gdk::ModifierType> const modifiers{
1101 Gdk::ModifierType{},
1102 Gdk::ModifierType::SHIFT_MASK,
1103 Gdk::ModifierType::CONTROL_MASK,
1104 Gdk::ModifierType::ALT_MASK,
1105 Gdk::ModifierType::SHIFT_MASK | Gdk::ModifierType::CONTROL_MASK,
1106 Gdk::ModifierType::SHIFT_MASK | Gdk::ModifierType::ALT_MASK,
1107 Gdk::ModifierType::CONTROL_MASK | Gdk::ModifierType::ALT_MASK,
1108 Gdk::ModifierType::SHIFT_MASK | Gdk::ModifierType::CONTROL_MASK | Gdk::ModifierType::ALT_MASK
1109 };
1110
1111 for (auto mod : modifiers) {
1112 for (char key = '!'; key <= '~'; ++key) {
1113 Glib::ustring action;
1114 Glib::ustring accel = Gtk::Accelerator::name(key, mod);
1115 auto const actions = get_actions(accel);
1116 if (!actions.empty()) {
1117 action = actions[0];
1118 }
1119
1120 std::cout << " shortcut:"
1121 << " " << std::setw( 8) << std::hex << static_cast<int>(mod)
1122 << " " << std::setw( 8) << std::hex << key
1123 << " " << std::setw(30) << std::left << accel
1124 << " " << action
1125 << std::endl;
1126 }
1127 }
1128
1129 int count = _liststore->get_n_items();
1130 for (int i = 0; i < count; ++i) {
1131 auto shortcut = _liststore->get_item(i);
1132 auto trigger = shortcut->get_trigger();
1133 auto action = shortcut->get_action();
1134 auto variant = shortcut->get_arguments();
1135
1136 std::cout << action->to_string();
1137 if (variant) {
1138 std::cout << "(" << variant.print() << ")";
1139 }
1140 std::cout << ": " << trigger->to_string() << std::endl;
1141 }
1142}
1143
1144void
1146{
1147 static unsigned int indent = 0;
1148 ++indent;
1149 for (int i = 0; i < indent; ++i) std::cout << " ";
1150
1151 auto const actionable = dynamic_cast<Gtk::Actionable *>(widget);
1152 auto const action = actionable ? actionable->get_action_name() : "";
1153
1154 std::cout << widget->get_name()
1155 << ": actionable: " << std::boolalpha << static_cast<bool>(actionable)
1156 << ": " << widget->get_tooltip_text()
1157 << ": " << action
1158 << std::endl;
1159
1160 for (auto const child : UI::get_children(*widget)) {
1162 }
1163
1164 --indent;
1165}
1166
1167} // namespace Inkscape
1168
1169/*
1170 Local Variables:
1171 mode:c++
1172 c-file-style:"stroustrup"
1173 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1174 indent-tabs-mode:nil
1175 fill-column:99
1176 End:
1177*/
1178// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
const char * action_name
std::vector< Glib::ustring > get_actions()
static InkscapeApplication * instance()
Singleton instance.
A class to represent ways functionality is driven by shift modifiers.
Definition modifiers.h:103
static Modifier * get(Type index)
A function to turn an enum index into a modifier object.
Definition modifiers.h:215
static std::vector< Modifier const * > getList()
List all the modifiers available.
Preference storage class.
Definition preferences.h:61
Glib::ustring getString(Glib::ustring const &pref_path, Glib::ustring const &def="")
Retrieve an UTF-8 string.
static Preferences * get()
Access the singleton Preferences object.
void setString(Glib::ustring const &pref_path, Glib::ustring const &value)
Set an UTF-8 string value.
std::vector< Glib::ustring > list_all_actions()
bool _add_shortcut(Glib::ustring const &detailed_action_name, Glib::ustring const &trigger_string, bool user, bool cache_action_names)
Add a shortcut.
bool invoke_action(Gtk::AccelKey const &shortcut)
Trigger action from a shortcut.
bool clear_user_shortcuts()
Remove all user's shortcuts (simply overwrites existing file).
std::unordered_multimap< std::string, ShortcutValue > _shortcuts
Definition shortcuts.h:164
static Glib::ustring get_label(const Gtk::AccelKey &shortcut)
static std::vector< std::pair< Glib::ustring, std::string > > get_file_names()
Get a list of filenames to populate menu in preferences dialog.
std::vector< Glib::ustring > list_all_detailed_action_names()
std::set< std::string > _list_action_names_cache
Definition shortcuts.h:154
void _dump_all_recursive(Gtk::Widget *widget)
static Gtk::AccelKey get_from(GtkEventControllerKey const *controller, unsigned keyval, unsigned keycode, GdkModifierType state, bool fix=false)
Controller provides the group. It can be nullptr; if so, we use group 0.
void update_gui_text_recursive(Gtk::Widget *widget)
void _clear()
Clear all shortcuts.
bool _read(Glib::RefPtr< Gio::File > const &file, bool user_set=false)
Read a shortcut file.
bool write_user()
Write user shortcuts to file.
const std::set< std::string > & _list_action_names(bool cached)
Get a sorted list of the non-detailed names of all actions.
sigc::signal< void()> _changed
Definition shortcuts.h:173
bool _write(Glib::RefPtr< Gio::File > const &file, What what=User)
std::vector< Glib::ustring > get_triggers(Glib::ustring const &action_name) const
Returns a vector of triggers for a given detailed_action_name.
bool add_user_shortcut(Glib::ustring const &detailed_action_name, Gtk::AccelKey const &trigger)
Glib::RefPtr< Gio::ListStore< Gtk::Shortcut > > _liststore
Definition shortcuts.h:170
bool is_user_set(Glib::ustring const &detailed_action_name)
Return if user set shortcut for Gio::Action.
bool remove_user_shortcut(Glib::ustring const &detailed_action_name)
sigc::connection connect_changed(sigc::slot< void()> const &slot)
Connects to a signal emitted whenever the shortcuts change.
bool _remove_shortcut_trigger(Glib::ustring const &trigger)
Remove shortcuts via AccelKey.
Gtk::Application *const app
Definition shortcuts.h:167
static Gtk::AccelKey get_from_event(KeyEvent const &event, bool fix=false)
std::vector< Glib::ustring > get_actions(Glib::ustring const &trigger) const
Returns a vector of detailed_action_names for a given trigger.
bool _remove_shortcuts(Glib::ustring const &detailed_action_name)
Remove all shortcuts for a detailed action.
Interface for refcounted XML nodes.
Definition node.h:80
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 char const * attribute(char const *key) const =0
Get the string representation of a node's attribute.
Css & result
std::unordered_map< std::string, std::unique_ptr< SPDocument > > map
std::string sp_relative_path_from_path(std::string const &path, std::string const &base)
Returns a form of path relative to base if that is easy to construct (eg if path appears to be in the...
Definition dir-util.cpp:21
TODO: insert short description here.
auto absolute(Geom::Point const &a)
Definition geom.h:93
Inkscape::XML::Node * node
Inkscape - An SVG editor.
Glib::ustring label
Definition desktop.h:50
static R & release(R &r)
Decrements the reference count of a anchored object.
std::string get_path_string(Domain domain, Type type, char const *filename, char const *extra)
Definition resource.cpp:148
std::vector< std::string > get_filenames(Type type, std::vector< const char * > const &extensions, std::vector< const char * > const &exclusions)
Definition resource.cpp:267
unsigned get_latin_keyval_impl(unsigned const event_keyval, unsigned const event_keycode, GdkModifierType const event_state, unsigned const event_group, unsigned *consumed_modifiers)
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
Helper class to stream background task notifications as a series of messages.
Gdk::ModifierType parse_modifier_string(char const *const modifiers_string)
static Gtk::AccelKey get_from_event_impl(unsigned const event_keyval, unsigned const event_keycode, GdkModifierType const event_state, unsigned const event_group, bool const fix)
static Glib::ustring join(std::vector< Glib::ustring > const &accels, char const separator)
static void append(std::vector< T > &target, std::vector< T > &&source)
Glib::RefPtr< Gio::File > choose_file_save(Glib::ustring const &title, Gtk::Window *parent, Glib::RefPtr< Gio::ListStore< Gtk::FileFilter > > const &filters_model, std::string const &file_name, std::string &current_folder)
Synchronously run a Gtk::FileDialog to select a file for saving data.
Glib::RefPtr< Gio::File > choose_file_open(Glib::ustring const &title, Gtk::Window *parent, Glib::RefPtr< Gio::ListStore< Gtk::FileFilter > > const &filters_model, std::string &current_folder, Glib::ustring const &accept)
Synchronously run a Gtk::FileDialog to open a single file for reading data.
static cairo_user_data_key_t key
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::IO::Resource - simple resource API.
Inkscape::XML::SimpleDocument - generic XML document implementation.
unsigned modifiers
The modifiers mask immediately before the event.
A key has been pressed.
uint32_t keyval
The key that was pressed/released. (Matches gdkkeysyms.h.)
int group
The keyboard group.
uint16_t keycode
The raw code of the key that was pressed/released.
Glib::RefPtr< Gtk::Shortcut > shortcut
Definition shortcuts.h:160
Interface for XML documents.
Definition document.h:43
std::vector< Texture > unused
Glib::ustring name
Definition toolbars.cpp:55
Interface for XML nodes.