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 }