1 // SPDX-License-Identifier: GPL-2.0-or-later
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"
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>
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"
39 #include "ui/modifiers.h"
40 #include "ui/tools/tool-base.h" // For latin keyval
41 #include "ui/dialog/filedialog.h" // Importing/exporting files.
43 #include "ui/widget/events/canvas-event.h"
44 #include "xml/simple-document.h"
46 #include "xml/node-iterators.h"
48 using namespace Inkscape::IO::Resource
;
49 using namespace Inkscape::Modifiers
;
53 Shortcuts::Shortcuts()
54 : app
{dynamic_cast<Gtk::Application
*>(Gio::Application::get_default().get())}
57 std::cerr
<< "Shortcuts::Shortcuts: No app! Shortcuts cannot be used without a Gtk::Application!" << std::endl
;
61 // Shared among all Shortcut controllers.
62 _liststore
= Gio::ListStore
<Gtk::Shortcut
>::create();
65 Shortcuts::~Shortcuts() {}
71 // Clear arrays (we may be re-reading).
74 bool success
= false; // We've read a shortcut file!
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");
85 if (!Glib::path_is_absolute(path
)) {
86 path
= get_path_string(SYSTEM
, KEYS
, path
.c_str());
90 Glib::RefPtr
<Gio::File
> file
= Gio::File::create_for_path(path
);
91 success
= _read(file
);
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());
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
);
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
);
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()) {
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()) {
136 // Emit changed signal in case of read-reading (user selects different file).
143 // ****** User Shortcuts ******
145 // Add a user shortcut, updating user's shortcut file if successful.
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.
153 detailed_action_name
,
154 trigger
.get_abbrev(),
155 true /* user shortcut */,
156 false /* do not cache action-names */
164 std::cerr
<< "Shortcut::add_user_shortcut: Failed to add: " << detailed_action_name
.raw()
165 << " with shortcut " << trigger
.get_abbrev().raw() << std::endl
;
169 // Remove a user shortcut, updating user's shortcut file.
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.
181 if (_remove_shortcuts(detailed_action_name
)) {
185 // Reread to get original shortcut (if any). And emit changes signal.
191 std::cerr
<< "Shortcuts::remove_user_shortcut: Failed to remove shortcut for: "
192 << detailed_action_name
.raw() << std::endl
;
198 * Remove all user's shortcuts (simply overwrites existing file).
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.
218 * Return if user set shortcut for Gio::Action.
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
);
234 * Write user shortcuts to file.
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.
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();
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
+ ")";
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();
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()) {
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
) + ")";
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 */
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
);
318 auto window
= dynamic_cast<InkscapeWindow
*>(app
->get_active_window());
320 window
->activate_action(action
, value
); // Not action_name in Gtk4!
328 /** Trigger action from a shortcut. Useful if we want to intercept the event from GTK */
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
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
);
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
);
380 Shortcuts::get_label(const Gtk::AccelKey
& shortcut
)
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");
392 label
+= Gtk::Accelerator::get_label(shortcut
.get_key(), shortcut
.get_mod());
399 get_from_event_impl(unsigned const event_keyval
, unsigned const event_keycode
,
400 GdkModifierType
const event_state
, unsigned const event_group
,
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.
425 keyval
= event_keyval
;
428 auto const unused_modifiers
= Gdk::ModifierType(static_cast<unsigned>(initial_modifiers
)
429 & ~consumed_modifiers
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.
455 Shortcuts::get_from(GtkEventControllerKey
const * const controller
,
456 unsigned const keyval
, unsigned const keycode
, GdkModifierType
const state
,
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
);
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
);
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
);
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();
513 auto map
= document
->getActionGroup();
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
));
521 std::cerr
<< "Shortcuts::list_all_actions: No document map!" << std::endl
;
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);
557 std::cerr
<< "Shortcut::get_file_names: could not parse file: " << filename
<< std::endl
;
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");
566 label
= Glib::ustring::compose("%1 (%2)", name
, label
);
568 names_and_paths
.emplace_back(std::move(label
), std::move(filename_relative
));
573 std::cerr
<< "Shortcuts::get_File_names: not a shortcut keys file: " << filename
<< std::endl
;
576 Inkscape::GC::release(document
);
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
;
598 // Import user shortcuts from a file.
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();
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();
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
;
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();
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.
652 auto file
= saveFileDialog
->getFile();
653 success
= _write(file
, User
);
655 std::cerr
<< "Shortcuts::export_shortcuts: Failed to save file!" << std::endl
;
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
;
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") {
706 modifiers
|= Gdk::ModifierType::META_MASK
;
708 modifiers
|= Gdk::ModifierType::CONTROL_MASK
;
711 std::cerr
<< "Shortcut::read: Unknown GDK modifier: " << mod
.c_str() << std::endl
;
718 // ******* Files *******
721 * Read a shortcut file.
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
;
731 XML::Document
*document
= sp_repr_read_file(file
->get_path().c_str(), nullptr, true);
733 std::cerr
<< "Shortcut::read: could not parse file: " << file
->get_path() << std::endl
;
737 XML::NodeConstSiblingIterator iter
= document
->firstChild();
738 for ( ; iter
; ++iter
) { // We iterate in case of comments.
739 if (strcmp(iter
->name(), "keys") == 0) {
745 std::cerr
<< "Shortcuts::read: File in wrong format: " << file
->get_path() << std::endl
;
749 // Loop through the children in <keys> (may have nested keys)
750 _read(*iter
, user_set
);
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
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");
770 std::cerr
<< "Shortcuts::read: Missing modifier for action!" << std::endl
;
774 Modifier
*mod
= Modifier::get(mod_name
);
775 if (mod
== nullptr) {
776 std::cerr
<< "Shortcuts::read: Can't find modifier: " << mod_name
<< std::endl
;
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");
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");
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
) {
802 mod
->set_user(and_modifier
, not_modifier
);
804 mod
->set_keys(and_modifier
, not_modifier
);
808 } else if (strcmp(iter
->name(), "keys") == 0) {
809 _read(*iter
, user_set
);
811 } else if (strcmp(iter
->name(), "bind") != 0) {
812 // Unknown element, do not complain.
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 << ", ";
846 // std::cout << std::endl;
854 // In principle, we only write User shortcuts. But for debugging, we might want to write something else.
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");
862 node
->setAttribute("name", "User Shortcuts");
865 node
->setAttribute("name", "System Shortcuts");
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");
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
);
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();
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>");
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;
962 // Remove previous use of trigger.
963 [[maybe_unused
]] auto const removed
= _remove_shortcut_trigger(trigger_normalized
);
965 // std::cerr << "Shortcut::add_shortcut: duplicate shortcut found for: " << trigger_normalized
966 // << " New: " << detailed_action_name.raw() << " !" << std::endl;
969 // A user shortcut replaces all others.
971 _remove_shortcuts(detailed_action_name
);
974 auto const trigger
= Gtk::ShortcutTrigger::parse_string(trigger_normalized
);
977 auto const action
= Gtk::NamedAction::create(action_name
);
980 auto shortcut
= Gtk::Shortcut::create(trigger
, action
);
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
));
995 * Remove shortcuts via AccelKey.
996 * Returns true of shortcut(s) removed, false if nothing removed.
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
);
1013 it
= _shortcuts
.erase(it
);
1028 * Remove all shortcuts for a detailed action. There can be multiple.
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
;
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
);
1048 it
= _shortcuts
.erase(it
);
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
)
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.
1091 _liststore
->remove_all();
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
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();
1138 std::cout
<< "(" << variant
.print() << ")";
1140 std::cout
<< ": " << trigger
->to_string() << std::endl
;
1145 Shortcuts::_dump_all_recursive(Gtk::Widget
* widget
)
1147 static unsigned int indent
= 0;
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()
1160 for (auto const child
: UI::get_children(*widget
)) {
1161 _dump_all_recursive(child
);
1167 } // namespace Inkscape
1172 c-file-style:"stroustrup"
1173 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1174 indent-tabs-mode:nil
1178 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :