File indexing completed on 2024-05-12 16:02:26

0001 /*
0002  *  SPDX-FileCopyrightText: 2015 Michael Abrahams <miabraha@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 
0008 #include <QString>
0009 #include <QHash>
0010 #include <QGlobalStatic>
0011 #include <QFile>
0012 #include <QFileInfo>
0013 #include <QDomElement>
0014 #include <KSharedConfig>
0015 #include <klocalizedstring.h>
0016 #include <KisShortcutsDialog.h>
0017 #include <KConfigGroup>
0018 #include <qdom.h>
0019 
0020 #include "kis_debug.h"
0021 #include "KoResourcePaths.h"
0022 #include "kis_icon_utils.h"
0023 
0024 #include "kis_action_registry.h"
0025 #include "kshortcutschemeshelper_p.h"
0026 
0027 
0028 namespace {
0029 
0030     /**
0031      * We associate several pieces of information with each shortcut. The first
0032      * piece of information is a QDomElement, containing the raw data from the
0033      * .action XML file. The second and third are QKeySequences, the first of
0034      * which is the default shortcut, the last of which is any custom shortcut.
0035      * The last two are the KisKActionCollection and KisKActionCategory used to
0036      * organize the shortcut editor.
0037      */
0038     struct ActionInfoItem {
0039         QDomElement  xmlData;
0040 
0041         QString      collectionName;
0042         QString      categoryName;
0043 
0044         inline QList<QKeySequence> defaultShortcuts() const {
0045             return m_defaultShortcuts;
0046         }
0047 
0048         inline void setDefaultShortcuts(const QList<QKeySequence> &value) {
0049             m_defaultShortcuts = value;
0050         }
0051 
0052         inline QList<QKeySequence> customShortcuts() const {
0053             return m_customShortcuts;
0054         }
0055 
0056         inline void setCustomShortcuts(const QList<QKeySequence> &value, bool explicitlyReset) {
0057             m_customShortcuts = value;
0058             m_explicitlyReset = explicitlyReset;
0059         }
0060 
0061         inline QList<QKeySequence> effectiveShortcuts() const {
0062             return m_customShortcuts.isEmpty() && !m_explicitlyReset ?
0063                 m_defaultShortcuts : m_customShortcuts;
0064         }
0065 
0066 
0067     private:
0068         QList<QKeySequence> m_defaultShortcuts;
0069         QList<QKeySequence> m_customShortcuts;
0070         bool m_explicitlyReset = false;
0071     };
0072 
0073     // Convenience macros to extract a child node.
0074     QDomElement getChild(QDomElement xml, QString node) {
0075         return xml.firstChildElement(node);
0076     }
0077 
0078     // Convenience macros to extract text of a child node.
0079     QString getChildContent(QDomElement xml, QString node) {
0080         return xml.firstChildElement(node).text();
0081     }
0082 
0083     QString getChildContentForOS(QDomElement xml, QString tagName, QString os = QString()) {
0084         bool found = false;
0085 
0086         QDomElement node = xml.firstChildElement(tagName);
0087         QDomElement nodeElse;
0088 
0089         while(!found && !node.isNull()) {
0090             if (node.attribute("operatingSystem") == os) {
0091                 found = true;
0092                 break;
0093             }
0094             else if (node.hasAttribute("operatingSystemElse")) {
0095                 nodeElse = node;
0096             }
0097             else if (nodeElse.isNull()) {
0098                 nodeElse = node;
0099             }
0100 
0101             node = node.nextSiblingElement(tagName);
0102         }
0103 
0104         if (!found && !nodeElse.isNull()) {
0105             return nodeElse.text();
0106         }
0107         return node.text();
0108     }
0109 
0110     // Use Krita debug logging categories instead of KDE's default qDebug() for
0111     // harmless empty strings and translations
0112     QString quietlyTranslate(const QDomElement &s) {
0113         if (s.isNull() || s.text().isEmpty()) {
0114             return QString();
0115         }
0116         QString translatedString;
0117         const QString attrContext = QStringLiteral("context");
0118         const QString attrDomain = QStringLiteral("translationDomain");
0119         QString context = QStringLiteral("action");
0120 
0121         if (!s.attribute(attrContext).isEmpty()) {
0122             context = s.attribute(attrContext);
0123         }
0124 
0125         QByteArray domain = s.attribute(attrDomain).toUtf8();
0126         if (domain.isEmpty()) {
0127             domain = s.ownerDocument().documentElement().attribute(attrDomain).toUtf8();
0128             if (domain.isEmpty()) {
0129                 domain = KLocalizedString::applicationDomain();
0130             }
0131         }
0132         translatedString = i18ndc(domain.constData(), context.toUtf8().constData(), s.text().toUtf8().constData());
0133         if (translatedString == s.text()) {
0134             translatedString = i18n(s.text().toUtf8().constData());
0135         }
0136         if (translatedString.isEmpty()) {
0137             dbgAction << "No translation found for" << s.text();
0138             return s.text();
0139         }
0140 
0141         return translatedString;
0142     }
0143 }
0144 
0145 
0146 
0147 class Q_DECL_HIDDEN KisActionRegistry::Private
0148 {
0149 public:
0150 
0151     Private(KisActionRegistry *_q) : q(_q) {}
0152 
0153     // This is the main place containing ActionInfoItems.
0154     QMap<QString, ActionInfoItem> actionInfoList;
0155     void loadActionFiles();
0156     void loadCustomShortcuts(QString filename = QStringLiteral("kritashortcutsrc"));
0157 
0158     // XXX: this adds a default item for the given name to the list of actioninfo objects!
0159     ActionInfoItem &actionInfo(const QString &name) {
0160         if (!actionInfoList.contains(name)) {
0161             dbgAction << "Tried to look up info for unknown action" << name;
0162         }
0163         return actionInfoList[name];
0164     }
0165 
0166     KisActionRegistry *q;
0167     QSet<QString> sanityPropertizedShortcuts;
0168 };
0169 
0170 
0171 Q_GLOBAL_STATIC(KisActionRegistry, s_instance)
0172 
0173 KisActionRegistry *KisActionRegistry::instance()
0174 {
0175     if (!s_instance.exists()) {
0176         dbgRegistry << "initializing KoActionRegistry";
0177     }
0178     return s_instance;
0179 }
0180 
0181 bool KisActionRegistry::hasAction(const QString &name) const
0182 {
0183     return d->actionInfoList.contains(name);
0184 }
0185 
0186 
0187 KisActionRegistry::KisActionRegistry()
0188     : d(new KisActionRegistry::Private(this))
0189 {
0190     KConfigGroup cg = KSharedConfig::openConfig()->group("Shortcut Schemes");
0191     QString schemeName = cg.readEntry("Current Scheme", "Default");
0192     QString schemeFileName = KisKShortcutSchemesHelper::schemeFileLocations().value(schemeName);
0193     if (!QFileInfo(schemeFileName).exists()) {
0194         schemeName = "Default";
0195     }
0196     loadShortcutScheme(schemeName);
0197     loadCustomShortcuts();
0198 }
0199 
0200 KisActionRegistry::~KisActionRegistry()
0201 {
0202 }
0203 
0204 KisActionRegistry::ActionCategory KisActionRegistry::fetchActionCategory(const QString &name) const
0205 {
0206     if (!d->actionInfoList.contains(name)) return ActionCategory();
0207 
0208     const ActionInfoItem info = d->actionInfoList.value(name);
0209     return ActionCategory(info.collectionName, info.categoryName);
0210 }
0211 
0212 void KisActionRegistry::notifySettingsUpdated()
0213 {
0214     d->loadCustomShortcuts();
0215 }
0216 
0217 void KisActionRegistry::loadCustomShortcuts()
0218 {
0219     d->loadCustomShortcuts();
0220 }
0221 
0222 void KisActionRegistry::loadShortcutScheme(const QString &schemeName)
0223 {
0224     // Load scheme file
0225     if (schemeName != QStringLiteral("Default")) {
0226         QString schemeFileName = KisKShortcutSchemesHelper::schemeFileLocations().value(schemeName);
0227         if (schemeFileName.isEmpty() || !QFileInfo(schemeFileName).exists()) {
0228             applyShortcutScheme();
0229             return;
0230         }
0231         KConfig schemeConfig(schemeFileName, KConfig::SimpleConfig);
0232         applyShortcutScheme(&schemeConfig);
0233     } else {
0234         // Apply default scheme, updating KisActionRegistry data
0235         applyShortcutScheme();
0236     }
0237 }
0238 
0239 QAction * KisActionRegistry::makeQAction(const QString &name, QObject *parent)
0240 {
0241     QAction * a = new QAction(parent);
0242     if (!d->actionInfoList.contains(name)) {
0243         qWarning() << "Warning: requested data for unknown action" << name;
0244         a->setObjectName(name);
0245         return a;
0246     }
0247 
0248     propertizeAction(name, a);
0249     return a;
0250 }
0251 
0252 void KisActionRegistry::settingsPageSaved()
0253 {
0254    // For now, custom shortcuts are dealt with by writing to file and reloading.
0255    loadCustomShortcuts();
0256 
0257    // Announce UI should reload current shortcuts.
0258    emit shortcutsUpdated();
0259 }
0260 
0261 
0262 void KisActionRegistry::applyShortcutScheme(const KConfigBase *config)
0263 {
0264     // First, update the things in KisActionRegistry
0265     d->actionInfoList.clear();
0266     d->loadActionFiles();
0267 
0268     if (config == 0) {
0269         // Use default shortcut scheme. Simplest just to reload everything.
0270         loadCustomShortcuts();
0271     } else {
0272         const auto schemeEntries = config->group(QStringLiteral("Shortcuts")).entryMap();
0273         // Load info item for each shortcut, reset custom shortcuts
0274         auto it = schemeEntries.constBegin();
0275         while (it != schemeEntries.end()) {
0276             ActionInfoItem &info = d->actionInfo(it.key());
0277             info.setDefaultShortcuts(QKeySequence::listFromString(it.value()));
0278             it++;
0279         }
0280     }
0281 }
0282 
0283 void KisActionRegistry::updateShortcut(const QString &name, QAction *action)
0284 {
0285     const ActionInfoItem &info = d->actionInfo(name);
0286     action->setShortcuts(info.effectiveShortcuts());
0287     action->setProperty("defaultShortcuts", QVariant::fromValue(info.defaultShortcuts()));
0288 
0289     d->sanityPropertizedShortcuts.insert(name);
0290 
0291     // TODO: KisShortcutsEditor overwrites shortcuts as you edit them, so we cannot know here
0292     // if the old shortcut is indeed "old" and must regenerate the tooltip unconditionally.
0293 
0294     QString plainTip = quietlyTranslate(getChild(info.xmlData, "toolTip"));
0295     if (action->shortcut().isEmpty()) {
0296         action->setToolTip(plainTip);
0297     } else {
0298         //qDebug() << "action with shortcut:" << name << action->shortcut();
0299         action->setToolTip(plainTip + " (" + action->shortcut().toString(QKeySequence::NativeText) + ")");
0300     }
0301 }
0302 
0303 bool KisActionRegistry::sanityCheckPropertized(const QString &name)
0304 {
0305     return d->sanityPropertizedShortcuts.contains(name);
0306 }
0307 
0308 QList<QString> KisActionRegistry::registeredShortcutIds() const
0309 {
0310     return d->actionInfoList.keys();
0311 }
0312 
0313 bool KisActionRegistry::propertizeAction(const QString &name, QAction * a)
0314 {
0315     if (!d->actionInfoList.contains(name)) {
0316         warnAction << "propertizeAction: No XML data found for action" << name;
0317         return false;
0318     }
0319 
0320     const ActionInfoItem info = d->actionInfo(name);
0321 
0322     QDomElement actionXml = info.xmlData;
0323     if (!actionXml.text().isEmpty()) {
0324         // i18n requires converting format from QString.
0325         auto getChildContent_i18n = [=](QString node){return quietlyTranslate(getChild(actionXml, node));};
0326 
0327         // Note: the fields in the .action documents marked for translation are determined by extractrc.
0328         QString icon      = getChildContent(actionXml, "icon");
0329         QString text      = getChildContent_i18n("text");
0330         QString whatsthis = getChildContent_i18n("whatsThis");
0331         // tooltip is set in updateShortcut() because shortcit gets appended to the tooltip
0332         //QString toolTip   = getChildContent_i18n("toolTip");
0333         QString statusTip = getChildContent_i18n("statusTip");
0334         QString iconText  = getChildContent_i18n("iconText");
0335         bool isCheckable  = getChildContent(actionXml, "isCheckable") == QString("true");
0336 
0337         a->setObjectName(name); // This is helpful, should be added more places in Krita
0338         if (!icon.isEmpty()) {
0339             a->setIcon(KisIconUtils::loadIcon(icon.toLatin1()));
0340             a->setProperty("iconName", QVariant::fromValue(icon)); // test
0341         }
0342         a->setText(text);
0343         a->setObjectName(name);
0344         a->setWhatsThis(whatsthis);
0345 
0346         a->setStatusTip(statusTip);
0347         a->setIconText(iconText);
0348         a->setCheckable(isCheckable);
0349     }
0350 
0351     updateShortcut(name, a);
0352     return true;
0353 }
0354 
0355 
0356 
0357 QString KisActionRegistry::getActionProperty(const QString &name, const QString &property)
0358 {
0359     ActionInfoItem info = d->actionInfo(name);
0360     QDomElement actionXml = info.xmlData;
0361     if (actionXml.text().isEmpty()) {
0362         dbgAction << "getActionProperty: No XML data found for action" << name;
0363         return QString();
0364     }
0365 
0366     return getChildContent(actionXml, property);
0367 
0368 }
0369 
0370 
0371 void KisActionRegistry::Private::loadActionFiles()
0372 {
0373     QStringList actionDefinitions =
0374         KoResourcePaths::findAllAssets("kis_actions", "*.action", KoResourcePaths::Recursive);
0375     dbgAction << "Action Definitions" << actionDefinitions;
0376 
0377     // Extract actions all XML .action files.
0378     Q_FOREACH (const QString &actionDefinition, actionDefinitions)  {
0379         QDomDocument doc;
0380         QFile f(actionDefinition);
0381         f.open(QFile::ReadOnly);
0382         doc.setContent(f.readAll());
0383 
0384         QDomElement base       = doc.documentElement(); // "ActionCollection" outer group
0385         QString collectionName = base.attribute("name");
0386         QString version        = base.attribute("version");
0387         if (version != "2") {
0388             qWarning() << ".action XML file" << actionDefinition << "has incorrect version; skipping.";
0389             continue;
0390         }
0391 
0392         // Loop over <Actions> nodes. Each of these corresponds to a
0393         // KisKActionCategory, producing a group of actions in the shortcut dialog.
0394         QDomElement actions = base.firstChild().toElement();
0395         while (!actions.isNull()) {
0396 
0397             // <text> field
0398             QDomElement categoryTextNode = actions.firstChild().toElement();
0399             QString categoryName         = quietlyTranslate(categoryTextNode);
0400 
0401             // <action></action> tags
0402             QDomElement actionXml  = categoryTextNode.nextSiblingElement();
0403 
0404             if (actionXml.isNull()) {
0405                 qWarning() << actionDefinition << "does not contain any valid actios! (Or the text element was left empty...)";
0406             }
0407 
0408             // Loop over individual actions
0409             while (!actionXml.isNull()) {
0410                 if (actionXml.tagName() == "Action") {
0411                     // Read name from format <Action name="save">
0412                     QString name      = actionXml.attribute("name");
0413 
0414                     // Bad things
0415                     if (name.isEmpty()) {
0416                         qWarning() << "Unnamed action in definitions file " << actionDefinition;
0417                     }
0418 
0419                     else if (actionInfoList.contains(name)) {
0420                         qWarning() << "NOT COOL: Duplicated action name from xml data: " << name;
0421                     }
0422 
0423                     else {
0424                         ActionInfoItem info;
0425                         info.xmlData         = actionXml;
0426 
0427                         // Use empty list to signify no shortcut
0428 #ifdef Q_OS_MACOS
0429                         QString shortcutText = getChildContentForOS(actionXml, "shortcut", "macos");
0430 #else
0431                         QString shortcutText = getChildContentForOS(actionXml, "shortcut");
0432 #endif
0433                         if (!shortcutText.isEmpty()) {
0434                             info.setDefaultShortcuts(QKeySequence::listFromString(shortcutText));
0435                         }
0436 
0437                         info.categoryName    = categoryName;
0438                         info.collectionName  = collectionName;
0439 
0440                         actionInfoList.insert(name,info);
0441                     }
0442                 }
0443                 actionXml = actionXml.nextSiblingElement();
0444             }
0445             actions = actions.nextSiblingElement();
0446         }
0447     }
0448 }
0449 
0450 void KisActionRegistry::Private::loadCustomShortcuts(QString filename)
0451 {
0452     const KConfigGroup localShortcuts(KSharedConfig::openConfig(filename),
0453                                       QStringLiteral("Shortcuts"));
0454 
0455     if (!localShortcuts.exists()) {
0456         return;
0457     }
0458 
0459     // Distinguish between two "null" states for custom shortcuts.
0460     for (auto i = actionInfoList.begin(); i != actionInfoList.end(); ++i) {
0461         if (localShortcuts.hasKey(i.key())) {
0462             QString entry = localShortcuts.readEntry(i.key(), QString());
0463             if (entry == QStringLiteral("none")) {
0464                 i.value().setCustomShortcuts(QList<QKeySequence>(), true);
0465             } else {
0466                 i.value().setCustomShortcuts(QKeySequence::listFromString(entry), false);
0467             }
0468         } else {
0469             i.value().setCustomShortcuts(QList<QKeySequence>(), false);
0470         }
0471     }
0472 }
0473 
0474 KisActionRegistry::ActionCategory::ActionCategory()
0475 {
0476 }
0477 
0478 KisActionRegistry::ActionCategory::ActionCategory(const QString &_componentName, const QString &_categoryName)
0479     : componentName(_componentName),
0480       categoryName(_categoryName),
0481       m_isValid(true)
0482 {
0483 }
0484 
0485 bool KisActionRegistry::ActionCategory::isValid() const
0486 {
0487     return m_isValid && !categoryName.isEmpty() && !componentName.isEmpty();
0488 }