Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

kis_action_registry.cpp 14.6 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 33
#include "kis_debug.h"
#include "KoResourcePaths.h"
#include "kis_icon_utils.h"
#include "kactioncollection.h"
Michael Abrahams's avatar
Michael Abrahams committed
34
#include "kactioncategory.h"
Michael Abrahams's avatar
Michael Abrahams committed
35 36 37


#include "kis_action_registry.h"
38
#include "kshortcutschemeshelper_p.h"
Michael Abrahams's avatar
Michael Abrahams committed
39 40 41 42


namespace {

Michael Abrahams's avatar
Michael Abrahams committed
43 44 45 46 47 48 49 50 51 52
    /**
     * 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;
Michael Abrahams's avatar
Michael Abrahams committed
53 54
        QKeySequence defaultShortcut;
        QKeySequence customShortcut;
Michael Abrahams's avatar
Michael Abrahams committed
55 56
        QString      collectionName;
        QString      categoryName;
Michael Abrahams's avatar
Michael Abrahams committed
57 58
    };

59 60 61
    // Convenience macros to extract text of a child node.
    QString getChildContent(QDomElement xml, QString node) {
        return xml.firstChildElement(node).text();
Michael Abrahams's avatar
Michael Abrahams committed
62
    };
63

Michael Abrahams's avatar
Michael Abrahams committed
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
    ActionInfoItem emptyActionInfo;  // Used as default return value


    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;
        } else {
            return action.customShortcut;
        }
    };

Michael Abrahams's avatar
Michael Abrahams committed
87 88 89 90 91 92 93 94
};



class Q_DECL_HIDDEN KisActionRegistry::Private
{
public:

95 96
    Private(KisActionRegistry *_q) : q(_q) {};

Michael Abrahams's avatar
Michael Abrahams committed
97 98
    // This is the main place containing ActionInfoItems.
    QMap<QString, ActionInfoItem> actionInfoList;
99 100
    void loadActionFiles();
    void loadActionCollections();
Michael Abrahams's avatar
Michael Abrahams committed
101
    void loadCustomShortcuts(QString filename = QStringLiteral("kritashortcutsrc"));
102 103 104 105 106
    ActionInfoItem &actionInfo(const QString &name) {
        if (!actionInfoList.contains(name)) {
            dbgAction << "Tried to look up info for unknown action" << name;
        }
        return actionInfoList[name];
Michael Abrahams's avatar
Michael Abrahams committed
107
    };
108 109 110

    KisActionRegistry *q;
    KActionCollection * defaultActionCollection;
Michael Abrahams's avatar
Michael Abrahams committed
111
    QMap<QString, KActionCollection*> actionCollections;
Michael Abrahams's avatar
Michael Abrahams committed
112 113 114 115 116 117 118 119 120 121 122 123
};


Q_GLOBAL_STATIC(KisActionRegistry, s_instance);

KisActionRegistry *KisActionRegistry::instance()
{
    return s_instance;
};


KisActionRegistry::KisActionRegistry()
124
    : d(new KisActionRegistry::Private(this))
Michael Abrahams's avatar
Michael Abrahams committed
125
{
126
    d->loadActionFiles();
127 128 129 130 131

    KConfigGroup cg = KSharedConfig::openConfig()->group("Shortcut Schemes");
    QString schemeName = cg.readEntry("Current Scheme", "Default");
    loadShortcutScheme(schemeName);

Michael Abrahams's avatar
Michael Abrahams committed
132
    d->loadCustomShortcuts();
133 134

    KoResourcePaths::addResourceType("kis_shortcuts", "data", "krita/shortcuts/");
Michael Abrahams's avatar
Michael Abrahams committed
135 136
}

137 138 139 140 141
QKeySequence KisActionRegistry::getCustomShortcut(const QString &name)
{
    return d->actionInfo(name).customShortcut;
};

Michael Abrahams's avatar
Michael Abrahams committed
142
QKeySequence KisActionRegistry::getPreferredShortcut(const QString &name)
Michael Abrahams's avatar
Michael Abrahams committed
143
{
Michael Abrahams's avatar
Michael Abrahams committed
144
    return preferredShortcut(d->actionInfo(name));
Michael Abrahams's avatar
Michael Abrahams committed
145 146
};

Michael Abrahams's avatar
Michael Abrahams committed
147
QKeySequence KisActionRegistry::getCategory(const QString &name)
Michael Abrahams's avatar
Michael Abrahams committed
148
{
Michael Abrahams's avatar
Michael Abrahams committed
149
    return d->actionInfo(name).categoryName;
Michael Abrahams's avatar
Michael Abrahams committed
150 151 152 153 154 155 156
};

QStringList KisActionRegistry::allActions()
{
    return d->actionInfoList.keys();
};

157 158
KActionCollection * KisActionRegistry::getDefaultCollection()
{
Michael Abrahams's avatar
Michael Abrahams committed
159
    return d->actionCollections.value("Krita");
160 161
};

Michael Abrahams's avatar
Michael Abrahams committed
162
void KisActionRegistry::addAction(const QString &name, QAction *a)
163
{
Michael Abrahams's avatar
Michael Abrahams committed
164
    auto info = d->actionInfo(name);
165

Michael Abrahams's avatar
Michael Abrahams committed
166 167
    KActionCollection *collection = d->actionCollections.value(info.collectionName);
    if (!collection) {
168
        dbgAction << "No collection found for action" << name;
Michael Abrahams's avatar
Michael Abrahams committed
169 170 171 172
        return;
    }
    if (collection->action(name)) {
        dbgAction << "duplicate action" << name << "in collection" << collection->componentName();
173 174 175
    }
    else {
    }
Michael Abrahams's avatar
Michael Abrahams committed
176
    collection->addCategorizedAction(name, a, info.categoryName);
177 178 179
};


Michael Abrahams's avatar
Michael Abrahams committed
180 181 182 183
void KisActionRegistry::notifySettingsUpdated()
{
    d->loadCustomShortcuts();
};
184

185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
void KisActionRegistry::loadCustomShortcuts()
{
    d->loadCustomShortcuts();
};

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();
    }
}

Michael Abrahams's avatar
Michael Abrahams committed
207
QAction * KisActionRegistry::makeQAction(const QString &name, QObject *parent)
208 209 210 211 212 213 214 215 216 217 218 219
{

    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;
};


220
void KisActionRegistry::configureShortcuts()
221 222
{
    KisShortcutsDialog dlg;
Michael Abrahams's avatar
Michael Abrahams committed
223

224 225
    for (auto i = d->actionCollections.constBegin(); i != d->actionCollections.constEnd(); i++ ) {
        dlg.addCollection(i.value(), i.key());
226 227
    }

Michael Abrahams's avatar
Michael Abrahams committed
228 229 230 231 232 233 234
    /* Testing */
    // QStringList mainWindowActions;
    // foreach (auto a, ac->actions()) {
    //     mainWindowActions << a->objectName();
    // }
    // dlg.addCollection(ac, "TESTING: XMLGUI-MAINWINDOW");

235
   dlg.configure();  // Show the dialog.
Michael Abrahams's avatar
Michael Abrahams committed
236 237

   d->loadCustomShortcuts();
238 239

   emit shortcutsUpdated();
240 241 242
}


243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
void KisActionRegistry::applyShortcutScheme(const KConfigBase *config)
{
    // First, update the things in KisActionRegistry
    if (config == 0) {
        // Simplest just to reload everything
        d->loadActionFiles();
    } 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();
            it++;
        }
    }
}

Michael Abrahams's avatar
Michael Abrahams committed
261 262
void KisActionRegistry::updateShortcut(const QString &name, QAction *action)
{
263 264 265 266
    const ActionInfoItem info = d->actionInfo(name);
    action->setShortcut(preferredShortcut(info));
    auto propertizedShortcut = qVariantFromValue(QList<QKeySequence>() << info.defaultShortcut);
    action->setProperty("defaultShortcuts", propertizedShortcut);
Michael Abrahams's avatar
Michael Abrahams committed
267
}
268

Michael Abrahams's avatar
Michael Abrahams committed
269 270

bool KisActionRegistry::propertizeAction(const QString &name, QAction * a)
271 272
{

273
    const ActionInfoItem info = d->actionInfo(name);
Michael Abrahams's avatar
Michael Abrahams committed
274 275 276 277 278
    QDomElement actionXml = info.xmlData;
    if (actionXml.text().isEmpty()) {
        dbgAction << "No XML data found for action" << name;
        return false;
    }
279 280 281


    // i18n requires converting format from QString.
282
    auto getChildContent_i18n = [=](QString node){return quietlyTranslate(getChildContent(actionXml, node));};
Michael Abrahams's avatar
Michael Abrahams committed
283 284

    // Note: the fields in the .action documents marked for translation are determined by extractrc.
285 286 287 288 289 290 291
    QString icon      = getChildContent(actionXml, "icon");
    QString text      = getChildContent(actionXml, "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");
292 293 294 295




Michael Abrahams's avatar
Michael Abrahams committed
296
    a->setObjectName(name); // This is helpful, should be added more places in Krita
297 298 299 300 301 302 303 304 305
    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);

