Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_input_profile_manager.cpp
Go to the documentation of this file.
1/*
2 * This file is part of the KDE project
3 * SPDX-FileCopyrightText: 2013 Arjen Hiemstra <ahiemstra@heimr.nl>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
9#include "kis_input_profile.h"
10
11#include <QMap>
12#include <QStringList>
13#include <QDir>
14#include <QGlobalStatic>
15#include <QRegularExpression>
16
17#include <KoResourcePaths.h>
18#include <kconfig.h>
19#include <kconfiggroup.h>
20
21#include "kis_config.h"
24#include "kis_pan_action.h"
29#include "kis_zoom_action.h"
35#include "KisCanvasOnlyAction.h"
38
39
40class Q_DECL_HIDDEN KisInputProfileManager::Private
41{
42public:
43 Private() : currentProfile(0) { }
44
46 QString profileFileName(const QString &profileName);
47
49
50 QMap<QString, KisInputProfile *> profiles;
51
53};
54
55Q_GLOBAL_STATIC(KisInputProfileManager, inputProfileManager)
56
58{
59 return inputProfileManager;
60}
61
63{
64 return d->profiles.values();
65}
66
68{
69 return d->profiles.keys();
70}
71
73{
74 if (d->profiles.contains(name)) {
75 return d->profiles.value(name);
76 }
77
78 return 0;
79}
80
82{
83 return d->currentProfile;
84}
85
87{
88 if (profile && profile != d->currentProfile) {
89 d->currentProfile = profile;
90 Q_EMIT currentProfileChanged();
91 }
92}
93
95{
96 if (d->profiles.contains(name)) {
97 return d->profiles.value(name);
98 }
99
101 profile->setName(name);
102 d->profiles.insert(name, profile);
103
104 Q_EMIT profilesChanged();
105
106 return profile;
107}
108
110{
111 if (d->profiles.contains(name)) {
112 QString currentProfileName = d->currentProfile->name();
113
114 delete d->profiles.value(name);
115 d->profiles.remove(name);
116
117 //Delete the settings file for the removed profile, if it exists
118 QDir userDir(KoResourcePaths::saveLocation("data", "input/"));
119
120 if (userDir.exists(d->profileFileName(name))) {
121 userDir.remove(d->profileFileName(name));
122 }
123
124 if (currentProfileName == name) {
125 d->currentProfile = d->profiles.begin().value();
126 Q_EMIT currentProfileChanged();
127 }
128
129 Q_EMIT profilesChanged();
130 }
131}
132
133bool KisInputProfileManager::renameProfile(const QString &oldName, const QString &newName)
134{
135 if (!d->profiles.contains(oldName)) {
136 return false;
137 }
138
139 KisInputProfile *profile = d->profiles.value(oldName);
140 if (profile) {
141 d->profiles.remove(oldName);
142 profile->setName(newName);
143 d->profiles.insert(newName, profile);
144
145 Q_EMIT profilesChanged();
146
147
148 return true;
149 }
150
151 return false;
152}
153
154void KisInputProfileManager::duplicateProfile(const QString &name, const QString &newName)
155{
156 if (!d->profiles.contains(name) || d->profiles.contains(newName)) {
157 return;
158 }
159
160 KisInputProfile *newProfile = new KisInputProfile(this);
161 newProfile->setName(newName);
162 d->profiles.insert(newName, newProfile);
163
164 KisInputProfile *profile = d->profiles.value(name);
166 Q_FOREACH(KisShortcutConfiguration * shortcut, shortcuts) {
167 newProfile->addShortcut(new KisShortcutConfiguration(*shortcut));
168 }
169
170 Q_EMIT profilesChanged();
171}
172
174{
175 return d->actions;
176}
177
179{
180 //Remove any profiles that already exist
181 d->currentProfile = nullptr;
182 qDeleteAll(d->profiles);
183 d->profiles.clear();
184
185 //Look up all profiles (this includes those installed to $prefix as well as the user's local data dir)
187
188 dbgKrita << "profiles" << profiles;
189
190 // We don't use list here, because we're assuming we are only going to be changing the user directory and
191 // there can only be one of a profile name.
192 QMap<QString, ProfileEntry> profileEntriesToMigrate;
193 QMap<QString, QList<ProfileEntry>> profileEntries;
194
195 KisConfig cfg(true);
196
197 // Get only valid entries...
198 Q_FOREACH(const QString & p, profiles) {
199
200 ProfileEntry entry;
201 entry.fullpath = p;
202
203 KConfig config(p, KConfig::SimpleConfig);
204 if (!config.hasGroup("General") || !config.group("General").hasKey("name") || !config.group("General").hasKey("version")) {
205 //Skip if we don't have the proper settings.
206 continue;
207 }
208
209 // Only entries of exactly the right version can be considered
210 entry.version = config.group("General").readEntry("version", 0);
211 entry.name = config.group("General").readEntry("name");
212
213 // NOTE: Migrating profiles doesn't just mean porting them to new version. Migrating a profile
214 // may override the existing newer profile file.
215 if (entry.version < PROFILE_VERSION) {
216 // we only utilize the first entry, because it is the most local one and the one which has to be
217 // migrated.
218 profileEntriesToMigrate[entry.name] = entry;
219
220 } else if (entry.version == PROFILE_VERSION) {
221 if (!profileEntries.contains(entry.name)) {
222 profileEntries[entry.name] = QList<ProfileEntry>();
223 }
224
225 // let all the current version entries pile up in the list, it is only later where we check if it
226 // is something we will use or a migrated entry.
227 profileEntries[entry.name].append(entry);
228 }
229 }
230
231 {
232 const QString userLocalSaveLocation = KoResourcePaths::saveLocation("data", "input/");
233 auto entriesIt = profileEntriesToMigrate.begin();
234 while (entriesIt != profileEntriesToMigrate.end()) {
235 ProfileEntry entry = *entriesIt;
236 // if entry doesn't exist in profileEntries, means there is no corresponding new version of the
237 // entry in user directory. Meaning, it is a certain candidate for migration.
238
239 if (profileEntries.contains(entry.name)) {
240
241 // we only need first() because if a user-local entry exists, it will be the first.
242 ProfileEntry existingEntry = profileEntries[entry.name].first();
243
244 // check if the entry's fullpath is a saveLocation, if so, we remove it from migration list.
245 if (existingEntry.fullpath.startsWith(userLocalSaveLocation)) {
246 entriesIt = profileEntriesToMigrate.erase(entriesIt);
247 } else {
248 // if the entry's fullpath is not a saveLocation, we will migrate it. Because (user's
249 // previous configuration + current default touch shortcuts) are better than. (All default
250 // shortcuts).
251 entriesIt++;
252
253 // Because this entry is supposed to be migrated, it will clash with an already existing
254 // default entry. So remove it.
255 profileEntries.remove(existingEntry.name);
256 }
257 } else {
258 entriesIt++;
259 }
260 }
261 }
262
263 for (const auto &profileEntry : profileEntriesToMigrate) {
264 loadProfileWithMigration(profileEntry);
265 }
266
267 Q_FOREACH(const QString & profileName, profileEntries.keys()) {
268
269 if (profileEntries[profileName].isEmpty()) {
270 continue;
271 }
272
273 // we have one or more entries for this profile name. We'll take the first,
274 // because that's the most local one.
275 ProfileEntry entry = profileEntries[profileName].first();
276
278 }
279
280 QString currentProfile = cfg.currentInputProfile();
281 if (d->profiles.size() > 0) {
282 if (currentProfile.isEmpty() || !d->profiles.contains(currentProfile)) {
283 QString kritaDefault = QStringLiteral("Krita Default");
284 if (d->profiles.contains(kritaDefault)) {
285 d->currentProfile = d->profiles.value(kritaDefault);
286 } else {
287 d->currentProfile = d->profiles.begin().value();
288 }
289 }
290 else {
291 d->currentProfile = d->profiles.value(currentProfile);
292 }
293 }
294 if (d->currentProfile) {
295 Q_EMIT currentProfileChanged();
296 }
297}
298
300{
301 KConfig config(fullPath, KConfig::SimpleConfig);
302
303 QList<KisShortcutConfiguration*> loadedShortcuts;
304
305 // Only entries of exactly the right version can be considered
306 const int version = config.group("General").readEntry("version", 0);
307 const QString name = config.group("General").readEntry("name");
308
309 // the profile must be already converted by this point
311
312 Q_FOREACH (KisAbstractInputAction *action, d->actions) {
313 if (!config.hasGroup(action->id())) {
314 continue;
315 }
316
317 KConfigGroup grp = config.group(action->id());
318 // Read the settings for the action and create the appropriate shortcuts.
319 Q_FOREACH (const QString &entry, grp.entryMap()) {
321 shortcut->setAction(action);
322
323 if (shortcut->unserialize(entry)) {
324 loadedShortcuts.append(shortcut);
325 } else {
326 delete shortcut;
327 }
328 }
329 }
330
331 KisInputProfile *newProfile = addProfile(name);
332 Q_FOREACH(KisShortcutConfiguration *shortcut, loadedShortcuts) {
333 newProfile->addShortcut(shortcut);
334 }
335 return newProfile;
336}
337
339{
340 QList<KisShortcutConfiguration> parsedShortcuts;
341
342 if (profileEntry.version == 5) {
343 KisInputProfileMigratorFrom5 migratorFromV5(this);
344 parsedShortcuts = migratorFromV5.migrate(profileEntry);
345 } else if (profileEntry.version == 6) {
346 KisInputProfileMigratorFrom6 migratorFromV6(this);
347 parsedShortcuts = migratorFromV6.migrate(profileEntry);
348 } else {
349 warnKrita << "Unable to migrate input profile: unknown version:" << profileEntry.version << "("
350 << profileEntry.fullpath << ")";
351 return nullptr;
352 }
353
354 dbgKrita << "Migrating a profile " << profileEntry.name << "(" << profileEntry.fullpath << ")" << "from version" << profileEntry.version;
355
356 const QString storagePath = KoResourcePaths::saveLocation("data", "input/", true);
357
358 {
359 // the profile we have here uses the previous config, the only thing we need to make sure is
360 // it doesn't overwrite the existing profile to preserve backwards compatibility.
361 const QString profilePath = profileEntry.fullpath;
362 QString oldProfileName = QFileInfo(profilePath).fileName();
363 oldProfileName.replace(".profile", QString::number(PROFILE_VERSION - 1) + ".profile");
364
365 QString oldProfilePath = storagePath + oldProfileName;
366 // copy the profile to a new file but add version number to the name
367 QFile::copy(profilePath, oldProfilePath);
368
369 KConfig config(oldProfilePath, KConfig::SimpleConfig);
370 config.group("General").writeEntry("migrated", PROFILE_VERSION);
371 }
372
373 KisInputProfile *newProfile = addProfile(profileEntry.name);
374 for (const auto &shortcut : parsedShortcuts) {
375 newProfile->addShortcut(new KisShortcutConfiguration(shortcut));
376 }
377
378 // save the new profile with migrated shortcuts. We overwrite the previous version of file (which
379 // previously has been moved for backward compatibility).
380 saveProfile(newProfile, storagePath);
381
382 return newProfile;
383}
384
386{
387 QString storagePath = KoResourcePaths::saveLocation("data", "input/", true);
388 Q_FOREACH(KisInputProfile * p, d->profiles) {
389 saveProfile(p, storagePath);
390 }
391
392 KisConfig config(false);
393 config.setCurrentInputProfile(d->currentProfile->name());
394
395 //Force a reload of the current profile in input manager and whatever else uses the profile.
396 Q_EMIT currentProfileChanged();
397}
398
399void KisInputProfileManager::saveProfile(KisInputProfile *profile, QString storagePath)
400{
401 const QString profilePath = storagePath + d->profileFileName(profile->name());
402 KConfig config(profilePath, KConfig::SimpleConfig);
403
404 config.group("General").writeEntry("name", profile->name());
405 config.group("General").writeEntry("version", PROFILE_VERSION);
406
407 Q_FOREACH(KisAbstractInputAction * action, d->actions) {
408 KConfigGroup grp = config.group(action->id());
409 grp.deleteGroup(); //Clear the group of any existing shortcuts.
410
411 int index = 0;
413 Q_FOREACH(KisShortcutConfiguration * shortcut, shortcuts) {
414 grp.writeEntry(QString("%1").arg(index++), shortcut->serialize());
415 }
416 }
417
418 config.sync();
419}
420
422{
423 QSet<KisShortcutConfiguration *> conflictedShortcuts;
425 for (auto startIt = shortcuts.constBegin(); startIt != shortcuts.constEnd(); ++startIt) {
426 KisShortcutConfiguration *first = *startIt;
427 for (auto index = startIt + 1; index != shortcuts.constEnd(); ++index) {
428 KisShortcutConfiguration *second = *index;
429 // since there can be multiple no-ops in the config and because no-ops are something a user can't
430 // perform, there should be no conflicts.
431 if (*first == *second && !first->isNoOp()) {
432 conflictedShortcuts.insert(first);
433 conflictedShortcuts.insert(second);
434 }
435 }
436 }
437 return conflictedShortcuts.values();
438}
439
441{
442 QString kdeHome = KoResourcePaths::getAppDataLocation();
444
445 Q_FOREACH (const QString &profile, profiles) {
446 if(profile.contains(kdeHome)) {
447 //This is a local file, remove it.
448 QFile::remove(profile);
449 }
450 }
451
452 //Load the profiles again, this should now only load those shipped with Krita.
453 loadProfiles();
454
455 Q_EMIT profilesChanged();
456}
457
459 : QObject(parent), d(new Private())
460{
461 d->createActions();
462}
463
465{
466 qDeleteAll(d->profiles);
467 qDeleteAll(d->actions);
468 delete d;
469}
470
471void KisInputProfileManager::Private::createActions()
472{
473 //TODO: Make this plugin based
474 //Note that the ordering here determines how things show up in the UI
475 actions.append(new KisToolInvocationAction());
476 actions.append(new KisTemporaryStraightRulerAction());
477 actions.append(new KisAlternateInvocationAction());
478 actions.append(new KisChangePrimarySettingAction());
479 actions.append(new KisPanAction());
480 actions.append(new KisRotateCanvasAction());
481 actions.append(new KisZoomAction());
482 actions.append(new KisPopupWidgetAction());
483 actions.append(new KisSelectLayerAction());
484 actions.append(new KisGammaExposureAction());
485 actions.append(new KisChangeFrameAction());
486 actions.append(new KisZoomAndRotateAction());
487 actions.append(new KisTouchGestureAction());
488}
489
490QString KisInputProfileManager::Private::profileFileName(const QString &profileName)
491{
492 QRegularExpression reg("[^a-z0-9]");
493 return profileName.toLower().remove(reg).append(".profile");
494}
const Params2D p
Q_GLOBAL_STATIC(KisStoragePluginRegistry, s_instance)
PythonPluginManager * instance
Abstract base class for input actions.
Alternate Invocation implementation of KisAbstractInputAction.
Change Primary Setting implementation of KisAbstractInputAction.
void setCurrentInputProfile(const QString &name)
QString currentInputProfile(bool defaultValue=false) const
A class to manage a list of profiles and actions.
QList< KisAbstractInputAction * > actions
KisInputProfile * profile(const QString &name) const
QList< KisShortcutConfiguration * > getConflictingShortcuts(KisInputProfile *profile)
QString profileFileName(const QString &profileName)
void removeProfile(const QString &name)
KisInputProfile * addProfile(const QString &name)
void saveProfile(KisInputProfile *profile, QString storagePath)
KisInputProfile * loadProfileInternal(const QString &fullPath)
KisInputProfile * loadProfileWithMigration(const ProfileEntry &profileEntry)
void setCurrentProfile(KisInputProfile *profile)
QMap< QString, KisInputProfile * > profiles
bool renameProfile(const QString &oldName, const QString &newName)
void duplicateProfile(const QString &name, const QString &newName)
QList< KisShortcutConfiguration > migrate(const ProfileEntry &profile) override
QList< KisShortcutConfiguration > migrate(const ProfileEntry &profile) override
A container class for sets of shortcuts associated with an action.
QList< KisShortcutConfiguration * > allShortcuts() const
QList< KisShortcutConfiguration * > shortcutsForAction(KisAbstractInputAction *action) const
void addShortcut(KisShortcutConfiguration *shortcut)
QString name() const
void setName(const QString &name)
Pan Canvas implementation of KisAbstractInputAction.
Get the current tool's popup widget and display it.
Rotate Canvas implementation of KisAbstractInputAction.
Select Layer implementation of KisAbstractInputAction.
A class encapsulating all settings for a single shortcut.
bool unserialize(const QString &serialized)
void setAction(KisAbstractInputAction *newAction)
Tool Invocation action of KisAbstractInputAction.
Zoom Canvas implementation of KisAbstractInputAction.
This class handles both rotation and zooming operation. This is separate from Zoom and Rotate operati...
static QString getAppDataLocation()
static QStringList findAllAssets(const QString &type, const QString &filter=QString(), SearchOptions options=NoSearchOptions)
static QString saveLocation(const QString &type, const QString &suffix=QString(), bool create=true)
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
#define dbgKrita
Definition kis_debug.h:49
#define warnKrita
Definition kis_debug.h:94
#define PROFILE_VERSION