kis_action_registry.cpp 13.5 KB
Newer Older
Michael Abrahams's avatar
Michael Abrahams committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
/*
 *  Copyright (c) 2015 Michael Abrahams <miabraha@gmail.com>
 *
 *  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 <QString>
#include <QHash>
#include <QGlobalStatic>
#include <QFile>
24 25 26 27
#include <QDomElement>
#include <KSharedConfig>
#include <klocalizedstring.h>
#include <KisShortcutsDialog.h>
Michael Abrahams's avatar
Michael Abrahams committed
28
#include <KConfigGroup>
Michael Abrahams's avatar
Michael Abrahams committed
29

30 31 32
#include "kis_debug.h"
#include "KoResourcePaths.h"
#include "kis_icon_utils.h"
Michael Abrahams's avatar
Michael Abrahams committed
33 34

#include "kis_action_registry.h"
35
#include "kshortcutschemeshelper_p.h"
Michael Abrahams's avatar
Michael Abrahams committed
36 37 38 39


namespace {

Michael Abrahams's avatar
Michael Abrahams committed
40 41 42 43 44 45 46 47 48 49
    /**
     * 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;
50

Michael Abrahams's avatar
Michael Abrahams committed
51 52
        QString      collectionName;
        QString      categoryName;
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

        inline QList<QKeySequence> defaultShortcuts() const {
            return m_defaultShortcuts;
        }

        inline void setDefaultShortcuts(const QList<QKeySequence> &value) {
            m_defaultShortcuts = value;
        }

        inline QList<QKeySequence> customShortcuts() const {
            return m_customShortcuts;
        }

        inline void setCustomShortcuts(const QList<QKeySequence> &value, bool explicitlyReset) {
            m_customShortcuts = value;
            m_explicitlyReset = explicitlyReset;
        }

        inline QList<QKeySequence> effectiveShortcuts() const {
            return m_customShortcuts.isEmpty() && !m_explicitlyReset ?
                m_defaultShortcuts : m_customShortcuts;
        }


    private:
        QList<QKeySequence> m_defaultShortcuts;
        QList<QKeySequence> m_customShortcuts;
        bool m_explicitlyReset = false;
Michael Abrahams's avatar
Michael Abrahams committed
81 82
    };

83 84 85
    // Convenience macros to extract text of a child node.
    QString getChildContent(QDomElement xml, QString node) {
        return xml.firstChildElement(node).text();
86
    }
Michael Abrahams's avatar
Michael Abrahams committed
87

88 89
    // Use Krita debug logging categories instead of KDE's default qDebug() for
    // harmless empty strings and translations
Michael Abrahams's avatar
Michael Abrahams committed
90 91 92 93
    QString quietlyTranslate(const QString &s) {
        if (s.isEmpty()) {
            return s;
        }
94
        QString translatedString = i18nc("action", s.toUtf8());
95 96 97
        if (translatedString == s) {
            translatedString = i18n(s.toUtf8());
        }
98
        if (translatedString.isEmpty()) {
Michael Abrahams's avatar
Michael Abrahams committed
99 100 101
            dbgAction << "No translation found for" << s;
            return s;
        }
102

103
        return translatedString;
104
    }
105
}
Michael Abrahams's avatar
Michael Abrahams committed
106 107 108 109 110 111 112



class Q_DECL_HIDDEN KisActionRegistry::Private
{
public:

113
    Private(KisActionRegistry *_q) : q(_q) {}
114

Michael Abrahams's avatar
Michael Abrahams committed
115 116
    // This is the main place containing ActionInfoItems.
    QMap<QString, ActionInfoItem> actionInfoList;
117
    void loadActionFiles();
Michael Abrahams's avatar
Michael Abrahams committed
118
    void loadCustomShortcuts(QString filename = QStringLiteral("kritashortcutsrc"));
Boudewijn Rempt's avatar
Boudewijn Rempt committed
119

Boudewijn Rempt's avatar
Boudewijn Rempt committed
120
    // XXX: this adds a default item for the given name to the list of actioninfo objects!
121 122 123 124 125
    ActionInfoItem &actionInfo(const QString &name) {
        if (!actionInfoList.contains(name)) {
            dbgAction << "Tried to look up info for unknown action" << name;
        }
        return actionInfoList[name];
126
    }
127 128

    KisActionRegistry *q;
129
    QSet<QString> sanityPropertizedShortcuts;
Michael Abrahams's avatar
Michael Abrahams committed
130 131 132
};


133
Q_GLOBAL_STATIC(KisActionRegistry, s_instance)
Michael Abrahams's avatar
Michael Abrahams committed
134 135 136

KisActionRegistry *KisActionRegistry::instance()
{
137 138 139
    if (!s_instance.exists()) {
        dbgRegistry << "initializing KoActionRegistry";
    }
Michael Abrahams's avatar
Michael Abrahams committed
140
    return s_instance;
141
}
Michael Abrahams's avatar
Michael Abrahams committed
142

Boudewijn Rempt's avatar
Boudewijn Rempt committed
143 144 145 146 147
bool KisActionRegistry::hasAction(const QString &name) const
{
    return d->actionInfoList.contains(name);
}

Michael Abrahams's avatar
Michael Abrahams committed
148 149

KisActionRegistry::KisActionRegistry()
150
    : d(new KisActionRegistry::Private(this))
Michael Abrahams's avatar
Michael Abrahams committed
151
{
152 153 154
    KConfigGroup cg = KSharedConfig::openConfig()->group("Shortcut Schemes");
    QString schemeName = cg.readEntry("Current Scheme", "Default");
    loadShortcutScheme(schemeName);
155
    loadCustomShortcuts();
Michael Abrahams's avatar
Michael Abrahams committed
156 157
}

158
KisActionRegistry::ActionCategory KisActionRegistry::fetchActionCategory(const QString &name) const
159
{
160
    if (!d->actionInfoList.contains(name)) return ActionCategory();
161

162 163 164
    const ActionInfoItem info = d->actionInfoList.value(name);
    return ActionCategory(info.collectionName, info.categoryName);
}
165

Michael Abrahams's avatar
Michael Abrahams committed
166 167 168
void KisActionRegistry::notifySettingsUpdated()
{
    d->loadCustomShortcuts();
169
}
170

171
void KisActionRegistry::loadCustomShortcuts()
172
{
173 174
    d->loadCustomShortcuts();
}
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191

void KisActionRegistry::loadShortcutScheme(const QString &schemeName)
{
    // Load scheme file
    if (schemeName != QStringLiteral("Default")) {
        QString schemeFileName = KShortcutSchemesHelper::schemeFileLocations().value(schemeName);
        if (schemeFileName.isEmpty()) {
            return;
        }
        KConfig schemeConfig(schemeFileName, KConfig::SimpleConfig);
        applyShortcutScheme(&schemeConfig);
    } else {
        // Apply default scheme, updating KisActionRegistry data
        applyShortcutScheme();
    }
}

Michael Abrahams's avatar
Michael Abrahams committed
192
QAction * KisActionRegistry::makeQAction(const QString &name, QObject *parent)
193 194 195
{
    QAction * a = new QAction(parent);
    if (!d->actionInfoList.contains(name)) {
196 197
        qWarning() << "Warning: requested data for unknown action" << name;
        a->setObjectName(name);
198 199
        return a;
    }
200

201 202
    propertizeAction(name, a);
    return a;
203
}
204

205 206 207
void KisActionRegistry::settingsPageSaved()
{
   // For now, custom shortcuts are dealt with by writing to file and reloading.
208
   loadCustomShortcuts();
209

210
   // Announce UI should reload current shortcuts.
211
   emit shortcutsUpdated();
212 213 214
}


215 216 217
void KisActionRegistry::applyShortcutScheme(const KConfigBase *config)
{
    // First, update the things in KisActionRegistry
218 219 220
    d->actionInfoList.clear();
    d->loadActionFiles();

221
    if (config == 0) {
222 223
        // Use default shortcut scheme. Simplest just to reload everything.
        loadCustomShortcuts();
224 225 226 227 228 229
    } 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());
230
            info.setDefaultShortcuts(QKeySequence::listFromString(it.value()));
231 232 233 234 235
            it++;
        }
    }
}

Michael Abrahams's avatar
Michael Abrahams committed
236 237
void KisActionRegistry::updateShortcut(const QString &name, QAction *action)
{
Michael Abrahams's avatar
Michael Abrahams committed
238
    const ActionInfoItem &info = d->actionInfo(name);
239 240
    action->setShortcuts(info.effectiveShortcuts());
    action->setProperty("defaultShortcuts", qVariantFromValue(info.defaultShortcuts()));
241 242 243 244 245 246 247

    d->sanityPropertizedShortcuts.insert(name);
}

bool KisActionRegistry::sanityCheckPropertized(const QString &name)
{
    return d->sanityPropertizedShortcuts.contains(name);
Michael Abrahams's avatar
Michael Abrahams committed
248
}
249

250 251 252 253 254
QList<QString> KisActionRegistry::registeredShortcutIds() const
{
    return d->actionInfoList.keys();
}

Michael Abrahams's avatar
Michael Abrahams committed
255
bool KisActionRegistry::propertizeAction(const QString &name, QAction * a)
256
{
257
    if (!d->actionInfoList.contains(name)) {
Boudewijn Rempt's avatar
Boudewijn Rempt committed
258
        warnAction << "No XML data found for action" << name;
Michael Abrahams's avatar
Michael Abrahams committed
259 260
        return false;
    }
261

262
    const ActionInfoItem info = d->actionInfo(name);
263

264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
    QDomElement actionXml = info.xmlData;
    if (!actionXml.text().isEmpty()) {
        // 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
279 280 281
        if (!icon.isEmpty()) {
            a->setIcon(KisIconUtils::loadIcon(icon.toLatin1()));
        }
282 283 284 285 286 287 288 289
        a->setText(text);
        a->setObjectName(name);
        a->setWhatsThis(whatsthis);
        a->setToolTip(toolTip);
        a->setStatusTip(statusTip);
        a->setIconText(iconText);
        a->setCheckable(isCheckable);
    }
290

291
    updateShortcut(name, a);
292 293 294 295 296
    return true;
}



297 298 299 300 301 302 303 304 305 306 307 308
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);

}
309 310 311


void KisActionRegistry::Private::loadActionFiles()
Michael Abrahams's avatar
Michael Abrahams committed
312 313
{
    QStringList actionDefinitions =
314
        KoResourcePaths::findAllResources("kis_actions", "*.action", KoResourcePaths::Recursive);
Michael Abrahams's avatar
Michael Abrahams committed
315 316

    // Extract actions all XML .action files.
317
    Q_FOREACH (const QString &actionDefinition, actionDefinitions)  {
Michael Abrahams's avatar
Michael Abrahams committed
318 319 320 321 322
        QDomDocument doc;
        QFile f(actionDefinition);
        f.open(QFile::ReadOnly);
        doc.setContent(f.readAll());

Michael Abrahams's avatar
Michael Abrahams committed
323 324 325 326 327 328 329
        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;
        }
Michael Abrahams's avatar
Michael Abrahams committed
330

Michael Abrahams's avatar
Michael Abrahams committed
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
        // Loop over <Actions> 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()) {

            // <text> field
            QDomElement categoryTextNode = actions.firstChild().toElement();
            QString categoryName         = quietlyTranslate(categoryTextNode.text());

            // <action></action> tags
            QDomElement actionXml  = categoryTextNode.nextSiblingElement();

            // Loop over individual actions
            while (!actionXml.isNull()) {
                if (actionXml.tagName() == "Action") {
                    // Read name from format <Action name="save">
                    QString name      = actionXml.attribute("name");

                    // Bad things
                    if (name.isEmpty()) {
                        errAction << "Unnamed action in definitions file " << actionDefinition;
                    }

                    else if (actionInfoList.contains(name)) {
Boudewijn Rempt's avatar
Boudewijn Rempt committed
355
                        qWarning() << "NOT COOL: Duplicated action name from xml data: " << name;
Michael Abrahams's avatar
Michael Abrahams committed
356 357 358 359 360
                    }

                    else {
                        ActionInfoItem info;
                        info.xmlData         = actionXml;
361 362 363

                        // Use empty list to signify no shortcut
                        QString shortcutText = getChildContent(actionXml, "shortcut");
364 365 366
                        if (!shortcutText.isEmpty()) {
                            info.setDefaultShortcuts(QKeySequence::listFromString(shortcutText));
                        }
367

Michael Abrahams's avatar
Michael Abrahams committed
368 369 370 371 372
                        info.categoryName    = categoryName;
                        info.collectionName  = collectionName;

                        actionInfoList.insert(name,info);
                    }
Michael Abrahams's avatar
Michael Abrahams committed
373
                }
Michael Abrahams's avatar
Michael Abrahams committed
374 375 376 377 378
                actionXml = actionXml.nextSiblingElement();
            }
            actions = actions.nextSiblingElement();
        }
    }
379
}
Michael Abrahams's avatar
Michael Abrahams committed
380

Michael Abrahams's avatar
Michael Abrahams committed
381 382
void KisActionRegistry::Private::loadCustomShortcuts(QString filename)
{
383
    const KConfigGroup localShortcuts(KSharedConfig::openConfig(filename),
Michael Abrahams's avatar
Michael Abrahams committed
384
                                      QStringLiteral("Shortcuts"));
Michael Abrahams's avatar
Michael Abrahams committed
385

Michael Abrahams's avatar
Michael Abrahams committed
386 387 388
    if (!localShortcuts.exists()) {
        return;
    }
Michael Abrahams's avatar
Michael Abrahams committed
389

390
    // Distinguish between two "null" states for custom shortcuts.
Michael Abrahams's avatar
Michael Abrahams committed
391 392 393
    for (auto i = actionInfoList.begin(); i != actionInfoList.end(); ++i) {
        if (localShortcuts.hasKey(i.key())) {
            QString entry = localShortcuts.readEntry(i.key(), QString());
394
            if (entry == QStringLiteral("none")) {
395
                i.value().setCustomShortcuts(QList<QKeySequence>(), true);
396
            } else {
397
                i.value().setCustomShortcuts(QKeySequence::listFromString(entry), false);
398
            }
399
        } else {
400
            i.value().setCustomShortcuts(QList<QKeySequence>(), false);
Michael Abrahams's avatar
Michael Abrahams committed
401 402
        }
    }
403
}
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419

KisActionRegistry::ActionCategory::ActionCategory()
{
}

KisActionRegistry::ActionCategory::ActionCategory(const QString &_componentName, const QString &_categoryName)
    : componentName(_componentName),
      categoryName(_categoryName),
      m_isValid(true)
{
}

bool KisActionRegistry::ActionCategory::isValid() const
{
    return m_isValid && !categoryName.isEmpty() && !componentName.isEmpty();
}