Context for the "About" label
[inkscape.git] / src / ui / shortcuts.cpp
blob2dfe345b96bf357a07891c9dcd8e024e5f9a15b7
1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * Shortcuts
5 * Copyright (C) 2020 Tavmjong Bah
6 * Rewrite of code (C) MenTalguY and others.
8 * The contents of this file may be used under the GNU General Public License Version 2 or later.
12 #include "shortcuts.h"
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>
32 #include "document.h"
33 #include "inkscape-application.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/filedialog.h" // Importing/exporting files.
42 #include "ui/util.h"
43 #include "ui/widget/events/canvas-event.h"
44 #include "xml/simple-document.h"
45 #include "xml/node.h"
46 #include "xml/node-iterators.h"
48 using namespace Inkscape::IO::Resource;
49 using namespace Inkscape::Modifiers;
51 namespace Inkscape {
53 Shortcuts::Shortcuts()
54 : app{dynamic_cast<Gtk::Application *>(Gio::Application::get_default().get())}
56 if (!app) {
57 std::cerr << "Shortcuts::Shortcuts: No app! Shortcuts cannot be used without a Gtk::Application!" << std::endl;
58 return;
61 // Shared among all Shortcut controllers.
62 _liststore = Gio::ListStore<Gtk::Shortcut>::create();
65 Shortcuts::~Shortcuts() {}
67 void
68 Shortcuts::init() {
69 initialized = true;
71 // Clear arrays (we may be re-reading).
72 _clear();
74 bool success = false; // We've read a shortcut file!
75 std::string path;
77 // ------------ Open Inkscape shortcut file ------------
79 // Try filename from preferences first.
80 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
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;
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;
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());
104 if (!success) {
105 // std::cerr << "Shortcut::Shortcut: " << reason << ", trying default.xml" << std::endl;
107 Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SYSTEM, KEYS, "default.xml"));
108 success = _read(file);
111 if (!success) {
112 std::cerr << "Shortcut::Shortcut: Failed to read file default.xml, trying inkscape.xml" << std::endl;
114 Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SYSTEM, KEYS, "inkscape.xml"));
115 success = _read(file);
118 if (!success) {
119 std::cerr << "Shortcut::Shortcut: Failed to read file inkscape.xml; giving up!" << std::endl;
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);
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);
136 // Emit changed signal in case of read-reading (user selects different file).
137 _changed.emit();
139 // _dump();
143 // ****** User Shortcuts ******
145 // Add a user shortcut, updating user's shortcut file if successful.
146 bool
147 Shortcuts::add_user_shortcut(Glib::ustring const &detailed_action_name,const Gtk::AccelKey& trigger)
149 // Add shortcut, if successful, save to file.
150 // Performance is not critical here. This is only called from the preferences dialog.
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();
160 // Save
161 return write_user();
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;
169 // Remove a user shortcut, updating user's shortcut file.
170 bool
171 Shortcuts::remove_user_shortcut(Glib::ustring const &detailed_action_name)
173 // Check if really user shortcut.
174 bool user_shortcut = is_user_set(detailed_action_name);
176 if (!user_shortcut) {
177 // We don't allow removing non-user shortcuts.
178 return false;
181 if (_remove_shortcuts(detailed_action_name)) {
182 // Save
183 write_user();
185 // Reread to get original shortcut (if any). And emit changes signal.
186 init();
188 return true;
191 std::cerr << "Shortcuts::remove_user_shortcut: Failed to remove shortcut for: "
192 << detailed_action_name.raw() << std::endl;
193 return false;
198 * Remove all user's shortcuts (simply overwrites existing file).
200 bool
201 Shortcuts::clear_user_shortcuts()
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);
212 // Re-read everything! And emit changed signal.
213 init();
214 return true;
218 * Return if user set shortcut for Gio::Action.
220 bool
221 Shortcuts::is_user_set(Glib::ustring const &detailed_action_name)
223 auto it = _shortcuts.find(detailed_action_name);
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);
230 return false;
234 * Write user shortcuts to file.
236 bool
237 Shortcuts::write_user()
239 Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
240 return _write(file, User);
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.
248 void
249 Shortcuts::update_gui_text_recursive(Gtk::Widget* widget)
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;
267 auto const &triggers = get_triggers(action);
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);
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 += " ";
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) + ")";
289 // Update tooltip.
290 widget->set_tooltip_markup(tooltip);
294 for (auto const child : UI::get_children(*widget)) {
295 update_gui_text_recursive(child);
299 // ******** Invoke Actions *******
301 /** Trigger action from a shortcut. Useful if we want to intercept the event from GTK */
302 bool
303 Shortcuts::invoke_action(Gtk::AccelKey const &shortcut)
305 // This can be simplified in GTK4.
306 Glib::ustring accel = Gtk::Accelerator::name(shortcut.get_key(), shortcut.get_mod());
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;
325 return false;
328 /** Trigger action from a shortcut. Useful if we want to intercept the event from GTK */
329 // Used by Tools
330 bool
331 Shortcuts::invoke_action(KeyEvent const &event)
333 auto const shortcut = get_from_event(event);
334 return invoke_action(shortcut);
337 /** Trigger action from a shortcut. Useful if we want to intercept the event from GTK */
338 // NOT USED CURRENTLY
339 bool
340 Shortcuts::invoke_action(GtkEventControllerKey const * const controller,
341 unsigned const keyval, unsigned const keycode,
342 GdkModifierType const state)
344 auto const shortcut = get_from(controller, keyval, keycode, state);
345 return invoke_action(shortcut);
348 // ******* Utility *******
351 * Returns a vector of triggers for a given detailed_action_name.
353 std::vector<Glib::ustring>
354 Shortcuts::get_triggers(Glib::ustring const &detailed_action_name) const
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);
361 return triggers;
365 * Returns a vector of detailed_action_names for a given trigger.
367 std::vector<Glib::ustring>
368 Shortcuts::get_actions(Glib::ustring const &trigger) const
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);
376 return actions;
379 Glib::ustring
380 Shortcuts::get_label(const Gtk::AccelKey& shortcut)
382 Glib::ustring label;
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 += " ";
392 label += Gtk::Accelerator::get_label(shortcut.get_key(), shortcut.get_mod());
395 return label;
398 static Gtk::AccelKey
399 get_from_event_impl(unsigned const event_keyval, unsigned const event_keycode,
400 GdkModifierType const event_state, unsigned const event_group,
401 bool const fix)
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;
408 auto consumed_modifiers = 0u;
409 auto keyval = Inkscape::UI::Tools::get_latin_keyval_impl(
410 event_keyval, event_keycode, event_state, event_group, &consumed_modifiers);
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);
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;
428 auto const unused_modifiers = Gdk::ModifierType(static_cast<unsigned>(initial_modifiers)
429 & ~consumed_modifiers
430 & GDK_MODIFIER_MASK
431 & ~GDK_LOCK_MASK);
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));
440 * Return: keyval translated to group 0 in lower 32 bits, modifier encoded in upper 32 bits.
442 * Usage of group 0 (i.e. the main, typically English layout) instead of simply event->keyval
443 * ensures that shortcuts work regardless of the active keyboard layout (e.g. Cyrillic).
445 * The returned modifiers are the modifiers that were not "consumed" by the translation and
446 * can be used by the application to define a shortcut, e.g.
447 * - when pressing "Shift+9" the resulting character is "(";
448 * the shift key was "consumed" to make this character and should not be part of the shortcut
449 * - when pressing "Ctrl+9" the resulting character is "9";
450 * the ctrl key was *not* consumed to make this character and must be included in the shortcut
451 * - Exception: letter keys like [A-Z] always need the shift modifier,
452 * otherwise lower case and uper case keys are treated as equivalent.
454 Gtk::AccelKey
455 Shortcuts::get_from(GtkEventControllerKey const * const controller,
456 unsigned const keyval, unsigned const keycode, GdkModifierType const state,
457 bool const fix)
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);
465 Gtk::AccelKey
466 Shortcuts::get_from(Gtk::EventControllerKey const &controller,
467 unsigned keyval, unsigned keycode, Gdk::ModifierType state, bool fix)
469 return get_from_event_impl(keyval, keycode, static_cast<GdkModifierType>(state), controller.get_group(), fix);
472 Gtk::AccelKey
473 Shortcuts::get_from_event(KeyEvent const &event, bool fix)
475 return get_from_event_impl(event.keyval, event.keycode,
476 static_cast<GdkModifierType>(event.modifiers), event.group, fix);
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.
481 std::vector<Glib::ustring>
482 Shortcuts::list_all_detailed_action_names()
484 auto *iapp = InkscapeApplication::instance();
485 InkActionExtraData& action_data = iapp->get_action_extra_data();
486 return action_data.get_actions();
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.
491 std::vector<Glib::ustring>
492 Shortcuts::list_all_actions()
494 std::vector<Glib::ustring> all_actions;
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));
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));
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));
520 } else {
521 std::cerr << "Shortcuts::list_all_actions: No document map!" << std::endl;
526 return all_actions;
529 template <typename T>
530 static void append(std::vector<T> &target, std::vector<T> &&source)
532 target.insert(target.end(), std::move_iterator{source.begin()}, std::move_iterator{source.end()});
536 * Get a list of filenames to populate menu in preferences dialog.
538 std::vector<std::pair<Glib::ustring, std::string>>
539 Shortcuts::get_file_names()
541 using namespace Inkscape::IO::Resource;
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"}));
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));
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;
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);
568 names_and_paths.emplace_back(std::move(label), std::move(filename_relative));
569 break;
572 if (!iter) {
573 std::cerr << "Shortcuts::get_File_names: not a shortcut keys file: " << filename << std::endl;
576 Inkscape::GC::release(document);
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;
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";
589 if (it_default != names_and_paths.end()) {
590 std::rotate(names_and_paths.begin(), it_default, it_default+1);
593 return names_and_paths;
596 // Dialogs
598 // Import user shortcuts from a file.
599 bool
600 Shortcuts::import_shortcuts() {
601 // Users key directory.
602 auto const &directory = get_path_string(USER, KEYS, {});
604 // Create and show the dialog
605 Gtk::Window* window = app->get_active_window();
606 if (!window) {
607 return false;
610 auto const importFileDialog = std::unique_ptr<UI::Dialog::FileOpenDialog>{
611 UI::Dialog::FileOpenDialog::create(*window, directory, Inkscape::UI::Dialog::CUSTOM_TYPE,
612 _("Select a file to import"))};
613 importFileDialog->addFilterMenu(_("Inkscape shortcuts (*.xml)"), "*.xml");
614 bool const success = importFileDialog->show();
616 if (!success) {
617 return false;
620 // Get file and read.
621 auto file_read = importFileDialog->getFile();
622 if (!_read(file_read, true)) {
623 std::cerr << "Shortcuts::import_shortcuts: Failed to read file!" << std::endl;
624 return false;
627 // Save
628 return write_user();
631 bool
632 Shortcuts::export_shortcuts() {
633 // Users key directory.
634 auto const &directory = get_path_string(USER, KEYS, {});
636 // Create and show the dialog
637 Gtk::Window* window = app->get_active_window();
638 if (!window) {
639 return false;
642 auto const saveFileDialog = std::unique_ptr<UI::Dialog::FileSaveDialog>{
643 UI::Dialog::FileSaveDialog::create(*window, directory, Inkscape::UI::Dialog::CUSTOM_TYPE,
644 _("Select a filename for export"),
645 {}, {}, Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS)};
646 saveFileDialog->addFilterMenu(_("Inkscape shortcuts (*.xml)"), "*.xml");
647 saveFileDialog->setCurrentName("shortcuts.xml");
648 bool success = saveFileDialog->show();
650 // Get file name and write.
651 if (success) {
652 auto file = saveFileDialog->getFile();
653 success = _write(file, User);
654 if (!success) {
655 std::cerr << "Shortcuts::export_shortcuts: Failed to save file!" << std::endl;
658 return success;
661 /** Connects to a signal emitted whenever the shortcuts change. */
662 sigc::connection Shortcuts::connect_changed(sigc::slot<void ()> const &slot)
664 return _changed.connect(slot);
668 // -------- Private --------
670 [[nodiscard]] static Glib::ustring
671 join(std::vector<Glib::ustring> const &accels, char const separator)
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;
681 return result;
684 Gdk::ModifierType
685 parse_modifier_string(char const * const modifiers_string)
687 Gdk::ModifierType modifiers{};
688 if (modifiers_string) {
689 std::vector<Glib::ustring> mod_vector = Glib::Regex::split_simple("\\s*,\\s*", modifiers_string);
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;
715 return modifiers;
718 // ******* Files *******
721 * Read a shortcut file.
723 bool
724 Shortcuts::_read(Glib::RefPtr<Gio::File> const &file, bool const user_set)
726 if (!file->query_exists()) {
727 std::cerr << "Shortcut::read: file does not exist: " << file->get_path() << std::endl;
728 return false;
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;
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;
744 if (!iter) {
745 std::cerr << "Shortcuts::read: File in wrong format: " << file->get_path() << std::endl;
746 return false;
749 // Loop through the children in <keys> (may have nested keys)
750 _read(*iter, user_set);
752 return true;
756 * Recursively reads shortcuts from shortcut file.
758 * @param keysnode The <keys> element. Its child nodes will be processed.
759 * @param user_set true if reading from user shortcut file
761 void
762 Shortcuts::_read(XML::Node const &keysnode, bool user_set)
764 bool cache_action_list = false; // see below
765 XML::NodeConstSiblingIterator iter {keysnode.firstChild()};
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;
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;
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);
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);
795 char const * const disabled_attr = iter->attribute("disabled");
796 if (disabled_attr && strcmp(disabled_attr, "true") == 0) {
797 and_modifier = NEVER;
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);
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;
816 // Gio::Action's
817 char const * const gaction = iter->attribute("gaction");
818 char const * const keys = iter->attribute("keys");
819 if (gaction && keys) {
821 // Trim leading spaces
822 Glib::ustring Keys = keys;
823 auto p = Keys.find_first_not_of(" ");
824 Keys = Keys.erase(0, p);
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.
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;
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 // }
849 continue;
854 // In principle, we only write User shortcuts. But for debugging, we might want to write something else.
855 bool
856 Shortcuts::_write(Glib::RefPtr<Gio::File> const &file, What const what)
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");
871 document->appendChild(node);
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) )
880 auto const &triggers = get_triggers(action_name);
881 if (!triggers.empty()) {
882 XML::Node * node = document->createElement("bind");
884 node->setAttribute("gaction", action_name);
886 auto const keys = join(triggers, ',');
887 node->setAttribute("keys", keys);
889 document->root()->appendChild(node);
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());
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);
909 document->root()->appendChild(node);
913 sp_repr_save_file(document, file->get_path().c_str(), nullptr);
914 GC::release(document);
916 return true;
919 // ******* Add/remove shortcuts *******
922 * Add a shortcut. Other shortcuts may already exist for the same action.
923 * For user shortcut, all other shortcuts for actions should have been removed.
924 * If shortcut added, return true.
926 * cache_action_names: Skip recomputing the list of action names.
927 * Set to false, except if you are certain that the list hasn't changed.
928 * For details see the "cached" parameter in _list_action_names().
930 bool Shortcuts::_add_shortcut(Glib::ustring const &detailed_action_name, Glib::ustring const &trigger_string, bool user,
931 bool cache_action_names)
933 // Format has changed between Gtk3 and Gtk4. Pass through xxx to standardize form.
934 auto str = trigger_string.raw();
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>");
944 #endif
946 Gtk::AccelKey key(str);
948 auto trigger_normalized = key.get_abbrev();
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);
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;
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 // }
969 // A user shortcut replaces all others.
970 if (user) {
971 _remove_shortcuts(detailed_action_name);
974 auto const trigger = Gtk::ShortcutTrigger::parse_string(trigger_normalized);
975 g_assert(trigger);
977 auto const action = Gtk::NamedAction::create(action_name);
978 g_assert(action);
980 auto shortcut = Gtk::Shortcut::create(trigger, action);
981 g_assert(shortcut);
982 if (target) {
983 shortcut->set_arguments(target);
986 _liststore->append(shortcut);
988 auto value = ShortcutValue{std::move(trigger_normalized), std::move(shortcut), user};
989 _shortcuts.emplace(detailed_action_name.raw(), std::move(value));
991 return true;
995 * Remove shortcuts via AccelKey.
996 * Returns true of shortcut(s) removed, false if nothing removed.
998 bool
999 Shortcuts::_remove_shortcut_trigger(Glib::ustring const& trigger)
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;
1013 it = _shortcuts.erase(it);
1014 changed = true;
1015 } else {
1016 ++it;
1020 if (changed) {
1021 return true;
1024 return false;
1028 * Remove all shortcuts for a detailed action. There can be multiple.
1030 bool
1031 Shortcuts::_remove_shortcuts(Glib::ustring const &detailed_action_name)
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);
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;
1047 removed = true;
1048 it = _shortcuts.erase(it);
1049 } else {
1050 ++it;
1054 return removed;
1057 * Get a sorted list of the non-detailed names of all actions.
1059 * "Non-detailed" means that they have been preprocessed with Gio::SimpleAction::parse_detailed_name_variant().
1061 * cached: Remember the last result
1062 * If true, the function returns a copy of the previous result, without checking if that result is still up to date.
1064 * Set to false if you are unsure. This will have a slight performance penalty (ca. 20ms per function call).
1065 * Set to true if you are absolutely sure that the list hasn't changed since the last call.
1067 * If you call this function repeatedly, without doing anything else inbetween that could add or remove actions
1068 * (e.g., installing an extension), then please set this to true for the second and following calls.
1070 const std::set<std::string> &Shortcuts::_list_action_names(bool cached)
1072 if (!cached) {
1073 // std::cerr << "Shortcuts::_list_action_names: invalidating cache." << std::endl;
1074 _list_action_names_cache.clear();
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());
1082 return _list_action_names_cache;
1086 * Clear all shortcuts.
1088 void
1089 Shortcuts::_clear()
1091 _liststore->remove_all();
1092 _shortcuts.clear();
1096 // For debugging.
1097 void
1098 Shortcuts::_dump() {
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
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];
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;
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();
1136 std::cout << action->to_string();
1137 if (variant) {
1138 std::cout << "(" << variant.print() << ")";
1140 std::cout << ": " << trigger->to_string() << std::endl;
1144 void
1145 Shortcuts::_dump_all_recursive(Gtk::Widget* widget)
1147 static unsigned int indent = 0;
1148 ++indent;
1149 for (int i = 0; i < indent; ++i) std::cout << " ";
1151 auto const actionable = dynamic_cast<Gtk::Actionable *>(widget);
1152 auto const action = actionable ? actionable->get_action_name() : "";
1154 std::cout << widget->get_name()
1155 << ": actionable: " << std::boolalpha << static_cast<bool>(actionable)
1156 << ": " << widget->get_tooltip_text()
1157 << ": " << action
1158 << std::endl;
1160 for (auto const child : UI::get_children(*widget)) {
1161 _dump_all_recursive(child);
1164 --indent;
1167 } // namespace Inkscape
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:
1178 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :