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