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 <QRegExp>
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"
28#include "kis_zoom_action.h"
34#include "KisCanvasOnlyAction.h"
37
38
39class Q_DECL_HIDDEN KisInputProfileManager::Private
40{
41public:
42 Private() : currentProfile(0) { }
43
45 QString profileFileName(const QString &profileName);
46
48
49 QMap<QString, KisInputProfile *> profiles;
50
52};
53
54Q_GLOBAL_STATIC(KisInputProfileManager, inputProfileManager)
55
57{
58 return inputProfileManager;
59}
60
62{
63 return d->profiles.values();
64}
65
67{
68 return d->profiles.keys();
69}
70
72{
73 if (d->profiles.contains(name)) {
74 return d->profiles.value(name);
75 }
76
77 return 0;
78}
79
81{
82 return d->currentProfile;
83}
84
86{
87 if (profile && profile != d->currentProfile) {
88 d->currentProfile = profile;
89 Q_EMIT currentProfileChanged();
90 }
91}
92
94{
95 if (d->profiles.contains(name)) {
96 return d->profiles.value(name);
97 }
98
100 profile->setName(name);
101 d->profiles.insert(name, profile);
102
103 Q_EMIT profilesChanged();
104
105 return profile;
106}
107
109{
110 if (d->profiles.contains(name)) {
111 QString currentProfileName = d->currentProfile->name();
112
113 delete d->profiles.value(name);
114 d->profiles.remove(name);
115
116 //Delete the settings file for the removed profile, if it exists
117 QDir userDir(KoResourcePaths::saveLocation("data", "input/"));
118
119 if (userDir.exists(d->profileFileName(name))) {
120 userDir.remove(d->profileFileName(name));
121 }
122
123 if (currentProfileName == name) {
124 d->currentProfile = d->profiles.begin().value();
125 Q_EMIT currentProfileChanged();
126 }
127
128 Q_EMIT profilesChanged();
129 }
130}
131
132bool KisInputProfileManager::renameProfile(const QString &oldName, const QString &newName)
133{
134 if (!d->profiles.contains(oldName)) {
135 return false;
136 }
137
138 KisInputProfile *profile = d->profiles.value(oldName);
139 if (profile) {
140 d->profiles.remove(oldName);
141 profile->setName(newName);
142 d->profiles.insert(newName, profile);
143
144 Q_EMIT profilesChanged();
145
146
147 return true;
148 }
149
150 return false;
151}
152
153void KisInputProfileManager::duplicateProfile(const QString &name, const QString &newName)
154{
155 if (!d->profiles.contains(name) || d->profiles.contains(newName)) {
156 return;
157 }
158
159 KisInputProfile *newProfile = new KisInputProfile(this);
160 newProfile->setName(newName);
161 d->profiles.insert(newName, newProfile);
162
163 KisInputProfile *profile = d->profiles.value(name);
165 Q_FOREACH(KisShortcutConfiguration * shortcut, shortcuts) {
166 newProfile->addShortcut(new KisShortcutConfiguration(*shortcut));
167 }
168
169 Q_EMIT profilesChanged();
170}
171
173{
174 return d->actions;
175}
176
178{
179 //Remove any profiles that already exist
180 d->currentProfile = nullptr;
181 qDeleteAll(d->profiles);
182 d->profiles.clear();
183
184 //Look up all profiles (this includes those installed to $prefix as well as the user's local data dir)
186
187 dbgKrita << "profiles" << profiles;
188
189 // We don't use list here, because we're assuming we are only going to be changing the user directory and
190 // there can only be one of a profile name.
191 QMap<QString, ProfileEntry> profileEntriesToMigrate;
192 QMap<QString, QList<ProfileEntry>> profileEntries;
193
194 KisConfig cfg(true);
195
196 // Get only valid entries...
197 Q_FOREACH(const QString & p, profiles) {
198
199 ProfileEntry entry;
200 entry.fullpath = p;
201
202 KConfig config(p, KConfig::SimpleConfig);
203 if (!config.hasGroup("General") || !config.group("General").hasKey("name") || !config.group("General").hasKey("version")) {
204 //Skip if we don't have the proper settings.
205 continue;
206 }
207
208 // Only entries of exactly the right version can be considered
209 entry.version = config.group("General").readEntry("version", 0);
210 entry.name = config.group("General").readEntry("name");
211
212 // NOTE: Migrating profiles doesn't just mean porting them to new version. Migrating a profile
213 // may override the existing newer profile file.
214 if (entry.version == PROFILE_VERSION - 1) {
215 // we only utilize the first entry, because it is the most local one and the one which has to be
216 // migrated.
217 profileEntriesToMigrate[entry.name] = entry;
218
219 } else if (entry.version == PROFILE_VERSION) {
220 if (!profileEntries.contains(entry.name)) {
221 profileEntries[entry.name] = QList<ProfileEntry>();
222 }
223
224 // let all the current version entries pile up in the list, it is only later where we check if it
225 // is something we will use or a migrated entry.
226 profileEntries[entry.name].append(entry);
227 }
228 }
229
230 {
231 const QString userLocalSaveLocation = KoResourcePaths::saveLocation("data", "input/");
232 auto entriesIt = profileEntriesToMigrate.begin();
233 while (entriesIt != profileEntriesToMigrate.end()) {
234 ProfileEntry entry = *entriesIt;
235 // if entry doesn't exist in profileEntries, means there is no corresponding new version of the
236 // entry in user directory. Meaning, it is a certain candidate for migration.
237
238 if (profileEntries.contains(entry.name)) {
239
240 // we only need first() because if a user-local entry exists, it will be the first.
241 ProfileEntry existingEntry = profileEntries[entry.name].first();
242
243 // check if the entry's fullpath is a saveLocation, if so, we remove it from migration list.
244 if (existingEntry.fullpath.startsWith(userLocalSaveLocation)) {
245 entriesIt = profileEntriesToMigrate.erase(entriesIt);
246 } else {
247 // if the entry's fullpath is not a saveLocation, we will migrate it. Because (user's
248 // previous configuration + current default touch shortcuts) are better than. (All default
249 // shortcuts).
250 entriesIt++;
251
252 // Because this entry is supposed to be migrated, it will clash with an already existing
253 // default entry. So remove it.
254 profileEntries.remove(existingEntry.name);
255 }
256 } else {
257 entriesIt++;
258 }
259 }
260 }
261
262 {
263 KisInputProfileMigrator5To6 migrator(this);
264 QMap<ProfileEntry, QList<KisShortcutConfiguration>> parsedProfilesToMigrate =
265 migrator.migrate(profileEntriesToMigrate);
266
267 for (ProfileEntry profileEntry : parsedProfilesToMigrate.keys()) {
268 const QString storagePath = KoResourcePaths::saveLocation("data", "input/", true);
269
270 {
271 // the profile we have here uses the previous config, the only thing we need to make sure is
272 // it doesn't overwrite the existing profile to preserve backwards compatibility.
273 const QString profilePath = profileEntry.fullpath;
274 QString oldProfileName = QFileInfo(profilePath).fileName();
275 oldProfileName.replace(".profile", QString::number(PROFILE_VERSION - 1) + ".profile");
276
277 QString oldProfilePath = storagePath + oldProfileName;
278 // copy the profile to a new file but add version number to the name
279 QFile::copy(profilePath, oldProfilePath);
280
281 KConfig config(oldProfilePath, KConfig::SimpleConfig);
282 config.group("General").writeEntry("migrated", PROFILE_VERSION);
283 }
284
285 KisInputProfile *newProfile = addProfile(profileEntry.name);
286 QList<KisShortcutConfiguration> shortcuts = parsedProfilesToMigrate.value(profileEntry);
287 for (const auto &shortcut : shortcuts) {
288 newProfile->addShortcut(new KisShortcutConfiguration(shortcut));
289 }
290
291 // save the new profile with migrated shortcuts. We overwrite the previous version of file (which
292 // previously has been moved for backward compatibility).
293 saveProfile(newProfile, storagePath);
294 }
295 }
296
297 Q_FOREACH(const QString & profileName, profileEntries.keys()) {
298
299 if (profileEntries[profileName].isEmpty()) {
300 continue;
301 }
302
303 // we have one or more entries for this profile name. We'll take the first,
304 // because that's the most local one.
305 ProfileEntry entry = profileEntries[profileName].first();
306
307 KConfig config(entry.fullpath, KConfig::SimpleConfig);
308
309 KisInputProfile *newProfile = addProfile(entry.name);
310 Q_FOREACH(KisAbstractInputAction * action, d->actions) {
311 if (!config.hasGroup(action->id())) {
312 continue;
313 }
314
315 KConfigGroup grp = config.group(action->id());
316 //Read the settings for the action and create the appropriate shortcuts.
317 Q_FOREACH(const QString & entry, grp.entryMap()) {
319 shortcut->setAction(action);
320
321 if (shortcut->unserialize(entry)) {
322 newProfile->addShortcut(shortcut);
323 }
324 else {
325 delete shortcut;
326 }
327 }
328 }
329 }
330
331 QString currentProfile = cfg.currentInputProfile();
332 if (d->profiles.size() > 0) {
333 if (currentProfile.isEmpty() || !d->profiles.contains(currentProfile)) {
334 QString kritaDefault = QStringLiteral("Krita Default");
335 if (d->profiles.contains(kritaDefault)) {
336 d->currentProfile = d->profiles.value(kritaDefault);
337 } else {
338 d->currentProfile = d->profiles.begin().value();
339 }
340 }
341 else {
342 d->currentProfile = d->profiles.value(currentProfile);
343 }
344 }
345 if (d->currentProfile) {
346 Q_EMIT currentProfileChanged();
347 }
348}
349
351{
352 QString storagePath = KoResourcePaths::saveLocation("data", "input/", true);
353 Q_FOREACH(KisInputProfile * p, d->profiles) {
354 saveProfile(p, storagePath);
355 }
356
357 KisConfig config(false);
358 config.setCurrentInputProfile(d->currentProfile->name());
359
360 //Force a reload of the current profile in input manager and whatever else uses the profile.
361 Q_EMIT currentProfileChanged();
362}
363
364void KisInputProfileManager::saveProfile(KisInputProfile *profile, QString storagePath)
365{
366 const QString profilePath = storagePath + d->profileFileName(profile->name());
367 KConfig config(profilePath, KConfig::SimpleConfig);
368
369 config.group("General").writeEntry("name", profile->name());
370 config.group("General").writeEntry("version", PROFILE_VERSION);
371
372 Q_FOREACH(KisAbstractInputAction * action, d->actions) {
373 KConfigGroup grp = config.group(action->id());
374 grp.deleteGroup(); //Clear the group of any existing shortcuts.
375
376 int index = 0;
378 Q_FOREACH(KisShortcutConfiguration * shortcut, shortcuts) {
379 grp.writeEntry(QString("%1").arg(index++), shortcut->serialize());
380 }
381 }
382
383 config.sync();
384}
385
387{
388 QSet<KisShortcutConfiguration *> conflictedShortcuts;
390 for (auto startIt = shortcuts.constBegin(); startIt != shortcuts.constEnd(); ++startIt) {
391 KisShortcutConfiguration *first = *startIt;
392 for (auto index = startIt + 1; index != shortcuts.constEnd(); ++index) {
393 KisShortcutConfiguration *second = *index;
394 // since there can be multiple no-ops in the config and because no-ops are something a user can't
395 // perform, there should be no conflicts.
396 if (*first == *second && !first->isNoOp()) {
397 conflictedShortcuts.insert(first);
398 conflictedShortcuts.insert(second);
399 }
400 }
401 }
402 return conflictedShortcuts.values();
403}
404
406{
407 QString kdeHome = KoResourcePaths::getAppDataLocation();
409
410 Q_FOREACH (const QString &profile, profiles) {
411 if(profile.contains(kdeHome)) {
412 //This is a local file, remove it.
413 QFile::remove(profile);
414 }
415 }
416
417 //Load the profiles again, this should now only load those shipped with Krita.
418 loadProfiles();
419
420 Q_EMIT profilesChanged();
421}
422
424 : QObject(parent), d(new Private())
425{
426 d->createActions();
427}
428
430{
431 qDeleteAll(d->profiles);
432 qDeleteAll(d->actions);
433 delete d;
434}
435
436void KisInputProfileManager::Private::createActions()
437{
438 //TODO: Make this plugin based
439 //Note that the ordering here determines how things show up in the UI
440 actions.append(new KisToolInvocationAction());
441 actions.append(new KisAlternateInvocationAction());
442 actions.append(new KisChangePrimarySettingAction());
443 actions.append(new KisPanAction());
444 actions.append(new KisRotateCanvasAction());
445 actions.append(new KisZoomAction());
446 actions.append(new KisPopupWidgetAction());
447 actions.append(new KisSelectLayerAction());
448 actions.append(new KisGammaExposureAction());
449 actions.append(new KisChangeFrameAction());
450 actions.append(new KisZoomAndRotateAction());
451 actions.append(new KisTouchGestureAction());
452}
453
454QString KisInputProfileManager::Private::profileFileName(const QString &profileName)
455{
456 QRegExp reg("[^a-z0-9]");
457#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
458 return profileName.toLower().remove(QRegExp("[^a-z0-9]")).append(".profile");
459#else
460 QString pf = profileName.toLower();
461 reg.removeIn(pf);
462 return pf.append(".profile");
463#endif
464
465}
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)
void setCurrentProfile(KisInputProfile *profile)
QMap< QString, KisInputProfile * > profiles
bool renameProfile(const QString &oldName, const QString &newName)
void duplicateProfile(const QString &name, const QString &newName)
QMap< ProfileEntry, QList< KisShortcutConfiguration > > migrate(const QMap< QString, ProfileEntry > profiles) 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 dbgKrita
Definition kis_debug.h:45
#define PROFILE_VERSION