Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_action_registry.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2015 Michael Abrahams <miabraha@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7
8#include <QString>
9#include <QGlobalStatic>
10#include <QFile>
11#include <QFileInfo>
12#include <QDomElement>
13#include <KSharedConfig>
14#include <klocalizedstring.h>
15#include <KisShortcutsDialog.h>
16#include <KConfigGroup>
17#include <qdom.h>
18
19#include "kis_debug.h"
20#include "KoResourcePaths.h"
21#include "kis_icon_utils.h"
22
23#include "kis_action_registry.h"
25
26
27namespace {
28
37 struct ActionInfoItem {
38 QDomElement xmlData;
39
40 QString collectionName;
41 QString categoryName;
42
43 inline QList<QKeySequence> defaultShortcuts() const {
44 return m_defaultShortcuts;
45 }
46
47 inline void setDefaultShortcuts(const QList<QKeySequence> &value) {
48 m_defaultShortcuts = value;
49 }
50
51 inline QList<QKeySequence> customShortcuts() const {
52 return m_customShortcuts;
53 }
54
55 inline void setCustomShortcuts(const QList<QKeySequence> &value, bool explicitlyReset) {
56 m_customShortcuts = value;
57 m_explicitlyReset = explicitlyReset;
58 }
59
60 inline QList<QKeySequence> effectiveShortcuts() const {
61 return m_customShortcuts.isEmpty() && !m_explicitlyReset ?
62 m_defaultShortcuts : m_customShortcuts;
63 }
64
65
66 private:
67 QList<QKeySequence> m_defaultShortcuts;
68 QList<QKeySequence> m_customShortcuts;
69 bool m_explicitlyReset = false;
70 };
71
72 // Convenience macros to extract a child node.
73 QDomElement getChild(QDomElement xml, QString node) {
74 return xml.firstChildElement(node);
75 }
76
77 // Convenience macros to extract text of a child node.
78 QString getChildContent(QDomElement xml, QString node) {
79 return xml.firstChildElement(node).text();
80 }
81
82 QString getChildContentForOS(QDomElement xml, QString tagName, QString os = QString()) {
83 bool found = false;
84
85 QDomElement node = xml.firstChildElement(tagName);
86 QDomElement nodeElse;
87
88 while(!found && !node.isNull()) {
89 if (node.attribute("operatingSystem") == os) {
90 found = true;
91 break;
92 }
93 else if (node.hasAttribute("operatingSystemElse")) {
94 nodeElse = node;
95 }
96 else if (nodeElse.isNull()) {
97 nodeElse = node;
98 }
99
100 node = node.nextSiblingElement(tagName);
101 }
102
103 if (!found && !nodeElse.isNull()) {
104 return nodeElse.text();
105 }
106 return node.text();
107 }
108
109 // Use Krita debug logging categories instead of KDE's default qDebug() for
110 // harmless empty strings and translations
111 QString quietlyTranslate(const QDomElement &s) {
112 if (s.isNull() || s.text().isEmpty()) {
113 return QString();
114 }
115 QString translatedString;
116 const QString attrContext = QStringLiteral("context");
117 const QString attrDomain = QStringLiteral("translationDomain");
118 QString context = QStringLiteral("action");
119
120 if (!s.attribute(attrContext).isEmpty()) {
121 context = s.attribute(attrContext);
122 }
123
124 QByteArray domain = s.attribute(attrDomain).toUtf8();
125 if (domain.isEmpty()) {
126 domain = s.ownerDocument().documentElement().attribute(attrDomain).toUtf8();
127 if (domain.isEmpty()) {
128 domain = KLocalizedString::applicationDomain();
129 }
130 }
131 translatedString = i18ndc(domain.constData(), context.toUtf8().constData(), s.text().toUtf8().constData());
132 if (translatedString == s.text()) {
133 translatedString = i18n(s.text().toUtf8().constData());
134 }
135 if (translatedString.isEmpty()) {
136 dbgAction << "No translation found for" << s.text();
137 return s.text();
138 }
139
140 return translatedString;
141 }
142}
143
144
145
146class Q_DECL_HIDDEN KisActionRegistry::Private
147{
148public:
149
151
152 // This is the main place containing ActionInfoItems.
153 QMap<QString, ActionInfoItem> actionInfoList;
155 void loadCustomShortcuts(QString filename = QStringLiteral("kritashortcutsrc"));
156
157 // XXX: this adds a default item for the given name to the list of actionInfo objects!
158 ActionInfoItem &actionInfo(const QString &name) {
159 if (!actionInfoList.contains(name)) {
160 dbgAction << "Tried to look up info for unknown action" << name;
161 }
162 return actionInfoList[name];
163 }
164
167};
168
169
171
173{
174 if (!s_instance.exists()) {
175 dbgRegistry << "initializing KoActionRegistry";
176 }
177 return s_instance;
178}
179
180bool KisActionRegistry::hasAction(const QString &name) const
181{
182 return d->actionInfoList.contains(name);
183}
184
185
187 : d(new KisActionRegistry::Private(this))
188{
189 KConfigGroup cg = KSharedConfig::openConfig()->group("Shortcut Schemes");
190 QString schemeName = cg.readEntry("Current Scheme", "Default");
191 QString schemeFileName = KisKShortcutSchemesHelper::schemeFileLocations().value(schemeName);
192 if (!QFileInfo(schemeFileName).exists()) {
193 schemeName = "Default";
194 }
195 loadShortcutScheme(schemeName);
197}
198
202
204{
205 if (!d->actionInfoList.contains(name)) return ActionCategory();
206
207 const ActionInfoItem info = d->actionInfoList.value(name);
208 return ActionCategory(info.collectionName, info.categoryName);
209}
210
212{
213 d->loadCustomShortcuts();
214}
215
217{
218 d->loadCustomShortcuts();
219}
220
221void KisActionRegistry::loadShortcutScheme(const QString &schemeName)
222{
223 // Load scheme file
224 if (schemeName != QStringLiteral("Default")) {
225 QString schemeFileName = KisKShortcutSchemesHelper::schemeFileLocations().value(schemeName);
226 if (schemeFileName.isEmpty() || !QFileInfo(schemeFileName).exists()) {
228 return;
229 }
230 KConfig schemeConfig(schemeFileName, KConfig::SimpleConfig);
231 applyShortcutScheme(&schemeConfig);
232 } else {
233 // Apply default scheme, updating KisActionRegistry data
235 }
236}
237
238QAction * KisActionRegistry::makeQAction(const QString &name, QObject *parent)
239{
240 QAction * a = new QAction(parent);
241 if (!d->actionInfoList.contains(name)) {
242 qWarning() << "Warning: requested data for unknown action" << name;
243 a->setObjectName(name);
244 return a;
245 }
246
247 propertizeAction(name, a);
248 return a;
249}
250
252{
253 // For now, custom shortcuts are dealt with by writing to file and reloading.
255
256 // Announce UI should reload current shortcuts.
257 Q_EMIT shortcutsUpdated();
258}
259
260
261void KisActionRegistry::applyShortcutScheme(const KConfigBase *config)
262{
263 // First, update the things in KisActionRegistry
264 d->actionInfoList.clear();
265 d->loadActionFiles();
266
267 if (config == 0) {
268 // Use default shortcut scheme. Simplest just to reload everything.
270 } else {
271 const auto schemeEntries = config->group(QStringLiteral("Shortcuts")).entryMap();
272 // Load info item for each shortcut, reset custom shortcuts
273 auto it = schemeEntries.constBegin();
274 while (it != schemeEntries.end()) {
275 ActionInfoItem &info = d->actionInfo(it.key());
276 info.setDefaultShortcuts(QKeySequence::listFromString(it.value()));
277 it++;
278 }
279 }
280}
281
282void KisActionRegistry::updateShortcut(const QString &name, QAction *action)
283{
284 const ActionInfoItem &info = d->actionInfo(name);
285 action->setShortcuts(info.effectiveShortcuts());
286 action->setProperty("defaultShortcuts", QVariant::fromValue(info.defaultShortcuts()));
287
288 d->sanityPropertizedShortcuts.insert(name);
289
290 // TODO: KisShortcutsEditor overwrites shortcuts as you edit them, so we cannot know here
291 // if the old shortcut is indeed "old" and must regenerate the tooltip unconditionally.
292
293 QString plainTip = quietlyTranslate(getChild(info.xmlData, "toolTip"));
294 if (action->shortcut().isEmpty()) {
295 action->setToolTip(plainTip);
296 } else {
297 //qDebug() << "action with shortcut:" << name << action->shortcut();
298 action->setToolTip(plainTip + " (" + action->shortcut().toString(QKeySequence::NativeText) + ")");
299 }
300}
301
303{
304 return d->sanityPropertizedShortcuts.contains(name);
305}
306
308{
309 return d->actionInfoList.keys();
310}
311
312bool KisActionRegistry::propertizeAction(const QString &name, QAction * a)
313{
314 if (!d->actionInfoList.contains(name)) {
315 warnAction << "propertizeAction: No XML data found for action" << name;
316 return false;
317 }
318
319 const ActionInfoItem info = d->actionInfo(name);
320
321 QDomElement actionXml = info.xmlData;
322 if (!actionXml.text().isEmpty()) {
323 // i18n requires converting format from QString.
324 auto getChildContent_i18n = [=](QString node){return quietlyTranslate(getChild(actionXml, node));};
325
326 // Note: the fields in the .action documents marked for translation are determined by extractrc.
327 QString icon = getChildContent(actionXml, "icon");
328 QString text = getChildContent_i18n("text");
329 QString whatsthis = getChildContent_i18n("whatsThis");
330 // tooltip is set in updateShortcut() because shortcut gets appended to the tooltip
331 //QString toolTip = getChildContent_i18n("toolTip");
332 QString statusTip = getChildContent_i18n("statusTip");
333 QString iconText = getChildContent_i18n("iconText");
334 bool isCheckable = getChildContent(actionXml, "isCheckable") == QString("true");
335
336 a->setObjectName(name); // This is helpful, should be added more places in Krita
337 if (!icon.isEmpty()) {
338 a->setIcon(KisIconUtils::loadIcon(icon.toLatin1()));
339 a->setProperty("iconName", QVariant::fromValue(icon)); // test
340 }
341 a->setText(text);
342 a->setObjectName(name);
343 a->setWhatsThis(whatsthis);
344
345 a->setStatusTip(statusTip);
346 a->setIconText(iconText);
347 a->setCheckable(isCheckable);
348 }
349
350 updateShortcut(name, a);
351 return true;
352}
353
354
355
356QString KisActionRegistry::getActionProperty(const QString &name, const QString &property)
357{
358 ActionInfoItem info = d->actionInfo(name);
359 QDomElement actionXml = info.xmlData;
360 if (actionXml.text().isEmpty()) {
361 dbgAction << "getActionProperty: No XML data found for action" << name;
362 return QString();
363 }
364
365 return getChildContent(actionXml, property);
366
367}
368
369
370void KisActionRegistry::Private::loadActionFiles()
371{
372 QStringList actionDefinitions =
374 dbgAction << "Action Definitions" << actionDefinitions;
375
376 // Extract actions all XML .action files.
377 Q_FOREACH (const QString &actionDefinition, actionDefinitions) {
378 QDomDocument doc;
379 QFile f(actionDefinition);
380 f.open(QFile::ReadOnly);
381 doc.setContent(f.readAll());
382
383 QDomElement base = doc.documentElement(); // "ActionCollection" outer group
384 QString collectionName = base.attribute("name");
385 QString version = base.attribute("version");
386 if (version != "2") {
387 qWarning() << ".action XML file" << actionDefinition << "has incorrect version; skipping.";
388 continue;
389 }
390
391 // Loop over <Actions> nodes. Each of these corresponds to a
392 // KisKActionCategory, producing a group of actions in the shortcut dialog.
393 QDomElement actions = base.firstChild().toElement();
394 while (!actions.isNull()) {
395
396 // <text> field
397 QDomElement categoryTextNode = actions.firstChild().toElement();
398 QString categoryName = quietlyTranslate(categoryTextNode);
399
400 // <action></action> tags
401 QDomElement actionXml = categoryTextNode.nextSiblingElement();
402
403 if (actionXml.isNull()) {
404 qWarning() << actionDefinition << "does not contain any valid actions! (Or the text element was left empty...)";
405 }
406
407 // Loop over individual actions
408 while (!actionXml.isNull()) {
409 if (actionXml.tagName() == "Action") {
410 // Read name from format <Action name="save">
411 QString name = actionXml.attribute("name");
412
413 // Bad things
414 if (name.isEmpty()) {
415 qWarning() << "Unnamed action in definitions file " << actionDefinition;
416 }
417
418 else if (actionInfoList.contains(name)) {
419 qWarning() << "NOT COOL: Duplicated action name from xml data: " << name;
420 }
421
422 else {
423 ActionInfoItem info;
424 info.xmlData = actionXml;
425
426 // Use empty list to signify no shortcut
427#ifdef Q_OS_MACOS
428 QString shortcutText = getChildContentForOS(actionXml, "shortcut", "macos");
429#else
430 QString shortcutText = getChildContentForOS(actionXml, "shortcut");
431#endif
432 if (!shortcutText.isEmpty()) {
433 info.setDefaultShortcuts(QKeySequence::listFromString(shortcutText));
434 }
435
436 info.categoryName = categoryName;
437 info.collectionName = collectionName;
438
439 actionInfoList.insert(name,info);
440 }
441 }
442 actionXml = actionXml.nextSiblingElement();
443 }
444 actions = actions.nextSiblingElement();
445 }
446 }
447}
448
449void KisActionRegistry::Private::loadCustomShortcuts(QString filename)
450{
451 const KConfigGroup localShortcuts(KSharedConfig::openConfig(filename),
452 QStringLiteral("Shortcuts"));
453
454 if (!localShortcuts.exists()) {
455 return;
456 }
457
458 // Distinguish between two "null" states for custom shortcuts.
459 for (auto i = actionInfoList.begin(); i != actionInfoList.end(); ++i) {
460 if (localShortcuts.hasKey(i.key())) {
461 QString entry = localShortcuts.readEntry(i.key(), QString());
462 if (entry == QStringLiteral("none")) {
463 i.value().setCustomShortcuts(QList<QKeySequence>(), true);
464 } else {
465 i.value().setCustomShortcuts(QKeySequence::listFromString(entry), false);
466 }
467 } else {
468 i.value().setCustomShortcuts(QList<QKeySequence>(), false);
469 }
470 }
471}
472
476
477KisActionRegistry::ActionCategory::ActionCategory(const QString &_componentName, const QString &_categoryName)
478 : componentName(_componentName),
479 categoryName(_categoryName),
480 m_isValid(true)
481{
482}
483
485{
486 return m_isValid && !categoryName.isEmpty() && !componentName.isEmpty();
487}
float value(const T *src, size_t ch)
Q_GLOBAL_STATIC(KisStoragePluginRegistry, s_instance)
PythonPluginManager * instance
bool hasAction(const QString &name) const
QMap< QString, ActionInfoItem > actionInfoList
QList< QString > registeredShortcutIds() const
void updateShortcut(const QString &name, QAction *ac)
void applyShortcutScheme(const KConfigBase *config=0)
QAction * makeQAction(const QString &name, QObject *parent=0)
KisActionRegistry * q
ActionCategory fetchActionCategory(const QString &name) const
bool propertizeAction(const QString &name, QAction *a)
void loadCustomShortcuts(QString filename=QStringLiteral("kritashortcutsrc"))
void loadShortcutScheme(const QString &schemeName)
loadShortcutScheme
ActionInfoItem & actionInfo(const QString &name)
const QScopedPointer< Private > d
QString getActionProperty(const QString &name, const QString &property)
bool sanityCheckPropertized(const QString &name)
Private(KisActionRegistry *_q)
QSet< QString > sanityPropertizedShortcuts
static QHash< QString, QString > schemeFileLocations()
static QStringList findAllAssets(const QString &type, const QString &filter=QString(), SearchOptions options=NoSearchOptions)
#define dbgAction
Definition kis_debug.h:58
#define dbgRegistry
Definition kis_debug.h:47
#define warnAction
Definition kis_debug.h:100
const char * name(StandardAction id)
QIcon loadIcon(const QString &name)