306
    updateShortcut(name, a);
307 308 309



310
    // TODO: check for colliding shortcuts, either here, or in loading code
Boudewijn Rempt's avatar
Boudewijn Rempt committed
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
#if 0
     QMap<QKeySequence, QAction*> 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
328 329 330 331 332 333

    return true;
}



334 335 336 337 338 339 340 341 342 343 344 345
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);

}
346 347


348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
void KisActionRegistry::writeCustomShortcuts() const
{
    KConfigGroup cg(KSharedConfig::openConfig("kritashortcutsrc"),
                    QStringLiteral("Shortcuts"));

    QList<QAction *> writeActions;
    for (auto it = d->actionInfoList.constBegin();
         it != d->actionInfoList.constEnd(); ++it) {

        QString actionName = it.key();
        QString s = it.value().customShortcut.toString();
        if (s.isEmpty()) {
            cg.deleteEntry(actionName, KConfigGroup::Persistent);
        } else {
            cg.writeEntry(actionName, s, KConfigGroup::Persistent);
        }
    }
    cg.sync();
}

368
void KisActionRegistry::Private::loadActionFiles()
Michael Abrahams's avatar
Michael Abrahams committed
369 370 371 372 373 374 375 376
{

    KoResourcePaths::addResourceType("kis_actions", "data", "krita/actions");
    auto searchType = KoResourcePaths::Recursive | KoResourcePaths::NoDuplicates;
    QStringList actionDefinitions =
        KoResourcePaths::findAllResources("kis_actions", "*.action", searchType);

    // Extract actions all XML .action files.
377
    Q_FOREACH (const QString &actionDefinition, actionDefinitions)  {
Michael Abrahams's avatar
Michael Abrahams committed
378 379 380 381 382
        QDomDocument doc;
        QFile f(actionDefinition);
        f.open(QFile::ReadOnly);
        doc.setContent(f.readAll());

Michael Abrahams's avatar
Michael Abrahams committed
383 384 385 386 387 388 389
        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
390 391


Michael Abrahams's avatar
Michael Abrahams committed
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
        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 <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());
409 410
            // KActionCategory *category    = actionCollection->getCategory(categoryName);
            // dbgAction << "Using category" << categoryName;
Michael Abrahams's avatar
Michael Abrahams committed
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432

            // <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)) {
                        // errAction << "NOT COOL: Duplicated action name from xml data: " << name;
                    }

                    else {
                        ActionInfoItem info;
                        info.xmlData         = actionXml;
433
                        info.defaultShortcut = getChildContent(actionXml, "shortcut");
Michael Abrahams's avatar
Michael Abrahams committed
434 435 436 437 438 439 440
                        info.customShortcut  = QKeySequence();
                        info.categoryName    = categoryName;
                        info.collectionName  = collectionName;

                        // dbgAction << "default shortcut for" << name << " - " << info.defaultShortcut;
                        actionInfoList.insert(name,info);
                    }
Michael Abrahams's avatar
Michael Abrahams committed
441 442

                }
Michael Abrahams's avatar
Michael Abrahams committed
443 444 445 446 447 448
                actionXml = actionXml.nextSiblingElement();
            }
            actions = actions.nextSiblingElement();
        }

    }
Michael Abrahams's avatar
Michael Abrahams committed
449

Michael Abrahams's avatar
Michael Abrahams committed
450
};
Michael Abrahams's avatar
Michael Abrahams committed
451

Michael Abrahams's avatar
Michael Abrahams committed
452 453 454
void KisActionRegistry::Private::loadCustomShortcuts(QString filename)
{
    Q_UNUSED(filename);
Michael Abrahams's avatar
Michael Abrahams committed
455

Michael Abrahams's avatar
Michael Abrahams committed
456 457
    const KConfigGroup localShortcuts(KSharedConfig::openConfig("kritashortcutsrc"),
                                      QStringLiteral("Shortcuts"));
Michael Abrahams's avatar
Michael Abrahams committed
458 459


Michael Abrahams's avatar
Michael Abrahams committed
460 461 462
    if (!localShortcuts.exists()) {
        return;
    }
Michael Abrahams's avatar
Michael Abrahams committed
463

Michael Abrahams's avatar
Michael Abrahams committed
464 465 466 467 468 469
    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);
        }
    }
Michael Abrahams's avatar
Michael Abrahams committed
470
};