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 
13 /* Much of the complexity of this code is in dealing with both Inkscape verbs and Gio::Actions at
14  * the same time. When we remove verbs we can avoid using 'unsigned long long int shortcut' to
15  * track keys and rely directly on Glib::ustring as used by
16  * Gtk::Application::get_accels_for_action(). This will then automatically handle the '<Primary>'
17  * modifier value (which takes care of the differences between Linux and OSX) as well as allowing
18  * us to set multiple accelerators for actions in InkscapePreferences. */
19 
20 #include "shortcuts.h"
21 
22 #include <iostream>
23 #include <iomanip>
24 
25 #include <glibmm.h>
26 #include <glibmm/i18n.h>
27 #include <gtkmm.h>
28 
29 #include "preferences.h"
30 #include "inkscape-application.h"
31 #include "inkscape-window.h"
32 
33 #include "verbs.h"
34 #include "helper/action.h"
35 #include "helper/action-context.h"
36 
37 #include "io/resource.h"
38 #include "io/dir-util.h"
39 
40 #include "ui/modifiers.h"
41 #include "ui/tools/tool-base.h"    // For latin keyval
42 #include "ui/dialog/filedialog.h"  // Importing/exporting files.
43 
44 #include "xml/simple-document.h"
45 #include "xml/node.h"
46 #include "xml/node-iterators.h"
47 
48 using namespace Inkscape::IO::Resource;
49 using namespace Inkscape::Modifiers;
50 
51 namespace Inkscape {
52 
Shortcuts()53 Shortcuts::Shortcuts()
54 {
55     Glib::RefPtr<Gio::Application> gapp = Gio::Application::get_default();
56     app = Glib::RefPtr<Gtk::Application>::cast_dynamic(gapp); // Save as we constantly use it.
57     if (!app) {
58         std::cerr << "Shortcuts::Shortcuts: No app! Shortcuts cannot be used without a Gtk::Application!" << std::endl;
59     }
60 }
61 
62 
63 void
init()64 Shortcuts::init() {
65 
66     initialized = true;
67 
68     // Clear arrays (we may be re-reading).
69     clear();
70 
71     bool success = false; // We've read a shortcut file!
72     std::string path;
73 
74     // ------------ Open Inkscape shortcut file ------------
75 
76     // Try filename from preferences first.
77     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
78 
79     path = prefs->getString("/options/kbshortcuts/shortcutfile");
80     if (!path.empty()) {
81         bool absolute = true;
82         if (!Glib::path_is_absolute(path)) {
83             path = get_path_string(SYSTEM, KEYS, path.c_str());
84             absolute = false;
85         }
86 
87         Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(path);
88         success = read(file);
89         if (!success) {
90             std::cerr << "Shortcut::Shortcut: Unable to read shortcut file listed in preferences: " + path << std::endl;;
91         }
92 
93         // Save relative path to "share/keys" if possible to handle parallel installations of
94         // Inskcape gracefully.
95         if (success && absolute) {
96             std::string relative_path = sp_relative_path_from_path(path, std::string(get_path(SYSTEM, KEYS)));
97             prefs->setString("/options/kbshortcuts/shortcutfile", relative_path.c_str());
98         }
99     }
100 
101     if (!success) {
102         // std::cerr << "Shortcut::Shortcut: " << reason << ", trying default.xml" << std::endl;
103 
104         Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SYSTEM, KEYS, "default.xml"));
105         success = read(file);
106     }
107 
108     if (!success) {
109         std::cerr << "Shortcut::Shortcut: Failed to read file default.xml, trying inkscape.xml" << std::endl;
110 
111         Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SYSTEM, KEYS, "inkscape.xml"));
112         success = read(file);
113     }
114 
115     if (!success) {
116         std::cerr << "Shortcut::Shortcut: Failed to read file inkscape.xml; giving up!" << std::endl;
117     }
118 
119 
120     // ------------ Open User shortcut file -------------
121     Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
122     // Test if file exists before attempting to read to avoid generating warning message.
123     if (file->query_exists()) {
124         read(file, true);
125     }
126 
127     // dump();
128 }
129 
130 // Clear all shortcuts
131 void
clear()132 Shortcuts::clear()
133 {
134     // Verbs: We track everything
135     shortcut_to_verb_map.clear();
136     primary.clear();
137     user_set.clear();
138 
139     // Actions: We rely on Gtk for everything except user/system setting.
140     for (auto action_description : app->list_action_descriptions()) {
141         app->unset_accels_for_action(action_description);
142     }
143     action_user_set.clear();
144 }
145 
146 
147 Gdk::ModifierType
parse_modifier_string(gchar const * modifiers_string,gchar const * verb_name)148 parse_modifier_string(gchar const *modifiers_string, gchar const *verb_name)
149 {
150     Gdk::ModifierType modifiers(Gdk::ModifierType(0));
151     if (modifiers_string) {
152 
153         Glib::ustring str(modifiers_string);
154         std::vector<Glib::ustring> mod_vector = Glib::Regex::split_simple("\\s*,\\s*", modifiers_string);
155 
156         for (auto mod : mod_vector) {
157             if (mod == "Control" || mod == "Ctrl") {
158                 modifiers |= Gdk::CONTROL_MASK;
159             } else if (mod == "Shift") {
160                 modifiers |= Gdk::SHIFT_MASK;
161             } else if (mod == "Alt") {
162                 modifiers |= Gdk::MOD1_MASK;
163             } else if (mod == "Super") {
164                 modifiers |= Gdk::SUPER_MASK; // Not used
165             } else if (mod == "Hyper") {
166                 modifiers |= Gdk::HYPER_MASK; // Not used
167             } else if (mod == "Meta") {
168                 modifiers |= Gdk::META_MASK;
169             } else if (mod == "Primary") {
170 
171                 // System dependent key to invoke menus. (Needed for OSX in particular.)
172                 // We only read "Primary" and never write it for verbs.
173                 auto display = Gdk::Display::get_default();
174                 if (display) {
175                     GdkKeymap* keymap = display->get_keymap();
176                     GdkModifierType type =
177                         gdk_keymap_get_modifier_mask (keymap, GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
178                     gdk_keymap_add_virtual_modifiers(keymap, &type);
179                     if (type & Gdk::CONTROL_MASK)
180                         modifiers |= Gdk::CONTROL_MASK;
181                     else if (type & Gdk::META_MASK)
182                         modifiers |= Gdk::META_MASK;
183                     else {
184                         std::cerr << "Shortcut::read: Unknown primary accelerator!" << std::endl;
185                         modifiers |= Gdk::CONTROL_MASK;
186                     }
187                 } else {
188                     modifiers |= Gdk::CONTROL_MASK;
189                 }
190             } else {
191                 std::cerr << "Shortcut::read: Unknown GDK modifier: " << mod.c_str() << std::endl;
192             }
193         }
194     }
195     return modifiers;
196 }
197 
198 
199 // Read a shortcut file. We do not check for conflicts between verbs and actions.
200 bool
read(Glib::RefPtr<Gio::File> file,bool user_set)201 Shortcuts::read(Glib::RefPtr<Gio::File> file, bool user_set)
202 {
203     if (!file->query_exists()) {
204         std::cerr << "Shortcut::read: file does not exist: " << file->get_path() << std::endl;
205         return false;
206     }
207 
208     XML::Document *document = sp_repr_read_file(file->get_path().c_str(), nullptr);
209     if (!document) {
210         std::cerr << "Shortcut::read: could not parse file: " << file->get_path() << std::endl;
211         return false;
212     }
213 
214     XML::NodeConstSiblingIterator iter = document->firstChild();
215     for ( ; iter ; ++iter ) { // We iterate in case of comments.
216         if (strcmp(iter->name(), "keys") == 0) {
217             break;
218         }
219     }
220 
221     if (!iter) {
222         std::cerr << "Shortcuts::read: File in wrong format: " << file->get_path() << std::endl;
223         return false;
224     }
225 
226     // Loop through the children in <keys> (may have nested keys)
227     _read(*iter, user_set);
228 
229     return true;
230 }
231 
232 /**
233  * Recursively reads shortcuts from shortcut file. Does not check for conflicts between verbs and actions.
234  *
235  * @param keysnode The <keys> element. Its child nodes will be processed.
236  * @param user_set true if reading from user shortcut file
237  */
238 void
_read(XML::Node const & keysnode,bool user_set)239 Shortcuts::_read(XML::Node const &keysnode, bool user_set)
240 {
241     XML::NodeConstSiblingIterator iter {keysnode.firstChild()};
242     for ( ; iter ; ++iter ) {
243 
244         if (strcmp(iter->name(), "modifier") == 0) {
245 
246             gchar const *mod_name = iter->attribute("action");
247             if (!mod_name) {
248                 std::cerr << "Shortcuts::read: Missing modifier for action!" << std::endl;;
249                 continue;
250             }
251 
252             Modifier *mod = Modifier::get(mod_name);
253             if (mod == nullptr) {
254                 std::cerr << "Shortcuts::read: Can't find modifer: " << mod_name << std::endl;
255                 continue;
256             }
257 
258             // If mods isn't specified then it should use default, if it's an empty string
259             // then the modifier is None (i.e. happens all the time without a modifier)
260             KeyMask and_modifier = NOT_SET;
261             gchar const *mod_attr = iter->attribute("modifiers");
262             if (mod_attr) {
263                 and_modifier = (KeyMask) parse_modifier_string(mod_attr, mod_name);
264             }
265 
266             // Parse not (cold key) modifier
267             KeyMask not_modifier = NOT_SET;
268             gchar const *not_attr = iter->attribute("not_modifiers");
269             if (not_attr) {
270                 not_modifier = (KeyMask) parse_modifier_string(not_attr, mod_name);
271             }
272 
273             gchar const *disabled_attr = iter->attribute("disabled");
274             if (disabled_attr && strcmp(disabled_attr, "true") == 0) {
275                 and_modifier = NEVER;
276             }
277 
278             if (and_modifier != NOT_SET) {
279                 if(user_set) {
280                     mod->set_user(and_modifier, not_modifier);
281                 } else {
282                     mod->set_keys(and_modifier, not_modifier);
283                 }
284             }
285             continue;
286         } else if (strcmp(iter->name(), "keys") == 0) {
287             _read(*iter, user_set);
288             continue;
289         } else if (strcmp(iter->name(), "bind") != 0) {
290             // Unknown element, do not complain.
291             continue;
292         }
293 
294         // Gio::Action's
295         gchar const *gaction = iter->attribute("gaction");
296         gchar const *keys    = iter->attribute("keys");
297         if (gaction && keys) {
298 
299             std::vector<Glib::ustring> key_vector = Glib::Regex::split_simple("\\s*,\\s*", keys);
300             // Set one shortcut at a time so we can check if it has been previously used.
301             for (auto key : key_vector) {
302                 add_shortcut(gaction, key, user_set);
303             }
304 
305             // Uncomment to see what the cat dragged in.
306             // if (!key_vector.empty()) {
307             //     std::cout << "Shortcut::read: gaction: "<< gaction
308             //               << ", user set: " << std::boolalpha << user_set << ", ";
309             //     for (auto key : key_vector) {
310             //         std::cout << key << ", ";
311             //     }
312             //     std::cout << std::endl;
313             // }
314 
315             continue;
316         }
317 
318         // Legacy verbs
319         bool is_primary =
320             iter->attribute("display")                        &&
321             strcmp(iter->attribute("display"), "false") != 0  &&
322             strcmp(iter->attribute("display"), "0")     != 0;
323 
324         gchar const *verb_name = iter->attribute("action");
325         if (!verb_name) {
326             std::cerr << "Shortcut::read: Missing verb name!" << std::endl;
327             continue;
328         }
329 
330         Inkscape::Verb *verb = Inkscape::Verb::getbyid(verb_name);
331         if (!verb) {
332             std::cerr << "Shortcut::read: invalid verb: " << verb_name << std::endl;
333             continue;
334         }
335 
336         gchar const *keyval_name = iter->attribute("key");
337         if (!keyval_name  ||!*keyval_name) {
338             // OK. Verb without shortcut (for reference).
339             continue;
340         }
341 
342         guint keyval = gdk_keyval_from_name(keyval_name);
343         if (keyval == GDK_KEY_VoidSymbol || keyval == 0) {
344             std::cerr << "Shortcut::read: Unknown keyval " << keyval_name << " for " << verb_name << std::endl;
345             continue;
346         }
347 
348         Gdk::ModifierType modifiers = parse_modifier_string(iter->attribute("modifiers"), verb_name);
349 
350         add_shortcut (verb_name, Gtk::AccelKey(keyval, modifiers), user_set, is_primary);
351     }
352 }
353 
354 bool
write_user()355 Shortcuts::write_user() {
356     Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
357     return write(file, User);
358 }
359 
360 // In principle, we only write User shortcuts. But for debugging, we might want to write something else.
361 bool
write(Glib::RefPtr<Gio::File> file,What what)362 Shortcuts::write(Glib::RefPtr<Gio::File> file, What what) {
363 
364     auto *document = new XML::SimpleDocument();
365     XML::Node * node = document->createElement("keys");
366     switch (what) {
367         case User:
368             node->setAttribute("name", "User Shortcuts");
369             break;
370         case System:
371             node->setAttribute("name", "System Shortcuts");
372             break;
373         default:
374             node->setAttribute("name", "Inkscape Shortcuts");
375     }
376 
377     document->appendChild(node);
378 
379     // Legacy verbs
380     for (auto entry : shortcut_to_verb_map) {
381         Gtk::AccelKey shortcut = entry.first;
382         Verb *verb = entry.second;
383         if ( what == All                               ||
384             (what == System && !is_user_set(shortcut)) ||
385             (what == User   &&  is_user_set(shortcut)) )
386         {
387             unsigned int      key_val = shortcut.get_key();
388             Gdk::ModifierType mod_val = shortcut.get_mod();
389 
390             gchar *key = gdk_keyval_name (key_val);
391             Glib::ustring mod = get_modifiers_verb (mod_val);
392             Glib::ustring id  = verb->get_id();
393 
394             XML::Node * node = document->createElement("bind");
395             node->setAttribute("key", key);
396             node->setAttributeOrRemoveIfEmpty("modifiers", mod);
397             node->setAttribute("action", id);
398             if (primary[verb].get_key() == shortcut.get_key() && primary[verb].get_mod() == shortcut.get_mod()) {
399                 node->setAttribute("display", "true");
400             }
401             document->root()->appendChild(node);
402         }
403     }
404 
405     // Actions: write out all actions with accelerators.
406     for (auto action_description : app->list_action_descriptions()) {
407         if ( what == All                                 ||
408             (what == System && !action_user_set[action_description]) ||
409             (what == User   &&  action_user_set[action_description]) )
410         {
411             std::vector<Glib::ustring> accels = app->get_accels_for_action(action_description);
412             if (!accels.empty()) {
413 
414                 XML::Node * node = document->createElement("bind");
415 
416                 node->setAttribute("gaction", action_description);
417 
418                 Glib::ustring keys;
419                 for (auto accel : accels) {
420                     keys += accel;
421                     keys += ",";
422                 }
423                 keys.resize(keys.size() - 1);
424                 node->setAttribute("keys", keys);
425 
426                 document->root()->appendChild(node);
427             }
428         }
429     }
430 
431     for(auto modifier: Inkscape::Modifiers::Modifier::getList()) {
432         if (what == User && modifier->is_set_user()) {
433             XML::Node * node = document->createElement("modifier");
434             node->setAttribute("action", modifier->get_id());
435 
436             if (modifier->get_config_user_disabled()) {
437                 node->setAttribute("disabled", "true");
438             } else {
439                 node->setAttribute("modifiers", modifier->get_config_user_and());
440                 auto not_mask = modifier->get_config_user_not();
441                 if (!not_mask.empty() and not_mask != "-") {
442                     node->setAttribute("not_modifiers", not_mask);
443                 }
444             }
445 
446             document->root()->appendChild(node);
447         }
448     }
449 
450     sp_repr_save_file(document, file->get_path().c_str(), nullptr);
451     GC::release(document);
452 
453     return true;
454 };
455 
456 // Return the primary shortcut for a verb or GDK_KEY_VoidSymbol if not found.
457 Gtk::AccelKey
get_shortcut_from_verb(Verb * verb)458 Shortcuts::get_shortcut_from_verb(Verb *verb)
459 {
460     for (auto const& it : shortcut_to_verb_map) {
461         if (it.second == verb) {
462             return primary[verb];
463         }
464     }
465 
466     return (Gtk::AccelKey());
467 }
468 
469 
470 // Return verb corresponding to shortcut or nullptr if no verb.
471 Verb*
get_verb_from_shortcut(const Gtk::AccelKey & shortcut)472 Shortcuts::get_verb_from_shortcut(const Gtk::AccelKey& shortcut)
473 {
474     auto it = shortcut_to_verb_map.find(shortcut);
475     if (it != shortcut_to_verb_map.end()) {
476         return it->second;
477     } else {
478         return nullptr;
479     }
480 }
481 
482 // Return if user set shortcut for verb.
483 bool
is_user_set(Gtk::AccelKey verb_shortcut)484 Shortcuts::is_user_set(Gtk::AccelKey verb_shortcut)
485 {
486     return (user_set.find(verb_shortcut) != user_set.end());
487 }
488 
489 // Return if user set shortcut for Gio::Action.
490 bool
is_user_set(Glib::ustring & action)491 Shortcuts::is_user_set(Glib::ustring& action)
492 {
493     auto it = action_user_set.find(action);
494     if (it != action_user_set.end()) {
495         return action_user_set[action];
496     } else {
497         return false;
498     }
499 }
500 
501 // Invoke verb corresponding to shortcut.
502 bool
invoke_verb(GdkEventKey const * event,UI::View::View * view)503 Shortcuts::invoke_verb(GdkEventKey const *event, UI::View::View *view)
504 {
505     // std::cout << "Shortcuts::invoke_verb: "
506     //           << std::hex << event->keyval << " "
507     //           << std::hex << event->state << std::endl;
508     Gtk::AccelKey shortcut = get_from_event(event);
509 
510     Verb* verb = get_verb_from_shortcut(shortcut);
511     if (verb) {
512         SPAction *action = verb->get_action(Inkscape::ActionContext(view));
513         if (action) {
514             sp_action_perform(action, nullptr);
515             return true;
516         }
517     }
518 
519     return false;
520 }
521 
522 // Get a list of detailed action names (as defined in action extra data).
523 // This is more useful for shortcuts than a list of all actions.
524 std::vector<Glib::ustring>
list_all_detailed_action_names()525 Shortcuts::list_all_detailed_action_names()
526 {
527     auto *iapp = InkscapeApplication::instance();
528     InkActionExtraData& action_data = iapp->get_action_extra_data();
529     return action_data.get_actions();
530 }
531 
532 // Get a list of all actions (application, window, and document), properly prefixed.
533 // We need to do this ourselves as Gtk::Application does not have a function for this.
534 std::vector<Glib::ustring>
list_all_actions()535 Shortcuts::list_all_actions()
536 {
537     std::vector<Glib::ustring> all_actions;
538 
539     std::vector<Glib::ustring> actions = app->list_actions();
540     std::sort(actions.begin(), actions.end());
541     for (auto action : actions) {
542         all_actions.emplace_back("app." + action);
543     }
544 
545     auto gwindow = app->get_active_window();
546     auto window = dynamic_cast<InkscapeWindow *>(gwindow);
547     if (window) {
548         std::vector<Glib::ustring> actions = window->list_actions();
549         std::sort(actions.begin(), actions.end());
550         for (auto action : actions) {
551             all_actions.emplace_back("win." + action);
552         }
553 
554         auto document = window->get_document();
555         if (document) {
556             auto map = document->getActionGroup();
557             if (map) {
558                 std::vector<Glib::ustring> actions = map->list_actions();
559                 for (auto action : actions) {
560                     all_actions.emplace_back("doc." + action);
561                 }
562             } else {
563                 std::cerr << "Shortcuts::list_all_actions: No document map!" << std::endl;
564             }
565         }
566     }
567 
568     return all_actions;
569 }
570 
571 
572 // Add a shortcut, removing any previous use of shortcut.
573 // is_primary is for use with verbs and can be removed after verbs are gone.
574 bool
add_shortcut(Glib::ustring name,const Gtk::AccelKey & shortcut,bool user,bool is_primary)575 Shortcuts::add_shortcut(Glib::ustring name, const Gtk::AccelKey& shortcut, bool user, bool is_primary)
576 {
577     // Remove previous use of shortcut (already removed if new user shortcut).
578     if (Glib::ustring old_name = remove_shortcut(shortcut); old_name != "") {
579         std::cerr << "Shortcut::add_shortcut: duplicate shortcut found for: " << shortcut.get_abbrev()
580                   << "  Old: " << old_name << "  New: " << name << " !" << std::endl;
581     }
582 
583     // Add shortcut
584 
585     // Try verb first
586     Verb* verb = Verb::getbyid(name.c_str(), false); // false => no error message
587     if (verb) {
588         if (shortcut.is_null()) {
589             // should we return false?
590             // currently just used as an early return
591             return true;
592         }
593         shortcut_to_verb_map[shortcut] = verb;
594         if (is_primary) {
595             primary[verb] = shortcut;
596         }
597         if (user) {
598             user_set.insert(shortcut);
599         }
600         return true;
601     }
602 
603     // To be removed after verbs are gone and initialization happens in InkscapeWindow constructor.
604     // We can then check if action exists before assigning shortcut to it.
605     // If not verb, must be action!
606     std::vector<Glib::ustring> accels = app->get_accels_for_action(name);
607     accels.push_back(shortcut.get_abbrev());
608     app->set_accels_for_action(name, accels);
609     action_user_set[name] = user;
610     return true;
611 
612     // To be uncommented after verbs are gone.
613     // for (auto action : list_all_detailed_action_names()) {
614     //     if (action == name) {
615     //         // Action exists
616     //         app->set_accel_for_action(action, shortcut.get_abbrev());
617     //         action_user_set[action] = user;
618     //         return true;
619     //     }
620     // }
621 
622     // // Oops, not an action either!
623     // std::cerr << "Shortcuts::add_shortcut: No Verb or Action for " << name << std::endl;
624     // return false;
625 }
626 
627 
628 // Add a user shortcut, updating user's shortcut file if successful.
629 bool
add_user_shortcut(Glib::ustring name,const Gtk::AccelKey & shortcut)630 Shortcuts::add_user_shortcut(Glib::ustring name, const Gtk::AccelKey& shortcut)
631 {
632     // Remove previous shortcut(s) for verb/action.
633     remove_shortcut(name);
634 
635     // Remove previous use of shortcut from other verbs/actions.
636     remove_shortcut(shortcut);
637 
638     // Add shortcut, if successful, save to file.
639     if (add_shortcut(name, shortcut, true, true)) {  // Always user, always primary (verbs only).
640         // Save
641         return write_user();
642     }
643 
644     std::cerr << "Shortcut::add_user_shortcut: Failed to add: " << name << " with shortcut " << shortcut.get_abbrev() << std::endl;
645     return false;
646 };
647 
648 
649 // Remove a shortcut via key. Return name of removed verb or action.
650 Glib::ustring
remove_shortcut(const Gtk::AccelKey & shortcut)651 Shortcuts::remove_shortcut(const Gtk::AccelKey& shortcut)
652 {
653     // Try verb first
654     if (auto it = shortcut_to_verb_map.find(shortcut); it != shortcut_to_verb_map.end()) {
655         auto verb = it->second;
656         shortcut_to_verb_map.erase(it);
657         auto primary_shortcut = get_shortcut_from_verb(verb);
658         // if primary shortcut is still in shortcut_to_verb_map, it is a different shortcut
659         if (shortcut_to_verb_map.find(primary_shortcut) == shortcut_to_verb_map.end()) {
660             primary.erase(verb);
661         }
662         user_set.erase(shortcut);
663         return verb->get_id();
664     }
665 
666     // Try action second
667     std::vector<Glib::ustring> actions = app->get_actions_for_accel(shortcut.get_abbrev());
668     if (actions.empty()) {
669         return Glib::ustring(); // No verb, no action, no pie.
670     }
671 
672     Glib::ustring action_name;
673     for (auto action : actions) {
674         // Remove just the one shortcut, leaving the others intact.
675         std::vector<Glib::ustring> accels = app->get_accels_for_action(action);
676         auto it = std::find(accels.begin(), accels.end(), shortcut.get_abbrev());
677         if (it != accels.end()) {
678             action_name = action;
679             accels.erase(it);
680         }
681         app->set_accels_for_action(action, accels);
682     }
683 
684     return action_name;
685 }
686 
687 
688 // Remove a shortcut via verb/action name.
689 bool
remove_shortcut(Glib::ustring name)690 Shortcuts::remove_shortcut(Glib::ustring name)
691 {
692     // Try verb first
693     Verb* verb = Verb::getbyid(name.c_str(), false); // Not verbose!
694     if (verb) {
695         Gtk::AccelKey shortcut = get_shortcut_from_verb(verb);
696         shortcut_to_verb_map.erase(shortcut);
697         primary.erase(verb);
698         user_set.erase(shortcut);
699         return true;
700     }
701 
702     // Try action second
703     for (auto action : list_all_detailed_action_names()) {
704         if (action == name) {
705             // Action exists
706             app->unset_accels_for_action(action);
707             action_user_set.erase(action);
708             return true;
709         }
710     }
711 
712     return false;
713 }
714 
715 // Remove a user shortcut, updating user's shortcut file.
716 bool
remove_user_shortcut(Glib::ustring name)717 Shortcuts::remove_user_shortcut(Glib::ustring name)
718 {
719     // Check if really user shortcut.
720     bool user_shortcut = false;
721     Verb *verb = Verb::getbyid(name.c_str(), false); // Not verbose
722     if (verb) {
723         auto primary_shortcut = get_shortcut_from_verb(verb);
724         user_shortcut = (!primary_shortcut.is_null()) && is_user_set(primary_shortcut);
725     } else {
726         user_shortcut = is_user_set(name);
727     }
728 
729     if (!user_shortcut) {
730         // We don't allow removing non-user shortcuts.
731         return false;
732     }
733 
734     if (remove_shortcut(name)) {
735         // Save
736         write_user();
737 
738         // Reread to get original shortcut (if any).
739         init();
740         return true;
741     }
742 
743     std::cerr << "Shortcuts::remove_user_shortcut: Failed to remove shortcut for: " << name << std::endl;
744     return false;
745 }
746 
747 
748 // Remove all user's shortcuts (simply overwrites existing file).
749 bool
clear_user_shortcuts()750 Shortcuts::clear_user_shortcuts()
751 {
752     // Create new empty document and save
753     auto *document = new XML::SimpleDocument();
754     XML::Node * node = document->createElement("keys");
755     node->setAttribute("name", "User Shortcuts");
756     document->appendChild(node);
757     Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
758     sp_repr_save_file(document, file->get_path().c_str(), nullptr);
759     GC::release(document);
760 
761     // Re-read everything!
762     init();
763     return true;
764 }
765 
766 Glib::ustring
get_label(const Gtk::AccelKey & shortcut)767 Shortcuts::get_label(const Gtk::AccelKey& shortcut)
768 {
769     Glib::ustring label;
770 
771     if (!shortcut.is_null()) {
772         // ::get_label shows key pad and numeric keys identically.
773         // TODO: Results in labels like "Numpad Alt+5"
774         if (shortcut.get_abbrev().find("KP") != Glib::ustring::npos) {
775             label += _("Numpad");
776             label += " ";
777         }
778 
779         label += Gtk::AccelGroup::get_label(shortcut.get_key(), shortcut.get_mod());
780     }
781 
782     return label;
783 }
784 
785 Glib::ustring
get_modifiers_verb(unsigned int mod_val)786 Shortcuts::get_modifiers_verb(unsigned int mod_val)
787 {
788     Glib::ustring modifiers;
789     if (mod_val & Gdk::CONTROL_MASK) modifiers += "Ctrl,";
790     if (mod_val & Gdk::SHIFT_MASK)   modifiers += "Shift,";
791     if (mod_val & Gdk::MOD1_MASK)    modifiers += "Alt,";
792     if (mod_val & Gdk::SUPER_MASK)   modifiers += "Super,";
793     if (mod_val & Gdk::HYPER_MASK)   modifiers += "Hyper,";
794     if (mod_val & Gdk::META_MASK)    modifiers += "Meta,";
795 
796     if (modifiers.length() > 0) {
797         modifiers.resize(modifiers.size() -1);
798     }
799 
800     return modifiers;
801 }
802 
803 Glib::ustring
shortcut_to_accelerator(const Gtk::AccelKey & shortcut)804 Shortcuts::shortcut_to_accelerator(const Gtk::AccelKey& shortcut)
805 {
806     unsigned int keyval = shortcut.get_key();
807     unsigned int modval = shortcut.get_mod();
808 
809     Glib::ustring accelerator;
810     if (modval & Gdk::CONTROL_MASK) accelerator += "<Ctrl>";
811     if (modval & Gdk::SHIFT_MASK)   accelerator += "<Shift>";
812     if (modval & Gdk::MOD1_MASK)    accelerator += "<Alt>";
813     if (modval & Gdk::SUPER_MASK)   accelerator += "<Super>";
814     if (modval & Gdk::HYPER_MASK)   accelerator += "<Hyper>";
815     if (modval & Gdk::META_MASK)    accelerator += "<Meta>";
816 
817     gchar* key = gdk_keyval_name(keyval);
818     if (key) {
819         accelerator += key;
820     }
821 
822     // Glib::ustring accelerator2 = Gtk::AccelGroup::name(keyval, Gdk::ModifierType(modval));
823     // Glib::ustring accelerator3 = Gtk::AccelGroup::get_label(keyval, Gdk::ModifierType(modval));
824 
825     // std::cout << "accelerator: " << accelerator << " " << accelerator2 << " " << accelerator3 << std::endl;
826     return accelerator;
827 }
828 
829 Gtk::AccelKey
accelerator_to_shortcut(const Glib::ustring & accelerator)830 Shortcuts::accelerator_to_shortcut(const Glib::ustring& accelerator)
831 {
832     Gdk::ModifierType modval = Gdk::ModifierType(0);
833     std::vector<Glib::ustring> parts = Glib::Regex::split_simple("<(<.*?>)", accelerator);
834     for (auto part : parts) {
835         if (part == "<Ctrl>")  modval |= Gdk::CONTROL_MASK;
836         if (part == "<Shift>") modval |= Gdk::SHIFT_MASK;
837         if (part == "<Alt>")   modval |= Gdk::MOD1_MASK;
838         if (part == "<Super>") modval |= Gdk::SUPER_MASK;
839         if (part == "<Hyper>") modval |= Gdk::HYPER_MASK;
840         if (part == "<Meta>")  modval |= Gdk::META_MASK;
841         if (part == "<Primary>") std::cerr << "Shortcuts::accelerator_to_shortcut: need to handle 'Primary'!" << std::endl;
842     }
843 
844     unsigned int keyval = gdk_keyval_from_name(parts[parts.size()-1].c_str());
845 
846     return Gtk::AccelKey(keyval, modval);
847 }
848 
849 /*
850  * Return: keyval translated to group 0 in lower 32 bits, modifier encoded in upper 32 bits.
851  *
852  * Usuage of group 0 (i.e. the main, typically English layout) instead of simply event->keyval
853  * ensures that shortcuts work regardless of the active keyboard layout (e.g. Cyrillic).
854  *
855  * The returned modifiers are the modifiers that were not "consumed" by the translation and
856  * can be used by the application to define a shortcut, e.g.
857  *  - when pressing "Shift+9" the resulting character is "(";
858  *    the shift key was "consumed" to make this character and should not be part of the shortcut
859  *  - when pressing "Ctrl+9" the resulting character is "9";
860  *    the ctrl key was *not* consumed to make this character and must be included in the shortcut
861  *  - Exception: letter keys like [A-Z] always need the shift modifier,
862  *               otherwise lower case and uper case keys are treated as equivalent.
863  */
864 Gtk::AccelKey
get_from_event(GdkEventKey const * event,bool fix)865 Shortcuts::get_from_event(GdkEventKey const *event, bool fix)
866 {
867     // MOD2 corresponds to the NumLock key. Masking it out allows
868     // shortcuts to work regardless of its state.
869     Gdk::ModifierType initial_modifiers  = Gdk::ModifierType(event->state & ~Gdk::MOD2_MASK);
870     unsigned int consumed_modifiers = 0;
871     //Gdk::ModifierType consumed_modifiers = Gdk::ModifierType(0);
872 
873     unsigned int keyval = Inkscape::UI::Tools::get_latin_keyval(event, &consumed_modifiers);
874 
875     // If a key value is "convertible", i.e. it has different lower case and upper case versions,
876     // convert to lower case and don't consume the "shift" modifier.
877     bool is_case_convertible = !(gdk_keyval_is_upper(keyval) && gdk_keyval_is_lower(keyval));
878     if (is_case_convertible) {
879         keyval = gdk_keyval_to_lower(keyval);
880         consumed_modifiers &= ~ Gdk::SHIFT_MASK;
881     }
882 
883     // The InkscapePreferences dialog returns an event structure where the Shift modifier is not
884     // set for keys like '('. This causes '(' to be converted to '9' by get_latin_keyval. It also
885     // returns 'Shift-k' for 'K' (instead of 'Shift-K') but this is not a problem.
886     // We fix this by restoring keyval to its original value.
887     if (fix) {
888         keyval = event->keyval;
889     }
890 
891     auto unused_modifiers = Gdk::ModifierType((initial_modifiers &~ consumed_modifiers)
892                                                                  & GDK_MODIFIER_MASK);
893 
894     // std::cout << "Shortcuts::get_from_event: End:   "
895     //           << " Key: " << std::hex << keyval << " (" << (char)keyval << ")"
896     //           << " Mod: " << std::hex << unused_modifiers << std::endl;
897     return (Gtk::AccelKey(keyval, unused_modifiers));
898 }
899 
900 
901 // Add an accelerator to the group.
902 void
add_accelerator(Gtk::Widget * widget,Verb * verb)903 Shortcuts::add_accelerator (Gtk::Widget *widget, Verb *verb)
904 {
905     Gtk::AccelKey shortcut = get_shortcut_from_verb(verb);
906 
907     if (shortcut.is_null()) {
908         return;
909     }
910 
911     static Glib::RefPtr<Gtk::AccelGroup> accel_group = Gtk::AccelGroup::create();
912 
913     widget->add_accelerator ("activate", accel_group, shortcut.get_key(), shortcut.get_mod(), Gtk::ACCEL_VISIBLE);
914 }
915 
916 
917 // Get a list of filenames to populate menu
918 std::vector<std::pair<Glib::ustring, Glib::ustring>>
get_file_names()919 Shortcuts::get_file_names()
920 {
921     // TODO  Filenames should be std::string but that means changing the whole stack.
922     using namespace Inkscape::IO::Resource;
923 
924     // Make a list of all key files from System and User.  Glib::ustring should be std::string!
925     std::vector<Glib::ustring> filenames = get_filenames(SYSTEM, KEYS, {".xml"});
926     // Exclude default.xml as it only contains user modifications.
927     std::vector<Glib::ustring> filenames_user = get_filenames(USER, KEYS, {".xml"}, {"default.xml"});
928     filenames.insert(filenames.end(), filenames_user.begin(), filenames_user.end());
929 
930     // Check file exists and extract out label if it does.
931     std::vector<std::pair<Glib::ustring, Glib::ustring>> names_and_paths;
932     for (auto &filename : filenames) {
933         std::string label = Glib::path_get_basename(filename);
934         Glib::ustring filename_relative = sp_relative_path_from_path(filename, std::string(get_path(SYSTEM, KEYS)));
935 
936         XML::Document *document = sp_repr_read_file(filename.c_str(), nullptr);
937         if (!document) {
938             std::cerr << "Shortcut::get_file_names: could not parse file: " << filename << std::endl;
939             continue;
940         }
941 
942         XML::NodeConstSiblingIterator iter = document->firstChild();
943         for ( ; iter ; ++iter ) { // We iterate in case of comments.
944             if (strcmp(iter->name(), "keys") == 0) {
945                 gchar const *name = iter->attribute("name");
946                 if (name) {
947                     label = Glib::ustring(name) + " (" + label + ")";
948                 }
949                 std::pair<Glib::ustring, Glib::ustring> name_and_path = std::make_pair(label, filename_relative);
950                 names_and_paths.emplace_back(name_and_path);
951                 break;
952             }
953         }
954         if (!iter) {
955             std::cerr << "Shortcuts::get_File_names: not a shortcut keys file: " << filename << std::endl;
956         }
957 
958         Inkscape::GC::release(document);
959     }
960 
961     // Sort by name
962     std::sort(names_and_paths.begin(), names_and_paths.end(),
963             [](std::pair<Glib::ustring, Glib::ustring> pair1, std::pair<Glib::ustring, Glib::ustring> pair2) {
964                 return Glib::path_get_basename(pair1.first).compare(Glib::path_get_basename(pair2.first)) < 0;
965             });
966 
967     // But default.xml at top
968     auto it_default = std::find_if(names_and_paths.begin(), names_and_paths.end(),
969             [](std::pair<Glib::ustring, Glib::ustring>& pair) {
970                 return !Glib::path_get_basename(pair.second).compare("default.xml");
971             });
972     if (it_default != names_and_paths.end()) {
973         std::rotate(names_and_paths.begin(), it_default, it_default+1);
974     }
975 
976     return names_and_paths;
977 }
978 
979 // void on_foreach(Gtk::Widget& widget) {
980 //     std::cout <<  "on_foreach: " << widget.get_name() << std::endl;;
981 // }
982 
983 /*
984  * Update text with shortcuts.
985  * Inkscape includes shortcuts in tooltips and in dialog titles. They need to be updated
986  * anytime a tooltip is changed.
987  */
988 void
update_gui_text_recursive(Gtk::Widget * widget)989 Shortcuts::update_gui_text_recursive(Gtk::Widget* widget)
990 {
991 
992     // NOT what we want
993     // auto activatable = dynamic_cast<Gtk::Activatable *>(widget);
994 
995     // We must do this until GTK4
996     GtkWidget* gwidget = widget->gobj();
997     bool is_actionable = GTK_IS_ACTIONABLE(gwidget);
998 
999     if (is_actionable) {
1000         const gchar* gaction = gtk_actionable_get_action_name(GTK_ACTIONABLE(gwidget));
1001         if (gaction) {
1002 
1003             Glib::ustring action = gaction;
1004             std::vector<Glib::ustring> accels = app->get_accels_for_action(action);
1005 
1006             Glib::ustring tooltip;
1007             auto *iapp = InkscapeApplication::instance();
1008             if (iapp) {
1009                 tooltip = iapp->get_action_extra_data().get_tooltip_for_action(action);
1010             }
1011 
1012             // Add new primary accelerator.
1013             if (accels.size() > 0) {
1014 
1015                 // Add space between tooltip and accel if there is a tooltip
1016                 if (!tooltip.empty()) {
1017                     tooltip += " ";
1018                 }
1019 
1020                 // Convert to more user friendly notation.
1021                 unsigned int key = 0;
1022                 Gdk::ModifierType mod = Gdk::ModifierType(0);
1023                 Gtk::AccelGroup::parse(accels[0], key, mod);
1024                 tooltip += "(" + Gtk::AccelGroup::get_label(key, mod) + ")";
1025             }
1026 
1027             // Update tooltip.
1028             widget->set_tooltip_text(tooltip);
1029         }
1030     }
1031 
1032     auto container = dynamic_cast<Gtk::Container *>(widget);
1033     if (container) {
1034         auto children = container->get_children();
1035         for (auto child : children) {
1036             update_gui_text_recursive(child);
1037         }
1038     }
1039 
1040 }
1041 
1042 // Dialogs
1043 
1044 // Import user shortcuts from a file.
1045 bool
import_shortcuts()1046 Shortcuts::import_shortcuts() {
1047 
1048     // Users key directory.
1049     Glib::ustring directory = get_path_string(USER, KEYS, "");
1050 
1051     // Create and show the dialog
1052     Gtk::Window* window = app->get_active_window();
1053     Inkscape::UI::Dialog::FileOpenDialog *importFileDialog =
1054         Inkscape::UI::Dialog::FileOpenDialog::create(*window, directory, Inkscape::UI::Dialog::CUSTOM_TYPE, _("Select a file to import"));
1055     importFileDialog->addFilterMenu(_("Inkscape shortcuts (*.xml)"), "*.xml");
1056     bool const success = importFileDialog->show();
1057 
1058     if (!success) {
1059         delete importFileDialog;
1060         return false;
1061     }
1062 
1063     // Get file name and read.
1064     Glib::ustring path = importFileDialog->getFilename(); // It's a full path, not just a filename!
1065     delete importFileDialog;
1066 
1067     Glib::RefPtr<Gio::File> file_read = Gio::File::create_for_path(path);
1068     if (!read(file_read, true)) {
1069         std::cerr << "Shortcuts::import_shortcuts: Failed to read file!" << std::endl;
1070         return false;
1071     }
1072 
1073     // Save
1074     return write_user();
1075 };
1076 
1077 bool
export_shortcuts()1078 Shortcuts::export_shortcuts() {
1079 
1080     // Users key directory.
1081     Glib::ustring directory = get_path_string(USER, KEYS, "");
1082 
1083     // Create and show the dialog
1084     Gtk::Window* window = app->get_active_window();
1085     Inkscape::UI::Dialog::FileSaveDialog *saveFileDialog =
1086         Inkscape::UI::Dialog::FileSaveDialog::create(*window, directory, Inkscape::UI::Dialog::CUSTOM_TYPE, _("Select a filename for export"),
1087                                                      "", "", Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS);
1088     saveFileDialog->addFileType(_("Inkscape shortcuts (*.xml)"), "*.xml");
1089     bool success = saveFileDialog->show();
1090 
1091     // Get file name and write.
1092     if (success) {
1093         Glib::ustring path = saveFileDialog->getFilename(); // It's a full path, not just a filename!
1094         if (path.size() > 0) {
1095             Glib::ustring newFileName = Glib::filename_to_utf8(path);  // Is this really correct? (Paths should be std::string.)
1096             Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(path);
1097             success = write(file, User);
1098         } else {
1099             // Can this ever happen?
1100             success = false;
1101         }
1102     }
1103 
1104     delete saveFileDialog;
1105 
1106     return success;
1107 };
1108 
1109 
1110 // For debugging.
1111 void
dump()1112 Shortcuts::dump() {
1113 
1114     // What shortcuts are being used?
1115     std::vector<Gdk::ModifierType> modifiers {
1116         Gdk::ModifierType(0),
1117         Gdk::SHIFT_MASK,
1118         Gdk::CONTROL_MASK,
1119         Gdk::MOD1_MASK,
1120         Gdk::SHIFT_MASK   |  Gdk::CONTROL_MASK,
1121         Gdk::SHIFT_MASK   |  Gdk::MOD1_MASK,
1122         Gdk::CONTROL_MASK |  Gdk::MOD1_MASK,
1123         Gdk::SHIFT_MASK   |  Gdk::CONTROL_MASK   | Gdk::MOD1_MASK
1124     };
1125     for (auto mod : modifiers) {
1126         for (gchar key = '!'; key <= '~'; ++key) {
1127 
1128             Glib::ustring action;
1129             Glib::ustring accel = Gtk::AccelGroup::name(key, mod);
1130             std::vector<Glib::ustring> actions = app->get_actions_for_accel(accel);
1131             if (!actions.empty()) {
1132                 action = actions[0];
1133             }
1134 
1135             Gtk::AccelKey shortcut(key, mod);
1136             Inkscape::Verb *verb = get_verb_from_shortcut(shortcut);
1137             if (verb) {
1138                 action = verb->get_name();
1139             }
1140 
1141             std::cout << "  shortcut:"
1142                       << "  " << std::setw(8) << std::hex << shortcut.get_mod()
1143                       << "  " << std::setw(8) << std::hex << shortcut.get_key()
1144                       << "  " << std::setw(30) << std::left << accel
1145                       << "  " << action
1146                       << std::endl;
1147         }
1148     }
1149 }
1150 
1151 void
dump_all_recursive(Gtk::Widget * widget)1152 Shortcuts::dump_all_recursive(Gtk::Widget* widget)
1153 {
1154     static unsigned int indent = 0;
1155     ++indent;
1156     for (int i = 0; i < indent; ++i) std::cout << "  ";
1157 
1158     // NOT what we want
1159     // auto activatable = dynamic_cast<Gtk::Activatable *>(widget);
1160 
1161     // We must do this until GTK4
1162     GtkWidget* gwidget = widget->gobj();
1163     bool is_actionable = GTK_IS_ACTIONABLE(gwidget);
1164     Glib::ustring action;
1165     if (is_actionable) {
1166         const gchar* gaction = gtk_actionable_get_action_name( GTK_ACTIONABLE(gwidget) );
1167         if (gaction) {
1168             action = gaction;
1169         }
1170     }
1171 
1172     std::cout << widget->get_name()
1173               << ":   actionable: " << std::boolalpha << is_actionable
1174               << ":   " << widget->get_tooltip_text()
1175               << ":   " << action
1176               << std::endl;
1177     auto container = dynamic_cast<Gtk::Container *>(widget);
1178     if (container) {
1179         auto children = container->get_children();
1180         for (auto child : children) {
1181             dump_all_recursive(child);
1182         }
1183     }
1184     --indent;
1185 }
1186 
1187 
1188 } // Namespace
1189 
1190 /*
1191   Local Variables:
1192   mode:c++
1193   c-file-style:"stroustrup"
1194   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1195   indent-tabs-mode:nil
1196   fill-column:99
1197   End:
1198 */
1199 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
1200