diff --git a/libs/widgetutils/kis_action_registry.cpp b/libs/widgetutils/kis_action_registry.cpp index 629ec052c4..7ca59441bf 100644 --- a/libs/widgetutils/kis_action_registry.cpp +++ b/libs/widgetutils/kis_action_registry.cpp @@ -1,477 +1,480 @@ /* * Copyright (c) 2015 Michael Abrahams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include "kis_debug.h" #include "KoResourcePaths.h" #include "kis_icon_utils.h" #include "kactioncollection.h" #include "kactioncategory.h" #include "kis_action_registry.h" #include "kshortcutschemeshelper_p.h" namespace { /** * We associate several pieces of information with each shortcut. The first * piece of information is a QDomElement, containing the raw data from the * .action XML file. The second and third are QKeySequences, the first of * which is the default shortcut, the last of which is any custom shortcut. * The last two are the KActionCollection and KActionCategory used to * organize the shortcut editor. */ struct ActionInfoItem { QDomElement xmlData; - QKeySequence defaultShortcut; - QKeySequence customShortcut; + QList defaultShortcuts; + QList customShortcuts; QString collectionName; QString categoryName; }; // Convenience macros to extract text of a child node. QString getChildContent(QDomElement xml, QString node) { return xml.firstChildElement(node).text(); }; ActionInfoItem emptyActionInfo; // Used as default return value // Use Krita debug logging categories instead of KDE's default qDebug() for // harmless empty strings and translations QString quietlyTranslate(const QString &s) { if (s.isEmpty()) { return s; } if (i18n(s.toUtf8().constData()).isEmpty()) { dbgAction << "No translation found for" << s; return s; } return i18n(s.toUtf8().constData()); }; - QKeySequence preferredShortcut(ActionInfoItem action) { - if (action.customShortcut.isEmpty()) { - return action.defaultShortcut; + QList preferredShortcuts(ActionInfoItem action) { + if (action.customShortcuts.isEmpty()) { + return action.defaultShortcuts; } else { - return action.customShortcut; + return action.customShortcuts; } }; }; class Q_DECL_HIDDEN KisActionRegistry::Private { public: Private(KisActionRegistry *_q) : q(_q) {}; // This is the main place containing ActionInfoItems. QMap actionInfoList; void loadActionFiles(); void loadActionCollections(); void loadCustomShortcuts(QString filename = QStringLiteral("kritashortcutsrc")); ActionInfoItem &actionInfo(const QString &name) { if (!actionInfoList.contains(name)) { dbgAction << "Tried to look up info for unknown action" << name; } return actionInfoList[name]; }; KisActionRegistry *q; KActionCollection * defaultActionCollection; QMap actionCollections; }; Q_GLOBAL_STATIC(KisActionRegistry, s_instance); KisActionRegistry *KisActionRegistry::instance() { return s_instance; }; KisActionRegistry::KisActionRegistry() : d(new KisActionRegistry::Private(this)) { d->loadActionFiles(); KConfigGroup cg = KSharedConfig::openConfig()->group("Shortcut Schemes"); QString schemeName = cg.readEntry("Current Scheme", "Default"); loadShortcutScheme(schemeName); loadCustomShortcuts(); } -QKeySequence KisActionRegistry::getCustomShortcut(const QString &name) +QList KisActionRegistry::getCustomShortcut(const QString &name) { - return d->actionInfo(name).customShortcut; + return d->actionInfo(name).customShortcuts; }; -QKeySequence KisActionRegistry::getPreferredShortcut(const QString &name) +QList KisActionRegistry::getPreferredShortcut(const QString &name) { - return preferredShortcut(d->actionInfo(name)); + return preferredShortcuts(d->actionInfo(name)); }; -QKeySequence KisActionRegistry::getCategory(const QString &name) +QString KisActionRegistry::getCategory(const QString &name) { return d->actionInfo(name).categoryName; }; QStringList KisActionRegistry::allActions() { return d->actionInfoList.keys(); }; KActionCollection * KisActionRegistry::getDefaultCollection() { return d->actionCollections.value("Krita"); }; void KisActionRegistry::addAction(const QString &name, QAction *a) { auto info = d->actionInfo(name); KActionCollection *collection = d->actionCollections.value(info.collectionName); if (!collection) { dbgAction << "No collection found for action" << name; return; } if (collection->action(name)) { dbgAction << "duplicate action" << name << "in collection" << collection->componentName(); } else { } collection->addCategorizedAction(name, a, info.categoryName); }; void KisActionRegistry::notifySettingsUpdated() { d->loadCustomShortcuts(); }; void KisActionRegistry::loadCustomShortcuts(const QString &path) { if (path.isEmpty()) { d->loadCustomShortcuts(); } else { d->loadCustomShortcuts(path); } }; void KisActionRegistry::loadShortcutScheme(const QString &schemeName) { // Load scheme file if (schemeName != QStringLiteral("Default")) { QString schemeFileName = KShortcutSchemesHelper::schemeFileLocations().value(schemeName); if (schemeFileName.isEmpty()) { // qDebug() << "No configuration file found for scheme" << schemeName; return; } KConfig schemeConfig(schemeFileName, KConfig::SimpleConfig); applyShortcutScheme(&schemeConfig); } else { // Apply default scheme, updating KisActionRegistry data applyShortcutScheme(); } } QAction * KisActionRegistry::makeQAction(const QString &name, QObject *parent) { QAction * a = new QAction(parent); if (!d->actionInfoList.contains(name)) { dbgAction << "Warning: requested data for unknown action" << name; return a; } propertizeAction(name, a); return a; }; void KisActionRegistry::setupDialog(KisShortcutsDialog *dlg) { for (auto i = d->actionCollections.constBegin(); i != d->actionCollections.constEnd(); i++ ) { dlg->addCollection(i.value(), i.key()); } } void KisActionRegistry::settingsPageSaved() { // For now, custom shortcuts are dealt with by writing to file and reloading. loadCustomShortcuts(); // Announce UI should reload current shortcuts. emit shortcutsUpdated(); } void KisActionRegistry::applyShortcutScheme(const KConfigBase *config) { // First, update the things in KisActionRegistry if (config == 0) { // Use default shortcut scheme. Simplest just to reload everything. d->actionInfoList.clear(); d->loadActionFiles(); loadCustomShortcuts(); } else { const auto schemeEntries = config->group(QStringLiteral("Shortcuts")).entryMap(); // Load info item for each shortcut, reset custom shortcuts auto it = schemeEntries.constBegin(); while (it != schemeEntries.end()) { ActionInfoItem &info = d->actionInfo(it.key()); - info.defaultShortcut = it.value(); + if (!it.value().isEmpty()) + info.defaultShortcuts = QKeySequence::listFromString(it.value()); it++; } } } void KisActionRegistry::updateShortcut(const QString &name, QAction *action) { const ActionInfoItem info = d->actionInfo(name); - action->setShortcut(preferredShortcut(info)); + auto newShortcuts = preferredShortcuts(info); + action->setShortcuts(newShortcuts); - auto defaultShortcutsList = QList(); - if (info.defaultShortcut != QKeySequence("")) { - // Use the empty list to represent no shortcut - defaultShortcutsList << info.defaultShortcut; - } - action->setProperty("defaultShortcuts", qVariantFromValue(defaultShortcutsList)); + action->setProperty("defaultShortcuts", qVariantFromValue(newShortcuts)); } bool KisActionRegistry::propertizeAction(const QString &name, QAction * a) { const ActionInfoItem info = d->actionInfo(name); QDomElement actionXml = info.xmlData; if (actionXml.text().isEmpty()) { dbgAction << "No XML data found for action" << name; return false; } // i18n requires converting format from QString. auto getChildContent_i18n = [=](QString node){return quietlyTranslate(getChildContent(actionXml, node));}; // Note: the fields in the .action documents marked for translation are determined by extractrc. QString icon = getChildContent(actionXml, "icon"); QString text = getChildContent_i18n("text"); QString whatsthis = getChildContent_i18n("whatsThis"); QString toolTip = getChildContent_i18n("toolTip"); QString statusTip = getChildContent_i18n("statusTip"); QString iconText = getChildContent_i18n("iconText"); bool isCheckable = getChildContent(actionXml, "isCheckable") == QString("true"); a->setObjectName(name); // This is helpful, should be added more places in Krita a->setIcon(KisIconUtils::loadIcon(icon.toLatin1())); a->setText(text); a->setObjectName(name); a->setWhatsThis(whatsthis); a->setToolTip(toolTip); a->setStatusTip(statusTip); a->setIconText(iconText); a->setCheckable(isCheckable); updateShortcut(name, a); - // TODO: check for colliding shortcuts, either here, or in loading code + // TODO: check for colliding shortcuts in .action files either here or in loading code #if 0 QMap existingShortcuts; Q_FOREACH (QAction* action, actionCollection->actions()) { if(action->shortcut() == QKeySequence(0)) { continue; } if (existingShortcuts.contains(action->shortcut())) { dbgAction << QString("Actions %1 and %2 have the same shortcut: %3") \ .arg(action->text()) \ .arg(existingShortcuts[action->shortcut()]->text()) \ .arg(action->shortcut()); } else { existingShortcuts[action->shortcut()] = action; } } #endif return true; } QString KisActionRegistry::getActionProperty(const QString &name, const QString &property) { ActionInfoItem info = d->actionInfo(name); QDomElement actionXml = info.xmlData; if (actionXml.text().isEmpty()) { dbgAction << "No XML data found for action" << name; return QString(); } return getChildContent(actionXml, property); } void KisActionRegistry::writeCustomShortcuts(KConfigBase *config) const { KConfigGroup cg; if (config == 0) { cg = KConfigGroup(KSharedConfig::openConfig("kritashortcutsrc"), QStringLiteral("Shortcuts")); } else { cg = KConfigGroup(config, QStringLiteral("Shortcuts")); } for (auto it = d->actionInfoList.constBegin(); it != d->actionInfoList.constEnd(); ++it) { QString actionName = it.key(); - QString s = it.value().customShortcut.toString(); + QString s = QKeySequence::listToString(it.value().customShortcuts); if (s.isEmpty()) { cg.deleteEntry(actionName, KConfigGroup::Persistent); } else { cg.writeEntry(actionName, s, KConfigGroup::Persistent); } } cg.sync(); } void KisActionRegistry::Private::loadActionFiles() { auto searchType = KoResourcePaths::Recursive | KoResourcePaths::NoDuplicates; QStringList actionDefinitions = KoResourcePaths::findAllResources("kis_actions", "*.action", searchType); // Extract actions all XML .action files. Q_FOREACH (const QString &actionDefinition, actionDefinitions) { QDomDocument doc; QFile f(actionDefinition); f.open(QFile::ReadOnly); doc.setContent(f.readAll()); QDomElement base = doc.documentElement(); // "ActionCollection" outer group QString collectionName = base.attribute("name"); QString version = base.attribute("version"); if (version != "2") { errAction << ".action XML file" << actionDefinition << "has incorrect version; skipping."; continue; } KActionCollection *actionCollection; if (!actionCollections.contains(collectionName)) { actionCollection = new KActionCollection(q, collectionName); actionCollections.insert(collectionName, actionCollection); dbgAction << "Adding a new action collection " << collectionName; } else { actionCollection = actionCollections.value(collectionName); } // Loop over nodes. Each of these corresponds to a // KActionCategory, producing a group of actions in the shortcut dialog. QDomElement actions = base.firstChild().toElement(); while (!actions.isNull()) { // field QDomElement categoryTextNode = actions.firstChild().toElement(); QString categoryName = quietlyTranslate(categoryTextNode.text()); // KActionCategory *category = actionCollection->getCategory(categoryName); // dbgAction << "Using category" << categoryName; // tags QDomElement actionXml = categoryTextNode.nextSiblingElement(); // Loop over individual actions while (!actionXml.isNull()) { if (actionXml.tagName() == "Action") { // Read name from format QString name = actionXml.attribute("name"); // Bad things if (name.isEmpty()) { errAction << "Unnamed action in definitions file " << actionDefinition; } else if (actionInfoList.contains(name)) { // errAction << "NOT COOL: Duplicated action name from xml data: " << name; } else { ActionInfoItem info; info.xmlData = actionXml; - info.defaultShortcut = getChildContent(actionXml, "shortcut"); - info.customShortcut = QKeySequence(); + + // Use empty list to signify no shortcut + QString shortcutText = getChildContent(actionXml, "shortcut"); + if (!shortcutText.isEmpty()) + info.defaultShortcuts << QKeySequence(shortcutText); + info.categoryName = categoryName; info.collectionName = collectionName; // dbgAction << "default shortcut for" << name << " - " << info.defaultShortcut; actionInfoList.insert(name,info); } } actionXml = actionXml.nextSiblingElement(); } actions = actions.nextSiblingElement(); } } }; void KisActionRegistry::Private::loadCustomShortcuts(QString filename) { const KConfigGroup localShortcuts(KSharedConfig::openConfig(filename), QStringLiteral("Shortcuts")); if (!localShortcuts.exists()) { return; } for (auto i = actionInfoList.begin(); i != actionInfoList.end(); ++i) { if (localShortcuts.hasKey(i.key())) { QString entry = localShortcuts.readEntry(i.key(), QString()); - i.value().customShortcut = QKeySequence(entry); - } else { - i.value().customShortcut = QKeySequence(); + if (entry != QStringLiteral("none")) { + i.value().customShortcuts = QKeySequence::listFromString(entry); + continue; + } } + i.value().customShortcuts = QList(); } }; diff --git a/libs/widgetutils/kis_action_registry.h b/libs/widgetutils/kis_action_registry.h index 5bad45f76e..7da8b0c391 100644 --- a/libs/widgetutils/kis_action_registry.h +++ b/libs/widgetutils/kis_action_registry.h @@ -1,166 +1,165 @@ /* * Copyright (c) 2015 Michael Abrahams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include "kritawidgetutils_export.h" class KActionCollection; class QDomElement; class KConfigBase; class KisShortcutsDialog; /** * KisActionRegistry is intended to manage the global action configuration data * for Krita. The data come from four sources: * - .action files, containing static action configuration data in XML format, * - .rc configuration files, originally from XMLGUI and now in WidgetUtils, * - kritashortcutsrc, containing temporary shortcut configuration, and * - .shortcuts scheme files providing sets of default shortcuts, also from XMLGUI * * This class can be used as a factory by calling makeQAction. It can be used to * add standard properties such as default shortcuts and default tooltip to an * existing action with propertizeAction. If you have a custom action class * which needs to add other properties, you can use propertizeAction to add any * sort of data you wish to the .action configuration file. * * This class is also in charge of displaying the shortcut configuration dialog. * The interplay between this class, KActionCollection, KisShortcutsEditor and * so on can be complex, and is sometimes synchronized by file I/O by reading * and writing the configuration files mentioned above. * * It is a global static. Grab an ::instance(). */ class KRITAWIDGETUTILS_EXPORT KisActionRegistry : public QObject { Q_OBJECT public: static KisActionRegistry *instance(); /** * Get shortcut for an action */ - QKeySequence getPreferredShortcut(const QString &name); + QList getPreferredShortcut(const QString &name); /** * Get shortcut for an action */ - QKeySequence getDefaultShortcut(const QString &name); + QList getDefaultShortcut(const QString &name); /** * Get custom shortcut for an action */ - QKeySequence getCustomShortcut(const QString &name); - + QList getCustomShortcut(const QString &name); /** * Get category name */ - QKeySequence getCategory(const QString &name); + QString getCategory(const QString &name); /** * @return value @p property for an action @p name. * * Allow flexible info structure for KisActions, etc. */ QString getActionProperty(const QString &name, const QString &property); /** * Saves action in a category. Note that this grabs ownership of the action. */ void addAction(const QString &name, QAction *a); /** * Produces a new QAction based on the .action data files. * * N.B. this action will not be saved in the registry. */ QAction * makeQAction(const QString &name, QObject *parent); /** * Fills the standard QAction properties of an action. * * @return true if the action was loaded successfully. */ bool propertizeAction(const QString &name, QAction *a); /** * @return list of actions with data available. */ QStringList allActions(); /** * Setup the shortcut configuration widget. */ void setupDialog(KisShortcutsDialog *dlg); /** * Called when "OK" button is pressed in settings dialog. */ void settingsPageSaved(); /** * Reload custom shortcuts from kritashortcutsrc */ void loadCustomShortcuts(const QString &path = QString()); /** * Write custom shortcuts to a specific file */ void writeCustomShortcuts(KConfigBase *config) const; /** * Call after settings are changed. */ void notifySettingsUpdated(); /** * Constructor. Please don't touch! */ KisActionRegistry(); // Undocumented void updateShortcut(const QString &name, QAction *ac); KActionCollection * getDefaultCollection(); void loadShortcutScheme(const QString &schemeName); // If config == 0, reload defaults void applyShortcutScheme(const KConfigBase *config = 0); Q_SIGNALS: void shortcutsUpdated(); private: class Private; Private * const d; }; diff --git a/libs/widgetutils/xmlgui/KisShortcutEditWidget.cpp b/libs/widgetutils/xmlgui/KisShortcutEditWidget.cpp index 38b832a0ad..f0760fed0e 100644 --- a/libs/widgetutils/xmlgui/KisShortcutEditWidget.cpp +++ b/libs/widgetutils/xmlgui/KisShortcutEditWidget.cpp @@ -1,192 +1,192 @@ /* This file is part of the KDE libraries Copyright (C) 1998 Mark Donohoe Copyright (C) 1997 Nicolas Hadacek Copyright (C) 1998 Matthias Ettrich Copyright (C) 2001 Ellis Whitehead Copyright (C) 2006 Hamish Rodda Copyright (C) 2007 Roberto Raggi Copyright (C) 2007 Andreas Hartmetz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KisShortcutsDialog_p.h" #include #include #include #include #include #include #include //#include #include "kkeysequencewidget.h" void ShortcutEditWidget::paintEvent(QPaintEvent *e) { QWidget::paintEvent(e); QPainter p(this); QPen pen(QPalette().highlight().color()); pen.setWidth(6); p.setPen(pen); p.drawLine(0, 0, width(), 0); if (qApp->isLeftToRight()) { p.drawLine(0, 0, 0, height()); } else { p.drawLine(width(), 0, width(), height()); } } ShortcutEditWidget::ShortcutEditWidget(QWidget *viewport, const QKeySequence &defaultSeq, const QKeySequence &activeSeq, bool allowLetterShortcuts) : QWidget(viewport), m_defaultKeySequence(defaultSeq), m_isUpdating(false), m_action(0) { QGridLayout *layout = new QGridLayout(this); m_defaultRadio = new QRadioButton(i18n("Default:"), this); m_defaultLabel = new QLabel(i18nc("No shortcut defined", "None"), this); QString defaultText = defaultSeq.toString(QKeySequence::NativeText); if (defaultText.isEmpty()) { defaultText = i18nc("No shortcut defined", "None"); } m_defaultLabel->setText(defaultText); m_customRadio = new QRadioButton(i18n("Custom:"), this); m_customEditor = new KKeySequenceWidget(this); m_customEditor->setModifierlessAllowed(allowLetterShortcuts); layout->addWidget(m_defaultRadio, 0, 0); layout->addWidget(m_defaultLabel, 0, 1); layout->addWidget(m_customRadio, 1, 0); layout->addWidget(m_customEditor, 1, 1); layout->setColumnStretch(2, 1); setKeySequence(activeSeq); connect(m_defaultRadio, SIGNAL(toggled(bool)), this, SLOT(defaultToggled(bool))); connect(m_customEditor, SIGNAL(keySequenceChanged(QKeySequence)), this, SLOT(setCustom(QKeySequence))); connect(m_customEditor, SIGNAL(stealShortcut(QKeySequence,QAction*)), this, SIGNAL(stealShortcut(QKeySequence,QAction*))); } KKeySequenceWidget::ShortcutTypes ShortcutEditWidget::checkForConflictsAgainst() const { return m_customEditor->checkForConflictsAgainst(); } //slot void ShortcutEditWidget::defaultToggled(bool checked) { if (m_isUpdating) { return; } m_isUpdating = true; if (checked) { // The default key sequence should be activated. We check first if this // is possible. if (m_customEditor->isKeySequenceAvailable(m_defaultKeySequence)) { // Clear the customs widget m_customEditor->clearKeySequence(); emit keySequenceChanged(m_defaultKeySequence); } else { // We tried to switch to the default key sequence and failed. // Go back. m_customRadio->setChecked(true); } } else { // The empty key sequence is always valid emit keySequenceChanged(QKeySequence()); } m_isUpdating = false; } void ShortcutEditWidget::setCheckActionCollections( const QList checkActionCollections) { // We just forward them to out KKeySequenceWidget. m_customEditor->setCheckActionCollections(checkActionCollections); } void ShortcutEditWidget::setCheckForConflictsAgainst(KKeySequenceWidget::ShortcutTypes types) { m_customEditor->setCheckForConflictsAgainst(types); } void ShortcutEditWidget::setComponentName(const QString componentName) { m_customEditor->setComponentName(componentName); } void ShortcutEditWidget::setMultiKeyShortcutsAllowed(bool allowed) { // We just forward them to out KKeySequenceWidget. m_customEditor->setMultiKeyShortcutsAllowed(allowed); } bool ShortcutEditWidget::multiKeyShortcutsAllowed() const { return m_customEditor->multiKeyShortcutsAllowed(); } void ShortcutEditWidget::setAction(QObject *action) { m_action = action; } //slot void ShortcutEditWidget::setCustom(const QKeySequence &seq) { if (m_isUpdating) { return; } // seq is a const reference to a private variable of KKeySequenceWidget. // Somewhere below we possible change that one. But we want to emit seq // whatever happens. So we make a copy. QKeySequence original = seq; m_isUpdating = true; // Check if the user typed in the default sequence into the custom field. // We do this by calling setKeySequence which will do the right thing. setKeySequence(original); emit keySequenceChanged(original); m_isUpdating = false; } void ShortcutEditWidget::setKeySequence(const QKeySequence &activeSeq) { - if (activeSeq.toString(QKeySequence::NativeText) == m_defaultLabel->text()) { + if (activeSeq.toString(QKeySequence::NativeText) == m_defaultKeySequence.toString(QKeySequence::NativeText)) { m_defaultRadio->setChecked(true); m_customEditor->clearKeySequence(); } else { m_customRadio->setChecked(true); // m_customEditor->setKeySequence does some stuff we only want to // execute when the sequence really changes. if (activeSeq != m_customEditor->keySequence()) { m_customEditor->setKeySequence(activeSeq); } } }