summaryrefslogtreecommitdiffstats
path: root/src/ui/shortcuts.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/shortcuts.cpp')
-rw-r--r--src/ui/shortcuts.cpp1024
1 files changed, 1024 insertions, 0 deletions
diff --git a/src/ui/shortcuts.cpp b/src/ui/shortcuts.cpp
new file mode 100644
index 0000000..f7d4c60
--- /dev/null
+++ b/src/ui/shortcuts.cpp
@@ -0,0 +1,1024 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Shortcuts
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ * Rewrite of code (C) MenTalguY and others.
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+
+/* Much of the complexity of this code is in dealing with both Inkscape verbs and Gio::Actions at
+ * the same time. When we remove verbs we can avoid using 'unsigned long long int shortcut' to
+ * track keys and rely directly on Glib::ustring as used by
+ * Gtk::Application::get_accels_for_action(). This will then automatically handle the '<Primary>'
+ * modifier value (which takes care of the differences between Linux and OSX) as well as allowing
+ * us to set multiple accelerators for actions in InkscapePreferences. */
+
+#include "shortcuts.h"
+
+#include <iostream>
+#include <iomanip>
+
+#include <glibmm.h>
+#include <glibmm/i18n.h>
+#include <gtkmm.h>
+
+#include "preferences.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+
+#include "io/dir-util.h"
+#include "io/resource.h"
+#include "io/sys.h"
+
+#include "ui/modifiers.h"
+#include "ui/tools/tool-base.h" // For latin keyval
+#include "ui/dialog/filedialog.h" // Importing/exporting files.
+
+#include "xml/simple-document.h"
+#include "xml/node.h"
+#include "xml/node-iterators.h"
+
+using namespace Inkscape::IO::Resource;
+using namespace Inkscape::Modifiers;
+
+namespace Inkscape {
+
+Shortcuts::Shortcuts()
+{
+ Glib::RefPtr<Gio::Application> gapp = Gio::Application::get_default();
+ app = Glib::RefPtr<Gtk::Application>::cast_dynamic(gapp); // Save as we constantly use it.
+ if (!app) {
+ std::cerr << "Shortcuts::Shortcuts: No app! Shortcuts cannot be used without a Gtk::Application!" << std::endl;
+ }
+}
+
+
+void
+Shortcuts::init() {
+
+ initialized = true;
+
+ // Clear arrays (we may be re-reading).
+ clear();
+
+ bool success = false; // We've read a shortcut file!
+ std::string path;
+
+ // ------------ Open Inkscape shortcut file ------------
+
+ // Try filename from preferences first.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ path = prefs->getString("/options/kbshortcuts/shortcutfile");
+ if (!path.empty()) {
+ bool absolute = true;
+ if (!Glib::path_is_absolute(path)) {
+ path = get_path_string(SYSTEM, KEYS, path.c_str());
+ absolute = false;
+ }
+
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(path);
+ success = read(file);
+ if (!success) {
+ std::cerr << "Shortcut::Shortcut: Unable to read shortcut file listed in preferences: " + path << std::endl;;
+ }
+
+ // Save relative path to "share/keys" if possible to handle parallel installations of
+ // Inskcape gracefully.
+ if (success && absolute) {
+ std::string relative_path = sp_relative_path_from_path(path, std::string(get_path(SYSTEM, KEYS)));
+ prefs->setString("/options/kbshortcuts/shortcutfile", relative_path.c_str());
+ }
+ }
+
+ if (!success) {
+ // std::cerr << "Shortcut::Shortcut: " << reason << ", trying default.xml" << std::endl;
+
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SYSTEM, KEYS, "default.xml"));
+ success = read(file);
+ }
+
+ if (!success) {
+ std::cerr << "Shortcut::Shortcut: Failed to read file default.xml, trying inkscape.xml" << std::endl;
+
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SYSTEM, KEYS, "inkscape.xml"));
+ success = read(file);
+ }
+
+ if (!success) {
+ std::cerr << "Shortcut::Shortcut: Failed to read file inkscape.xml; giving up!" << std::endl;
+ }
+
+ // ------------ Open Shared shortcut file -------------
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(SHARED, KEYS, "default.xml"));
+ // Test if file exists before attempting to read to avoid generating warning message.
+ if (file->query_exists()) {
+ read(file, true);
+ }
+ // ------------ Open User shortcut file -------------
+ file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
+ // Test if file exists before attempting to read to avoid generating warning message.
+ if (file->query_exists()) {
+ read(file, true);
+ }
+
+ // dump();
+}
+
+// Clear all shortcuts
+void
+Shortcuts::clear()
+{
+ // Actions: We rely on Gtk for everything except user/system setting.
+ for (auto action_description : app->list_action_descriptions()) {
+ app->unset_accels_for_action(action_description);
+ }
+ action_user_set.clear();
+}
+
+/** Trigger action from a shortcut. Useful if we want to intercept the event from GTK */
+bool
+Shortcuts::invoke_action(GdkEventKey const *event)
+{
+ Gtk::AccelKey shortcut = get_from_event(event);
+
+ bool return_value = false;
+
+ // This can be simplified in GTK4.
+ Glib::ustring accel = Gtk::AccelGroup::name(shortcut.get_key(), shortcut.get_mod());
+ std::vector<Glib::ustring> actions = app->get_actions_for_accel(accel);
+ if (!actions.empty()) {
+ Glib::ustring action = actions[0];
+ Glib::ustring action_name;
+ Glib::VariantBase value;
+ Gio::SimpleAction::parse_detailed_name_variant(action.substr(4), action_name, value);
+ if (action.compare(0, 4, "app.") == 0) {
+ app->activate_action(action_name, value);
+ return_value = true;
+ } else if (action.compare(0, 4, "win.") == 0) {
+ auto window = dynamic_cast<InkscapeWindow *>(app->get_active_window());
+ if (window) {
+ window->activate_action(action_name, value);
+ return_value = true;
+ }
+ }
+ }
+ return return_value;
+}
+
+Gdk::ModifierType
+parse_modifier_string(gchar const *modifiers_string)
+{
+ Gdk::ModifierType modifiers(Gdk::ModifierType(0));
+ if (modifiers_string) {
+
+ Glib::ustring str(modifiers_string);
+ std::vector<Glib::ustring> mod_vector = Glib::Regex::split_simple("\\s*,\\s*", modifiers_string);
+
+ for (auto mod : mod_vector) {
+ if (mod == "Control" || mod == "Ctrl") {
+ modifiers |= Gdk::CONTROL_MASK;
+ } else if (mod == "Shift") {
+ modifiers |= Gdk::SHIFT_MASK;
+ } else if (mod == "Alt") {
+ modifiers |= Gdk::MOD1_MASK;
+ } else if (mod == "Super") {
+ modifiers |= Gdk::SUPER_MASK; // Not used
+ } else if (mod == "Hyper") {
+ modifiers |= Gdk::HYPER_MASK; // Not used
+ } else if (mod == "Meta") {
+ modifiers |= Gdk::META_MASK;
+ } else if (mod == "Primary") {
+
+ // System dependent key to invoke menus. (Needed for OSX in particular.)
+ // We only read "Primary" and never write it.
+ auto display = Gdk::Display::get_default();
+ if (display) {
+ GdkKeymap* keymap = display->get_keymap();
+ GdkModifierType type =
+ gdk_keymap_get_modifier_mask (keymap, GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
+ gdk_keymap_add_virtual_modifiers(keymap, &type);
+ if (type & Gdk::CONTROL_MASK)
+ modifiers |= Gdk::CONTROL_MASK;
+ else if (type & Gdk::META_MASK)
+ modifiers |= Gdk::META_MASK;
+ else {
+ std::cerr << "Shortcut::read: Unknown primary accelerator!" << std::endl;
+ modifiers |= Gdk::CONTROL_MASK;
+ }
+ } else {
+ modifiers |= Gdk::CONTROL_MASK;
+ }
+ } else {
+ std::cerr << "Shortcut::read: Unknown GDK modifier: " << mod.c_str() << std::endl;
+ }
+ }
+ }
+ return modifiers;
+}
+
+
+// Read a shortcut file.
+bool
+Shortcuts::read(Glib::RefPtr<Gio::File> file, bool user_set)
+{
+ if (!file->query_exists()) {
+ std::cerr << "Shortcut::read: file does not exist: " << file->get_path() << std::endl;
+ return false;
+ }
+
+ XML::Document *document = sp_repr_read_file(file->get_path().c_str(), nullptr, true);
+ if (!document) {
+ std::cerr << "Shortcut::read: could not parse file: " << file->get_path() << std::endl;
+ return false;
+ }
+
+ XML::NodeConstSiblingIterator iter = document->firstChild();
+ for ( ; iter ; ++iter ) { // We iterate in case of comments.
+ if (strcmp(iter->name(), "keys") == 0) {
+ break;
+ }
+ }
+
+ if (!iter) {
+ std::cerr << "Shortcuts::read: File in wrong format: " << file->get_path() << std::endl;
+ return false;
+ }
+
+ // Loop through the children in <keys> (may have nested keys)
+ _read(*iter, user_set);
+
+ return true;
+}
+
+/**
+ * Recursively reads shortcuts from shortcut file.
+ *
+ * @param keysnode The <keys> element. Its child nodes will be processed.
+ * @param user_set true if reading from user shortcut file
+ */
+void
+Shortcuts::_read(XML::Node const &keysnode, bool user_set)
+{
+ XML::NodeConstSiblingIterator iter {keysnode.firstChild()};
+ for ( ; iter ; ++iter ) {
+
+ if (strcmp(iter->name(), "modifier") == 0) {
+
+ gchar const *mod_name = iter->attribute("action");
+ if (!mod_name) {
+ std::cerr << "Shortcuts::read: Missing modifier for action!" << std::endl;;
+ continue;
+ }
+
+ Modifier *mod = Modifier::get(mod_name);
+ if (mod == nullptr) {
+ std::cerr << "Shortcuts::read: Can't find modifier: " << mod_name << std::endl;
+ continue;
+ }
+
+ // If mods isn't specified then it should use default, if it's an empty string
+ // then the modifier is None (i.e. happens all the time without a modifier)
+ KeyMask and_modifier = NOT_SET;
+ gchar const *mod_attr = iter->attribute("modifiers");
+ if (mod_attr) {
+ and_modifier = (KeyMask) parse_modifier_string(mod_attr);
+ }
+
+ // Parse not (cold key) modifier
+ KeyMask not_modifier = NOT_SET;
+ gchar const *not_attr = iter->attribute("not_modifiers");
+ if (not_attr) {
+ not_modifier = (KeyMask) parse_modifier_string(not_attr);
+ }
+
+ gchar const *disabled_attr = iter->attribute("disabled");
+ if (disabled_attr && strcmp(disabled_attr, "true") == 0) {
+ and_modifier = NEVER;
+ }
+
+ if (and_modifier != NOT_SET) {
+ if(user_set) {
+ mod->set_user(and_modifier, not_modifier);
+ } else {
+ mod->set_keys(and_modifier, not_modifier);
+ }
+ }
+ continue;
+ } else if (strcmp(iter->name(), "keys") == 0) {
+ _read(*iter, user_set);
+ continue;
+ } else if (strcmp(iter->name(), "bind") != 0) {
+ // Unknown element, do not complain.
+ continue;
+ }
+
+ // Gio::Action's
+ gchar const *gaction = iter->attribute("gaction");
+ gchar const *keys = iter->attribute("keys");
+ if (gaction && keys) {
+
+ // Trim leading spaces
+ Glib::ustring Keys = keys;
+ auto p = Keys.find_first_not_of(" ");
+ Keys = Keys.erase(0, p);
+
+ std::vector<Glib::ustring> key_vector = Glib::Regex::split_simple("\\s*,\\s*", Keys);
+ // Set one shortcut at a time so we can check if it has been previously used.
+ for (auto key : key_vector) {
+ add_shortcut(gaction, key, user_set);
+ }
+
+ // Uncomment to see what the cat dragged in.
+ // if (!key_vector.empty()) {
+ // std::cout << "Shortcut::read: gaction: "<< gaction
+ // << ", user set: " << std::boolalpha << user_set << ", ";
+ // for (auto key : key_vector) {
+ // std::cout << key << ", ";
+ // }
+ // std::cout << std::endl;
+ // }
+
+ continue;
+ }
+ }
+}
+
+bool
+Shortcuts::write_user() {
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
+ return write(file, User);
+}
+
+// In principle, we only write User shortcuts. But for debugging, we might want to write something else.
+bool
+Shortcuts::write(Glib::RefPtr<Gio::File> file, What what) {
+
+ auto *document = new XML::SimpleDocument();
+ XML::Node * node = document->createElement("keys");
+ switch (what) {
+ case User:
+ node->setAttribute("name", "User Shortcuts");
+ break;
+ case System:
+ node->setAttribute("name", "System Shortcuts");
+ break;
+ default:
+ node->setAttribute("name", "Inkscape Shortcuts");
+ }
+
+ document->appendChild(node);
+
+ // Actions: write out all actions with accelerators.
+ for (auto action_name : list_all_detailed_action_names()) {
+ if ( what == All ||
+ (what == System && !action_user_set[action_name]) ||
+ (what == User && action_user_set[action_name]) )
+ {
+ std::vector<Glib::ustring> accels = app->get_accels_for_action(action_name);
+ if (!accels.empty()) {
+
+ XML::Node * node = document->createElement("bind");
+
+ node->setAttribute("gaction", action_name);
+
+ Glib::ustring keys;
+ for (auto accel : accels) {
+ keys += accel;
+ keys += ",";
+ }
+ keys.resize(keys.size() - 1);
+ node->setAttribute("keys", keys);
+
+ document->root()->appendChild(node);
+ }
+ }
+ }
+
+ for(auto modifier: Inkscape::Modifiers::Modifier::getList()) {
+ if (what == User && modifier->is_set_user()) {
+ XML::Node * node = document->createElement("modifier");
+ node->setAttribute("action", modifier->get_id());
+
+ if (modifier->get_config_user_disabled()) {
+ node->setAttribute("disabled", "true");
+ } else {
+ node->setAttribute("modifiers", modifier->get_config_user_and());
+ auto not_mask = modifier->get_config_user_not();
+ if (!not_mask.empty() and not_mask != "-") {
+ node->setAttribute("not_modifiers", not_mask);
+ }
+ }
+
+ document->root()->appendChild(node);
+ }
+ }
+
+ sp_repr_save_file(document, file->get_path().c_str(), nullptr);
+ GC::release(document);
+
+ return true;
+};
+
+// Return if user set shortcut for Gio::Action.
+bool
+Shortcuts::is_user_set(Glib::ustring& action)
+{
+ auto it = action_user_set.find(action);
+ if (it != action_user_set.end()) {
+ return action_user_set[action];
+ } else {
+ return false;
+ }
+}
+
+// Get a list of detailed action names (as defined in action extra data).
+// This is more useful for shortcuts than a list of all actions.
+std::vector<Glib::ustring>
+Shortcuts::list_all_detailed_action_names()
+{
+ auto *iapp = InkscapeApplication::instance();
+ InkActionExtraData& action_data = iapp->get_action_extra_data();
+ return action_data.get_actions();
+}
+
+// Get a list of all actions (application, window, and document), properly prefixed.
+// We need to do this ourselves as Gtk::Application does not have a function for this.
+std::vector<Glib::ustring>
+Shortcuts::list_all_actions()
+{
+ std::vector<Glib::ustring> all_actions;
+
+ std::vector<Glib::ustring> actions = app->list_actions();
+ std::sort(actions.begin(), actions.end());
+ for (auto const &action : actions) {
+ all_actions.emplace_back("app." + action);
+ }
+
+ auto gwindow = app->get_active_window();
+ auto window = dynamic_cast<InkscapeWindow *>(gwindow);
+ if (window) {
+ std::vector<Glib::ustring> actions = window->list_actions();
+ std::sort(actions.begin(), actions.end());
+ for (auto const &action : actions) {
+ all_actions.emplace_back("win." + action);
+ }
+
+ auto document = window->get_document();
+ if (document) {
+ auto map = document->getActionGroup();
+ if (map) {
+ std::vector<Glib::ustring> actions = map->list_actions();
+ for (auto const &action : actions) {
+ all_actions.emplace_back("doc." + action);
+ }
+ } else {
+ std::cerr << "Shortcuts::list_all_actions: No document map!" << std::endl;
+ }
+ }
+ }
+
+ return all_actions;
+}
+
+
+// Add a shortcut, removing any previous use of shortcut.
+bool
+Shortcuts::add_shortcut(Glib::ustring name, const Gtk::AccelKey& shortcut, bool user)
+{
+ // Remove previous use of shortcut (already removed if new user shortcut).
+ if (Glib::ustring old_name = remove_shortcut(shortcut); old_name != "") {
+ // std::cerr << "Shortcut::add_shortcut: duplicate shortcut found for: " << shortcut.get_abbrev().raw()
+ // << " Old: " << old_name.raw() << " New: " << name.raw() << " !" << std::endl;
+ }
+
+ // Add shortcut
+
+ // To see if action exists, We need to compare action names without values...
+ Glib::ustring action_name_new;
+ Glib::VariantBase value_new;
+ Gio::SimpleAction::parse_detailed_name_variant(name, action_name_new, value_new);
+
+ for (auto const &action : list_all_detailed_action_names()) {
+ Glib::ustring action_name_old;
+ Glib::VariantBase value_old;
+ Gio::SimpleAction::parse_detailed_name_variant(action, action_name_old, value_old);
+
+ if (action_name_new == action_name_old) {
+ std::vector<Glib::ustring> accels;
+ // Action exists, add shortcut to list of shortcuts, if it's not a user shortcut.
+ // If it is a user-defined shortcut, then it replaces any defaults that might have been present.
+ // That's what we show in the UI when we define shortcuts (only new one) and that's also
+ // the only way to let user "overwrite" default shortcut, as there's no removal possible.
+ if (!user) {
+ accels = app->get_accels_for_action(name);
+ }
+ accels.push_back(shortcut.get_abbrev());
+ app->set_accels_for_action(name, accels);
+ action_user_set[name] = user;
+ _changed.emit();
+ return true;
+ }
+ }
+
+ // Oops, not an action!
+ std::cerr << "Shortcuts::add_shortcut: No Action for " << name.raw() << std::endl;
+ return false;
+}
+
+
+// Add a user shortcut, updating user's shortcut file if successful.
+bool
+Shortcuts::add_user_shortcut(Glib::ustring name, const Gtk::AccelKey& shortcut)
+{
+ // Remove previous shortcut(s) for action.
+ remove_shortcut(name);
+
+ // Remove previous use of shortcut from other actions.
+ remove_shortcut(shortcut);
+
+ // Add shortcut, if successful, save to file.
+ if (add_shortcut(name, shortcut, true)) { // Always user.
+ // Save
+ return write_user();
+ }
+
+ std::cerr << "Shortcut::add_user_shortcut: Failed to add: " << name.raw() << " with shortcut " << shortcut.get_abbrev().raw() << std::endl;
+ return false;
+};
+
+
+// Remove a shortcut via key. Return name of removed action.
+Glib::ustring
+Shortcuts::remove_shortcut(const Gtk::AccelKey& shortcut)
+{
+ std::vector<Glib::ustring> actions = app->get_actions_for_accel(shortcut.get_abbrev());
+ if (actions.empty()) {
+ return Glib::ustring(); // No action, no pie.
+ }
+
+ Glib::ustring action_name;
+ for (auto const &action : actions) {
+ // Remove just the one shortcut, leaving the others intact.
+ std::vector<Glib::ustring> accels = app->get_accels_for_action(action);
+ auto it = std::find(accels.begin(), accels.end(), shortcut.get_abbrev());
+ if (it != accels.end()) {
+ action_name = action;
+ accels.erase(it);
+ _changed.emit();
+ }
+ app->set_accels_for_action(action, accels);
+ }
+
+ return action_name;
+}
+
+
+// Remove a shortcut via action name.
+bool
+Shortcuts::remove_shortcut(Glib::ustring name)
+{
+ for (auto const &action : list_all_detailed_action_names()) {
+ if (action == name) {
+ // Action exists
+ app->unset_accels_for_action(action);
+ action_user_set.erase(action);
+ _changed.emit();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// Remove a user shortcut, updating user's shortcut file.
+bool
+Shortcuts::remove_user_shortcut(Glib::ustring name)
+{
+ // Check if really user shortcut.
+ bool user_shortcut = is_user_set(name);
+
+ if (!user_shortcut) {
+ // We don't allow removing non-user shortcuts.
+ return false;
+ }
+
+ if (remove_shortcut(name)) {
+ // Save
+ write_user();
+
+ // Reread to get original shortcut (if any).
+ init();
+ return true;
+ }
+
+ std::cerr << "Shortcuts::remove_user_shortcut: Failed to remove shortcut for: " << name.raw() << std::endl;
+ return false;
+}
+
+
+// Remove all user's shortcuts (simply overwrites existing file).
+bool
+Shortcuts::clear_user_shortcuts()
+{
+ // Create new empty document and save
+ auto *document = new XML::SimpleDocument();
+ XML::Node * node = document->createElement("keys");
+ node->setAttribute("name", "User Shortcuts");
+ document->appendChild(node);
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(get_path_string(USER, KEYS, "default.xml"));
+ sp_repr_save_file(document, file->get_path().c_str(), nullptr);
+ GC::release(document);
+
+ // Re-read everything!
+ init();
+ _changed.emit();
+ return true;
+}
+
+Glib::ustring
+Shortcuts::get_label(const Gtk::AccelKey& shortcut)
+{
+ Glib::ustring label;
+
+ if (!shortcut.is_null()) {
+ // ::get_label shows key pad and numeric keys identically.
+ // TODO: Results in labels like "Numpad Alt+5"
+ if (shortcut.get_abbrev().find("KP") != Glib::ustring::npos) {
+ label += _("Numpad");
+ label += " ";
+ }
+
+ label += Gtk::AccelGroup::get_label(shortcut.get_key(), shortcut.get_mod());
+ }
+
+ return label;
+}
+
+/*
+ * Return: keyval translated to group 0 in lower 32 bits, modifier encoded in upper 32 bits.
+ *
+ * Usage of group 0 (i.e. the main, typically English layout) instead of simply event->keyval
+ * ensures that shortcuts work regardless of the active keyboard layout (e.g. Cyrillic).
+ *
+ * The returned modifiers are the modifiers that were not "consumed" by the translation and
+ * can be used by the application to define a shortcut, e.g.
+ * - when pressing "Shift+9" the resulting character is "(";
+ * the shift key was "consumed" to make this character and should not be part of the shortcut
+ * - when pressing "Ctrl+9" the resulting character is "9";
+ * the ctrl key was *not* consumed to make this character and must be included in the shortcut
+ * - Exception: letter keys like [A-Z] always need the shift modifier,
+ * otherwise lower case and uper case keys are treated as equivalent.
+ */
+Gtk::AccelKey
+Shortcuts::get_from_event(GdkEventKey const *event, bool fix)
+{
+ // MOD2 corresponds to the NumLock key. Masking it out allows
+ // shortcuts to work regardless of its state.
+ Gdk::ModifierType initial_modifiers = Gdk::ModifierType(event->state & ~Gdk::MOD2_MASK);
+ unsigned int consumed_modifiers = 0;
+ //Gdk::ModifierType consumed_modifiers = Gdk::ModifierType(0);
+
+ unsigned int keyval = Inkscape::UI::Tools::get_latin_keyval(event, &consumed_modifiers);
+
+ // If a key value is "convertible", i.e. it has different lower case and upper case versions,
+ // convert to lower case and don't consume the "shift" modifier.
+ bool is_case_convertible = !(gdk_keyval_is_upper(keyval) && gdk_keyval_is_lower(keyval));
+ if (is_case_convertible) {
+ keyval = gdk_keyval_to_lower(keyval);
+ consumed_modifiers &= ~ Gdk::SHIFT_MASK;
+ }
+
+ // The InkscapePreferences dialog returns an event structure where the Shift modifier is not
+ // set for keys like '('. This causes '(' to be converted to '9' by get_latin_keyval. It also
+ // returns 'Shift-k' for 'K' (instead of 'Shift-K') but this is not a problem.
+ // We fix this by restoring keyval to its original value.
+ if (fix) {
+ keyval = event->keyval;
+ }
+
+ auto unused_modifiers = Gdk::ModifierType((initial_modifiers &~ consumed_modifiers)
+ & GDK_MODIFIER_MASK
+ &~ GDK_LOCK_MASK);
+
+ // std::cout << "Shortcuts::get_from_event: End: "
+ // << " Key: " << std::hex << keyval << " (" << (char)keyval << ")"
+ // << " Mod: " << std::hex << unused_modifiers << std::endl;
+ return (Gtk::AccelKey(keyval, unused_modifiers));
+}
+
+
+// Get a list of filenames to populate menu
+std::vector<std::pair<Glib::ustring, Glib::ustring>>
+Shortcuts::get_file_names()
+{
+ // TODO Filenames should be std::string but that means changing the whole stack.
+ using namespace Inkscape::IO::Resource;
+
+ // Make a list of all key files from System and User. Glib::ustring should be std::string!
+ std::vector<Glib::ustring> filenames = get_filenames(SYSTEM, KEYS, {".xml"});
+ // Exclude default.xml as it only contains user modifications.
+ std::vector<Glib::ustring> filenames_shared = get_filenames(SHARED, KEYS, {".xml"}, {"default.xml"});
+ // Exclude default.xml as it only contains user modifications.
+ std::vector<Glib::ustring> filenames_user = get_filenames(USER, KEYS, {".xml"}, {"default.xml"});
+ filenames.insert(filenames.end(), filenames_user.begin(), filenames_user.end());
+ filenames.insert(filenames.end(), filenames_shared.begin(), filenames_shared.end());
+
+ // Check file exists and extract out label if it does.
+ std::vector<std::pair<Glib::ustring, Glib::ustring>> names_and_paths;
+ for (auto &filename : filenames) {
+ std::string label = Glib::path_get_basename(filename);
+ Glib::ustring filename_relative = sp_relative_path_from_path(filename, std::string(get_path(SYSTEM, KEYS)));
+
+ XML::Document *document = sp_repr_read_file(filename.c_str(), nullptr, true);
+ if (!document) {
+ std::cerr << "Shortcut::get_file_names: could not parse file: " << filename.raw() << std::endl;
+ continue;
+ }
+
+ XML::NodeConstSiblingIterator iter = document->firstChild();
+ for ( ; iter ; ++iter ) { // We iterate in case of comments.
+ if (strcmp(iter->name(), "keys") == 0) {
+ gchar const *name = iter->attribute("name");
+ if (name) {
+ label = Glib::ustring(name) + " (" + label + ")";
+ }
+ std::pair<Glib::ustring, Glib::ustring> name_and_path = std::make_pair(label, filename_relative);
+ names_and_paths.emplace_back(name_and_path);
+ break;
+ }
+ }
+ if (!iter) {
+ std::cerr << "Shortcuts::get_File_names: not a shortcut keys file: " << filename.raw() << std::endl;
+ }
+
+ Inkscape::GC::release(document);
+ }
+
+ // Sort by name
+ std::sort(names_and_paths.begin(), names_and_paths.end(),
+ [](std::pair<Glib::ustring, Glib::ustring> pair1, std::pair<Glib::ustring, Glib::ustring> pair2) {
+ return Glib::path_get_basename(pair1.first).compare(Glib::path_get_basename(pair2.first)) < 0;
+ });
+
+ // But default.xml at top
+ auto it_default = std::find_if(names_and_paths.begin(), names_and_paths.end(),
+ [](std::pair<Glib::ustring, Glib::ustring>& pair) {
+ return !Glib::path_get_basename(pair.second).compare("default.xml");
+ });
+ if (it_default != names_and_paths.end()) {
+ std::rotate(names_and_paths.begin(), it_default, it_default+1);
+ }
+
+ return names_and_paths;
+}
+
+// void on_foreach(Gtk::Widget& widget) {
+// std::cout << "on_foreach: " << widget.get_name() << std::endl;;
+// }
+
+/*
+ * Update text with shortcuts.
+ * Inkscape includes shortcuts in tooltips and in dialog titles. They need to be updated
+ * anytime a tooltip is changed.
+ */
+void
+Shortcuts::update_gui_text_recursive(Gtk::Widget* widget)
+{
+
+ // NOT what we want
+ // auto activatable = dynamic_cast<Gtk::Activatable *>(widget);
+
+ // We must do this until GTK4
+ GtkWidget* gwidget = widget->gobj();
+ bool is_actionable = GTK_IS_ACTIONABLE(gwidget);
+
+ if (is_actionable) {
+ const gchar* gaction = gtk_actionable_get_action_name(GTK_ACTIONABLE(gwidget));
+ if (gaction) {
+ Glib::ustring action = gaction;
+
+ Glib::ustring variant;
+ GVariant* gvariant = gtk_actionable_get_action_target_value(GTK_ACTIONABLE(gwidget));
+ if (gvariant) {
+ Glib::ustring type = g_variant_get_type_string(gvariant);
+ if (type == "s") {
+ variant = g_variant_get_string(gvariant, nullptr);
+ action += "('" + variant + "')";
+ } else if (type == "i") {
+ variant = std::to_string(g_variant_get_int32(gvariant));
+ action += "(" + variant + ")";
+ } else {
+ std::cerr << "Shortcuts::update_gui_text_recursive: unhandled variant type: " << type.raw() << std::endl;
+ }
+ }
+
+ std::vector<Glib::ustring> accels = app->get_accels_for_action(action);
+
+ Glib::ustring tooltip;
+ auto *iapp = InkscapeApplication::instance();
+ if (iapp) {
+ tooltip = iapp->get_action_extra_data().get_tooltip_for_action(action, true, true);
+ }
+
+ // Add new primary accelerator.
+ if (accels.size() > 0) {
+
+ // Add space between tooltip and accel if there is a tooltip
+ if (!tooltip.empty()) {
+ tooltip += " ";
+ }
+
+ // Convert to more user friendly notation.
+ unsigned int key = 0;
+ Gdk::ModifierType mod = Gdk::ModifierType(0);
+ Gtk::AccelGroup::parse(accels[0], key, mod);
+ tooltip += "(" + Gtk::AccelGroup::get_label(key, mod) + ")";
+ }
+
+ // Update tooltip.
+ widget->set_tooltip_markup(tooltip);
+ }
+ }
+
+ auto container = dynamic_cast<Gtk::Container *>(widget);
+ if (container) {
+ auto children = container->get_children();
+ for (auto child : children) {
+ update_gui_text_recursive(child);
+ }
+ }
+
+}
+
+// Dialogs
+
+// Import user shortcuts from a file.
+bool
+Shortcuts::import_shortcuts() {
+
+ // Users key directory.
+ Glib::ustring directory = get_path_string(USER, KEYS, "");
+
+ // Create and show the dialog
+ Gtk::Window* window = app->get_active_window();
+ if (!window) {
+ return false;
+ }
+ Inkscape::UI::Dialog::FileOpenDialog *importFileDialog =
+ Inkscape::UI::Dialog::FileOpenDialog::create(*window, directory, Inkscape::UI::Dialog::CUSTOM_TYPE, _("Select a file to import"));
+ importFileDialog->addFilterMenu(_("Inkscape shortcuts (*.xml)"), "*.xml");
+ bool const success = importFileDialog->show();
+
+ if (!success) {
+ delete importFileDialog;
+ return false;
+ }
+
+ // Get file name and read.
+ Glib::ustring path = importFileDialog->getFilename(); // It's a full path, not just a filename!
+ delete importFileDialog;
+
+ Glib::RefPtr<Gio::File> file_read = Gio::File::create_for_path(path);
+ if (!read(file_read, true)) {
+ std::cerr << "Shortcuts::import_shortcuts: Failed to read file!" << std::endl;
+ return false;
+ }
+
+ // Save
+ return write_user();
+};
+
+bool
+Shortcuts::export_shortcuts() {
+
+ // Users key directory.
+ Glib::ustring directory = get_path_string(USER, KEYS, "");
+
+ // Create and show the dialog
+ Gtk::Window* window = app->get_active_window();
+ if (!window) {
+ return false;
+ }
+ Inkscape::UI::Dialog::FileSaveDialog *saveFileDialog =
+ Inkscape::UI::Dialog::FileSaveDialog::create(*window, directory, Inkscape::UI::Dialog::CUSTOM_TYPE, _("Select a filename for export"),
+ "", "", Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS);
+ saveFileDialog->addFilterMenu(_("Inkscape shortcuts (*.xml)"), "*.xml");
+ saveFileDialog->setFilename("shortcuts.xml");
+ bool success = saveFileDialog->show();
+
+ // Get file name and write.
+ if (success) {
+ Glib::ustring path = saveFileDialog->getFilename(); // It's a full path, not just a filename!
+ if (Inkscape::IO::get_file_extension(path) != ".xml") {
+ path += ".xml";
+ }
+ if (path.size() > 0) {
+ Glib::ustring newFileName = Glib::filename_to_utf8(path); // Is this really correct? (Paths should be std::string.)
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(path);
+ success = write(file, User);
+ } else {
+ // Can this ever happen?
+ success = false;
+ }
+ }
+
+ delete saveFileDialog;
+
+ return success;
+};
+
+/** Connects to a signal emitted whenever the shortcuts change */
+sigc::connection Shortcuts::connect_changed(sigc::slot<void ()> const &slot)
+{
+ return _changed.connect(slot);
+}
+
+// For debugging.
+void
+Shortcuts::dump() {
+
+ // What shortcuts are being used?
+ std::vector<Gdk::ModifierType> modifiers {
+ Gdk::ModifierType(0),
+ Gdk::SHIFT_MASK,
+ Gdk::CONTROL_MASK,
+ Gdk::MOD1_MASK,
+ Gdk::SHIFT_MASK | Gdk::CONTROL_MASK,
+ Gdk::SHIFT_MASK | Gdk::MOD1_MASK,
+ Gdk::CONTROL_MASK | Gdk::MOD1_MASK,
+ Gdk::SHIFT_MASK | Gdk::CONTROL_MASK | Gdk::MOD1_MASK
+ };
+ for (auto mod : modifiers) {
+ for (gchar key = '!'; key <= '~'; ++key) {
+
+ Glib::ustring action;
+ Glib::ustring accel = Gtk::AccelGroup::name(key, mod);
+ std::vector<Glib::ustring> actions = app->get_actions_for_accel(accel);
+ if (!actions.empty()) {
+ action = actions[0];
+ }
+
+ std::cout << " shortcut:"
+ << " " << std::setw(8) << std::hex << mod
+ << " " << std::setw(8) << std::hex << key
+ << " " << std::setw(30) << std::left << accel
+ << " " << action
+ << std::endl;
+ }
+ }
+}
+
+void
+Shortcuts::dump_all_recursive(Gtk::Widget* widget)
+{
+ static unsigned int indent = 0;
+ ++indent;
+ for (int i = 0; i < indent; ++i) std::cout << " ";
+
+ // NOT what we want
+ // auto activatable = dynamic_cast<Gtk::Activatable *>(widget);
+
+ // We must do this until GTK4
+ GtkWidget* gwidget = widget->gobj();
+ bool is_actionable = GTK_IS_ACTIONABLE(gwidget);
+ Glib::ustring action;
+ if (is_actionable) {
+ const gchar* gaction = gtk_actionable_get_action_name( GTK_ACTIONABLE(gwidget) );
+ if (gaction) {
+ action = gaction;
+ }
+ }
+
+ std::cout << widget->get_name()
+ << ": actionable: " << std::boolalpha << is_actionable
+ << ": " << widget->get_tooltip_text()
+ << ": " << action
+ << std::endl;
+ auto container = dynamic_cast<Gtk::Container *>(widget);
+ if (container) {
+ auto children = container->get_children();
+ for (auto child : children) {
+ dump_all_recursive(child);
+ }
+ }
+ --indent;
+}
+
+
+} // Namespace
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :