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 }