File indexing completed on 2024-12-22 04:12:42

0001 /*
0002  * This file is part of the KDE project
0003  * SPDX-FileCopyrightText: 2013 Arjen Hiemstra <ahiemstra@heimr.nl>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "kis_input_profile_manager.h"
0009 #include "kis_input_profile.h"
0010 
0011 #include <QMap>
0012 #include <QStringList>
0013 #include <QDir>
0014 #include <QGlobalStatic>
0015 
0016 #include <KoResourcePaths.h>
0017 #include <kconfig.h>
0018 #include <kconfiggroup.h>
0019 
0020 #include "kis_config.h"
0021 #include "kis_alternate_invocation_action.h"
0022 #include "kis_change_primary_setting_action.h"
0023 #include "kis_pan_action.h"
0024 #include "kis_rotate_canvas_action.h"
0025 #include "KisPopupWidgetAction.h"
0026 #include "kis_tool_invocation_action.h"
0027 #include "kis_zoom_action.h"
0028 #include "kis_shortcut_configuration.h"
0029 #include "kis_select_layer_action.h"
0030 #include "kis_gamma_exposure_action.h"
0031 #include "kis_change_frame_action.h"
0032 #include "kis_zoom_and_rotate_action.h"
0033 #include "KisCanvasOnlyAction.h"
0034 #include "KisTouchGestureAction.h"
0035 #include "KisInputProfileMigrator.h"
0036 
0037 
0038 class Q_DECL_HIDDEN KisInputProfileManager::Private
0039 {
0040 public:
0041     Private() : currentProfile(0) { }
0042 
0043     void createActions();
0044     QString profileFileName(const QString &profileName);
0045 
0046     KisInputProfile *currentProfile;
0047 
0048     QMap<QString, KisInputProfile *> profiles;
0049 
0050     QList<KisAbstractInputAction *> actions;
0051 };
0052 
0053 Q_GLOBAL_STATIC(KisInputProfileManager, inputProfileManager)
0054 
0055 KisInputProfileManager *KisInputProfileManager::instance()
0056 {
0057     return inputProfileManager;
0058 }
0059 
0060 QList< KisInputProfile * > KisInputProfileManager::profiles() const
0061 {
0062     return d->profiles.values();
0063 }
0064 
0065 QStringList KisInputProfileManager::profileNames() const
0066 {
0067     return d->profiles.keys();
0068 }
0069 
0070 KisInputProfile *KisInputProfileManager::profile(const QString &name) const
0071 {
0072     if (d->profiles.contains(name)) {
0073         return d->profiles.value(name);
0074     }
0075 
0076     return 0;
0077 }
0078 
0079 KisInputProfile *KisInputProfileManager::currentProfile() const
0080 {
0081     return d->currentProfile;
0082 }
0083 
0084 void KisInputProfileManager::setCurrentProfile(KisInputProfile *profile)
0085 {
0086     if (profile && profile != d->currentProfile) {
0087         d->currentProfile = profile;
0088         emit currentProfileChanged();
0089     }
0090 }
0091 
0092 KisInputProfile *KisInputProfileManager::addProfile(const QString &name)
0093 {
0094     if (d->profiles.contains(name)) {
0095         return d->profiles.value(name);
0096     }
0097 
0098     KisInputProfile *profile = new KisInputProfile(this);
0099     profile->setName(name);
0100     d->profiles.insert(name, profile);
0101 
0102     emit profilesChanged();
0103 
0104     return profile;
0105 }
0106 
0107 void KisInputProfileManager::removeProfile(const QString &name)
0108 {
0109     if (d->profiles.contains(name)) {
0110         QString currentProfileName = d->currentProfile->name();
0111 
0112         delete d->profiles.value(name);
0113         d->profiles.remove(name);
0114 
0115         //Delete the settings file for the removed profile, if it exists
0116         QDir userDir(KoResourcePaths::saveLocation("data", "input/"));
0117 
0118         if (userDir.exists(d->profileFileName(name))) {
0119             userDir.remove(d->profileFileName(name));
0120         }
0121 
0122         if (currentProfileName == name) {
0123             d->currentProfile = d->profiles.begin().value();
0124             emit currentProfileChanged();
0125         }
0126 
0127         emit profilesChanged();
0128     }
0129 }
0130 
0131 bool KisInputProfileManager::renameProfile(const QString &oldName, const QString &newName)
0132 {
0133     if (!d->profiles.contains(oldName)) {
0134         return false;
0135     }
0136 
0137     KisInputProfile *profile = d->profiles.value(oldName);
0138     if (profile) {
0139         d->profiles.remove(oldName);
0140         profile->setName(newName);
0141         d->profiles.insert(newName, profile);
0142 
0143         emit profilesChanged();
0144 
0145 
0146         return true;
0147     }
0148 
0149     return false;
0150 }
0151 
0152 void KisInputProfileManager::duplicateProfile(const QString &name, const QString &newName)
0153 {
0154     if (!d->profiles.contains(name) || d->profiles.contains(newName)) {
0155         return;
0156     }
0157 
0158     KisInputProfile *newProfile = new KisInputProfile(this);
0159     newProfile->setName(newName);
0160     d->profiles.insert(newName, newProfile);
0161 
0162     KisInputProfile *profile = d->profiles.value(name);
0163     QList<KisShortcutConfiguration *> shortcuts = profile->allShortcuts();
0164     Q_FOREACH(KisShortcutConfiguration * shortcut, shortcuts) {
0165         newProfile->addShortcut(new KisShortcutConfiguration(*shortcut));
0166     }
0167 
0168     emit profilesChanged();
0169 }
0170 
0171 QList< KisAbstractInputAction * > KisInputProfileManager::actions()
0172 {
0173     return d->actions;
0174 }
0175 
0176 void KisInputProfileManager::loadProfiles()
0177 {
0178     //Remove any profiles that already exist
0179     d->currentProfile = nullptr;
0180     qDeleteAll(d->profiles);
0181     d->profiles.clear();
0182 
0183     //Look up all profiles (this includes those installed to $prefix as well as the user's local data dir)
0184     QStringList profiles = KoResourcePaths::findAllAssets("data", "input/*.profile", KoResourcePaths::Recursive);
0185 
0186     dbgKrita << "profiles" << profiles;
0187 
0188     // We don't use list here, because we're assuming we are only going to be changing the user directory and
0189     // there can only be one of a profile name.
0190     QMap<QString, ProfileEntry> profileEntriesToMigrate;
0191     QMap<QString, QList<ProfileEntry>> profileEntries;
0192 
0193     KisConfig cfg(true);
0194 
0195     // Get only valid entries...
0196     Q_FOREACH(const QString & p, profiles) {
0197 
0198         ProfileEntry entry;
0199         entry.fullpath = p;
0200 
0201         KConfig config(p, KConfig::SimpleConfig);
0202         if (!config.hasGroup("General") || !config.group("General").hasKey("name") || !config.group("General").hasKey("version")) {
0203             //Skip if we don't have the proper settings.
0204             continue;
0205         }
0206 
0207         // Only entries of exactly the right version can be considered
0208         entry.version = config.group("General").readEntry("version", 0);
0209         entry.name = config.group("General").readEntry("name");
0210 
0211         // NOTE: Migrating profiles doesn't just mean porting them to new version. Migrating a profile
0212         // may override the existing newer profile file.
0213         if (entry.version == PROFILE_VERSION - 1) {
0214             // we only utilize the first entry, because it is the most local one and the one which has to be
0215             // migrated.
0216             profileEntriesToMigrate[entry.name] = entry;
0217 
0218         } else if (entry.version == PROFILE_VERSION) {
0219             if (!profileEntries.contains(entry.name)) {
0220                 profileEntries[entry.name] = QList<ProfileEntry>();
0221             }
0222 
0223             // let all the current version entries pile up in the list, it is only later where we check if it
0224             // is something we will use or a migrated entry.
0225             profileEntries[entry.name].append(entry);
0226         }
0227     }
0228 
0229     {
0230         const QString userLocalSaveLocation = KoResourcePaths::saveLocation("data", "input/");
0231         auto entriesIt = profileEntriesToMigrate.begin();
0232         while (entriesIt != profileEntriesToMigrate.end()) {
0233             ProfileEntry entry = *entriesIt;
0234             // if entry doesn't exist in profileEntries, means there is no corresponding new version of the
0235             // entry in user directory. Meaning, it is a certain candidate for migration.
0236 
0237             if (profileEntries.contains(entry.name)) {
0238 
0239                 // we only need first() because if a user-local entry exists, it will be the first.
0240                 ProfileEntry existingEntry = profileEntries[entry.name].first();
0241 
0242                 // check if the entry's fullpath is a saveLocation, if so, we remove it from migration list.
0243                 if (existingEntry.fullpath.startsWith(userLocalSaveLocation)) {
0244                     entriesIt = profileEntriesToMigrate.erase(entriesIt);
0245                 } else {
0246                     // if the entry's fullpath is not a saveLocation, we will migrate it. Because (user's
0247                     // previous configuration + current default touch shortcuts) are better than. (All default
0248                     // shortcuts).
0249                     entriesIt++;
0250 
0251                     // Because this entry is supposed to be migrated, it will clash with an already existing
0252                     // default entry. So remove it.
0253                     profileEntries.remove(existingEntry.name);
0254                 }
0255             } else {
0256                 entriesIt++;
0257             }
0258         }
0259     }
0260 
0261     {
0262         KisInputProfileMigrator5To6 migrator(this);
0263         QMap<ProfileEntry, QList<KisShortcutConfiguration>> parsedProfilesToMigrate =
0264             migrator.migrate(profileEntriesToMigrate);
0265 
0266         for (ProfileEntry profileEntry : parsedProfilesToMigrate.keys()) {
0267             const QString storagePath = KoResourcePaths::saveLocation("data", "input/", true);
0268 
0269             {
0270                 // the profile we have here uses the previous config, the only thing we need to make sure is
0271                 // it doesn't overwrite the existing profile to preserve backwards compatibility.
0272                 const QString profilePath = profileEntry.fullpath;
0273                 QString oldProfileName = QFileInfo(profilePath).fileName();
0274                 oldProfileName.replace(".profile", QString::number(PROFILE_VERSION - 1) + ".profile");
0275 
0276                 QString oldProfilePath = storagePath + oldProfileName;
0277                 // copy the profile to a new file but add version number to the name
0278                 QFile::copy(profilePath, oldProfilePath);
0279 
0280                 KConfig config(oldProfilePath, KConfig::SimpleConfig);
0281                 config.group("General").writeEntry("migrated", PROFILE_VERSION);
0282             }
0283 
0284             KisInputProfile *newProfile = addProfile(profileEntry.name);
0285             QList<KisShortcutConfiguration> shortcuts = parsedProfilesToMigrate.value(profileEntry);
0286             for (const auto &shortcut : shortcuts) {
0287                 newProfile->addShortcut(new KisShortcutConfiguration(shortcut));
0288             }
0289 
0290             // save the new profile with migrated shortcuts. We overwrite the previous version of file (which
0291             // previously has been moved for backward compatibility).
0292             saveProfile(newProfile, storagePath);
0293         }
0294     }
0295 
0296     Q_FOREACH(const QString & profileName, profileEntries.keys()) {
0297 
0298         if (profileEntries[profileName].isEmpty()) {
0299             continue;
0300         }
0301 
0302         // we have one or more entries for this profile name. We'll take the first,
0303         // because that's the most local one.
0304         ProfileEntry entry = profileEntries[profileName].first();
0305 
0306         KConfig config(entry.fullpath, KConfig::SimpleConfig);
0307 
0308         KisInputProfile *newProfile = addProfile(entry.name);
0309         Q_FOREACH(KisAbstractInputAction * action, d->actions) {
0310             if (!config.hasGroup(action->id())) {
0311                 continue;
0312             }
0313 
0314             KConfigGroup grp = config.group(action->id());
0315             //Read the settings for the action and create the appropriate shortcuts.
0316             Q_FOREACH(const QString & entry, grp.entryMap()) {
0317                 KisShortcutConfiguration *shortcut = new KisShortcutConfiguration;
0318                 shortcut->setAction(action);
0319 
0320                 if (shortcut->unserialize(entry)) {
0321                     newProfile->addShortcut(shortcut);
0322                 }
0323                 else {
0324                     delete shortcut;
0325                 }
0326             }
0327         }
0328     }
0329 
0330     QString currentProfile = cfg.currentInputProfile();
0331     if (d->profiles.size() > 0) {
0332         if (currentProfile.isEmpty() || !d->profiles.contains(currentProfile)) {
0333             QString kritaDefault = QStringLiteral("Krita Default");
0334             if (d->profiles.contains(kritaDefault)) {
0335                 d->currentProfile = d->profiles.value(kritaDefault);
0336             } else {
0337                 d->currentProfile = d->profiles.begin().value();
0338             }
0339         }
0340         else {
0341             d->currentProfile = d->profiles.value(currentProfile);
0342         }
0343     }
0344     if (d->currentProfile) {
0345         emit currentProfileChanged();
0346     }
0347 }
0348 
0349 void KisInputProfileManager::saveProfiles()
0350 {
0351     QString storagePath = KoResourcePaths::saveLocation("data", "input/", true);
0352     Q_FOREACH(KisInputProfile * p, d->profiles) {
0353         saveProfile(p, storagePath);
0354     }
0355 
0356     KisConfig config(false);
0357     config.setCurrentInputProfile(d->currentProfile->name());
0358 
0359     //Force a reload of the current profile in input manager and whatever else uses the profile.
0360     emit currentProfileChanged();
0361 }
0362 
0363 void KisInputProfileManager::saveProfile(KisInputProfile *profile, QString storagePath)
0364 {
0365     const QString profilePath = storagePath + d->profileFileName(profile->name());
0366     KConfig config(profilePath, KConfig::SimpleConfig);
0367 
0368     config.group("General").writeEntry("name", profile->name());
0369     config.group("General").writeEntry("version", PROFILE_VERSION);
0370 
0371     Q_FOREACH(KisAbstractInputAction * action, d->actions) {
0372         KConfigGroup grp = config.group(action->id());
0373         grp.deleteGroup(); //Clear the group of any existing shortcuts.
0374 
0375         int index = 0;
0376         QList<KisShortcutConfiguration *> shortcuts = profile->shortcutsForAction(action);
0377         Q_FOREACH(KisShortcutConfiguration * shortcut, shortcuts) {
0378             grp.writeEntry(QString("%1").arg(index++), shortcut->serialize());
0379         }
0380     }
0381 
0382     config.sync();
0383 }
0384 
0385 QList<KisShortcutConfiguration *> KisInputProfileManager::getConflictingShortcuts(KisInputProfile *profile)
0386 {
0387     QSet<KisShortcutConfiguration *> conflictedShortcuts;
0388     const QList<KisShortcutConfiguration *> shortcuts = profile->allShortcuts();
0389     for (auto startIt = shortcuts.constBegin(); startIt != shortcuts.constEnd(); ++startIt) {
0390         KisShortcutConfiguration *first = *startIt;
0391         for (auto index = startIt + 1; index != shortcuts.constEnd(); ++index) {
0392             KisShortcutConfiguration *second = *index;
0393             // since there can be multiple no-ops in the config and because no-ops are something a user can't
0394             // perform, there should be no conflicts.
0395             if (*first == *second && !first->isNoOp()) {
0396                 conflictedShortcuts.insert(first);
0397                 conflictedShortcuts.insert(second);
0398             }
0399         }
0400     }
0401     return conflictedShortcuts.values();
0402 }
0403 
0404 void KisInputProfileManager::resetAll()
0405 {
0406     QString kdeHome = KoResourcePaths::getAppDataLocation();
0407     QStringList profiles = KoResourcePaths::findAllAssets("data", "input/*", KoResourcePaths::Recursive);
0408 
0409     Q_FOREACH (const QString &profile, profiles) {
0410         if(profile.contains(kdeHome)) {
0411             //This is a local file, remove it.
0412             QFile::remove(profile);
0413         }
0414     }
0415 
0416     //Load the profiles again, this should now only load those shipped with Krita.
0417     loadProfiles();
0418 
0419     emit profilesChanged();
0420 }
0421 
0422 KisInputProfileManager::KisInputProfileManager(QObject *parent)
0423     : QObject(parent), d(new Private())
0424 {
0425     d->createActions();
0426 }
0427 
0428 KisInputProfileManager::~KisInputProfileManager()
0429 {
0430     qDeleteAll(d->profiles);
0431     qDeleteAll(d->actions);
0432     delete d;
0433 }
0434 
0435 void KisInputProfileManager::Private::createActions()
0436 {
0437     //TODO: Make this plugin based
0438     //Note that the ordering here determines how things show up in the UI
0439     actions.append(new KisToolInvocationAction());
0440     actions.append(new KisAlternateInvocationAction());
0441     actions.append(new KisChangePrimarySettingAction());
0442     actions.append(new KisPanAction());
0443     actions.append(new KisRotateCanvasAction());
0444     actions.append(new KisZoomAction());
0445     actions.append(new KisPopupWidgetAction());
0446     actions.append(new KisSelectLayerAction());
0447     actions.append(new KisGammaExposureAction());
0448     actions.append(new KisChangeFrameAction());
0449     actions.append(new KisZoomAndRotateAction());
0450     actions.append(new KisTouchGestureAction());
0451 }
0452 
0453 QString KisInputProfileManager::Private::profileFileName(const QString &profileName)
0454 {
0455     return profileName.toLower().remove(QRegExp("[^a-z0-9]")).append(".profile");
0456 }