File indexing completed on 2024-04-28 05:50:45
0001 /* 0002 This source file is part of Konsole, a terminal emulator. 0003 0004 SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 // Own 0010 #include "ProfileManager.h" 0011 #include "PopStackOnExit.h" 0012 0013 // Qt 0014 #include <QDir> 0015 #include <QFileInfo> 0016 #include <QString> 0017 #include <QUrl> 0018 0019 // KDE 0020 #include <KConfig> 0021 #include <KConfigGroup> 0022 #include <KLocalizedString> 0023 #include <KMessageBox> 0024 0025 // Konsole 0026 #include "ProfileGroup.h" 0027 #include "ProfileModel.h" 0028 #include "ProfileReader.h" 0029 #include "ProfileWriter.h" 0030 0031 Q_LOGGING_CATEGORY(KonsoleProfileDebug, "org.kde.konsole.profile", QtDebugMsg) 0032 0033 using namespace Konsole; 0034 0035 static bool stringLessThan(const QString &p1, const QString &p2) 0036 { 0037 return QString::localeAwareCompare(p1, p2) < 0; 0038 } 0039 0040 static bool profileNameLessThan(const Profile::Ptr &p1, const Profile::Ptr &p2) 0041 { 0042 // Always put the built-in profile at the top 0043 if (p1->isBuiltin()) { 0044 return true; 0045 } else if (p2->isBuiltin()) { 0046 return false; 0047 } 0048 0049 return stringLessThan(p1->name(), p2->name()); 0050 } 0051 0052 ProfileManager::ProfileManager() 0053 : m_config(KSharedConfig::openConfig()) 0054 { 0055 // ensure built-in profile is available 0056 initBuiltinProfile(); 0057 _defaultProfile = _builtinProfile; 0058 0059 // lookup the default profile specified in <App>rc 0060 // For stand-alone Konsole, m_config is just "konsolerc" 0061 // For konsolepart, m_config might be "yakuakerc", "dolphinrc", "katerc"... 0062 KConfigGroup group = m_config->group(QStringLiteral("Desktop Entry")); 0063 QString defaultProfileFileName = group.readEntry("DefaultProfile", ""); 0064 0065 // if the hosting application of konsolepart does not specify its own 0066 // default profile, use the default profile of stand-alone Konsole. 0067 if (defaultProfileFileName.isEmpty()) { 0068 KSharedConfigPtr konsoleConfig = KSharedConfig::openConfig(QStringLiteral("konsolerc")); 0069 group = konsoleConfig->group(QStringLiteral("Desktop Entry")); 0070 defaultProfileFileName = group.readEntry("DefaultProfile", ""); 0071 } 0072 0073 loadAllProfiles(defaultProfileFileName); 0074 loadShortcuts(); 0075 0076 Q_ASSERT(_profiles.size() > 0); 0077 Q_ASSERT(_defaultProfile); 0078 } 0079 0080 ProfileManager::~ProfileManager() = default; 0081 0082 Q_GLOBAL_STATIC(ProfileManager, theProfileManager) 0083 ProfileManager *ProfileManager::instance() 0084 { 0085 return theProfileManager; 0086 } 0087 0088 ProfileManager::Iterator ProfileManager::findProfile(const Profile::Ptr &profile) const 0089 { 0090 return std::find(_profiles.cbegin(), _profiles.cend(), profile); 0091 } 0092 0093 void ProfileManager::initBuiltinProfile() 0094 { 0095 _builtinProfile = Profile::Ptr(new Profile()); 0096 _builtinProfile->useBuiltin(); 0097 addProfile(_builtinProfile); 0098 } 0099 0100 Profile::Ptr ProfileManager::loadProfile(const QString &shortPath) 0101 { 0102 if (shortPath == builtinProfile()->path()) { 0103 return builtinProfile(); 0104 } 0105 0106 QString path = shortPath; 0107 0108 // add a suggested suffix and relative prefix if missing 0109 QFileInfo fileInfo(path); 0110 0111 if (fileInfo.isDir()) { 0112 return Profile::Ptr(); 0113 } 0114 0115 if (fileInfo.suffix() != QLatin1String("profile")) { 0116 path.append(QLatin1String(".profile")); 0117 } 0118 if (fileInfo.path().isEmpty() || fileInfo.path() == QLatin1String(".")) { 0119 path.prepend(QLatin1String("konsole") + QDir::separator()); 0120 } 0121 0122 // if the file is not an absolute path, look it up 0123 if (!fileInfo.isAbsolute()) { 0124 path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, path); 0125 } 0126 0127 // if the file is not found, return immediately 0128 if (path.isEmpty()) { 0129 return Profile::Ptr(); 0130 } 0131 0132 // check that we have not already loaded this profile 0133 for (const Profile::Ptr &profile : _profiles) { 0134 if (profile->path() == path) { 0135 return profile; 0136 } 0137 } 0138 0139 // guard to prevent problems if a profile specifies itself as its parent 0140 // or if there is recursion in the "inheritance" chain 0141 // (eg. two profiles, A and B, specifying each other as their parents) 0142 static QStack<QString> recursionGuard; 0143 PopStackOnExit<QString> popGuardOnExit(recursionGuard); 0144 0145 if (recursionGuard.contains(path)) { 0146 qCDebug(KonsoleProfileDebug) << "Ignoring attempt to load profile recursively from" << path; 0147 return _builtinProfile; 0148 } 0149 recursionGuard.push(path); 0150 0151 // load the profile 0152 ProfileReader reader; 0153 0154 Profile::Ptr newProfile = Profile::Ptr(new Profile(builtinProfile())); 0155 newProfile->setProperty(Profile::Path, path); 0156 0157 QString parentProfilePath; 0158 bool result = reader.readProfile(path, newProfile, parentProfilePath); 0159 0160 if (!parentProfilePath.isEmpty()) { 0161 Profile::Ptr parentProfile = loadProfile(parentProfilePath); 0162 // To avoid crashes make sure to NOT set an invalid parent 0163 if (parentProfile == nullptr) { 0164 qCDebug(KonsoleProfileDebug) << "Profile " << newProfile->name() << " has an invalid parent " << parentProfilePath; 0165 } else { 0166 newProfile->setParent(parentProfile); 0167 } 0168 } 0169 0170 if (!result) { 0171 qCDebug(KonsoleProfileDebug) << "Could not load profile from " << path; 0172 return Profile::Ptr(); 0173 } else if (newProfile->name().isEmpty()) { 0174 qCWarning(KonsoleProfileDebug) << path << " does not have a valid name, ignoring."; 0175 return Profile::Ptr(); 0176 } else { 0177 addProfile(newProfile); 0178 return newProfile; 0179 } 0180 } 0181 QStringList ProfileManager::availableProfilePaths() const 0182 { 0183 ProfileReader reader; 0184 0185 QStringList paths; 0186 paths += reader.findProfiles(); 0187 0188 std::stable_sort(paths.begin(), paths.end(), stringLessThan); 0189 0190 return paths; 0191 } 0192 0193 QStringList ProfileManager::availableProfileNames() const 0194 { 0195 QStringList names; 0196 0197 const QList<Profile::Ptr> allProfiles = ProfileManager::instance()->allProfiles(); 0198 for (const Profile::Ptr &profile : allProfiles) { 0199 if (!profile->isHidden()) { 0200 names.push_back(profile->name()); 0201 } 0202 } 0203 0204 std::stable_sort(names.begin(), names.end(), stringLessThan); 0205 0206 return names; 0207 } 0208 0209 void ProfileManager::loadAllProfiles(const QString &defaultProfileFileName) 0210 { 0211 const QStringList &paths = availableProfilePaths(); 0212 for (const QString &path : paths) { 0213 Profile::Ptr profile = loadProfile(path); 0214 if (profile && !defaultProfileFileName.isEmpty() && path.endsWith(QLatin1Char('/') + defaultProfileFileName)) { 0215 _defaultProfile = profile; 0216 } 0217 } 0218 } 0219 0220 void ProfileManager::saveSettings() 0221 { 0222 saveShortcuts(); 0223 } 0224 0225 void ProfileManager::sortProfiles() 0226 { 0227 std::sort(_profiles.begin(), _profiles.end(), profileNameLessThan); 0228 } 0229 0230 QList<Profile::Ptr> ProfileManager::allProfiles() 0231 { 0232 sortProfiles(); 0233 return loadedProfiles(); 0234 } 0235 0236 QList<Profile::Ptr> ProfileManager::loadedProfiles() const 0237 { 0238 return {_profiles.cbegin(), _profiles.cend()}; 0239 } 0240 0241 Profile::Ptr ProfileManager::defaultProfile() const 0242 { 0243 return _defaultProfile; 0244 } 0245 Profile::Ptr ProfileManager::builtinProfile() const 0246 { 0247 return _builtinProfile; 0248 } 0249 0250 QString ProfileManager::generateUniqueName() const 0251 { 0252 const QStringList existingProfileNames = availableProfileNames(); 0253 int nameSuffix = 1; 0254 QString uniqueProfileName; 0255 do { 0256 uniqueProfileName = i18nc("New profile name, %1 is a number", "Profile %1", QString::number(nameSuffix)); 0257 ++nameSuffix; 0258 } while (existingProfileNames.contains(uniqueProfileName)); 0259 0260 return uniqueProfileName; 0261 } 0262 0263 QString ProfileManager::saveProfile(const Profile::Ptr &profile) 0264 { 0265 ProfileWriter writer; 0266 0267 QString newPath = writer.getPath(profile); 0268 0269 if (!writer.writeProfile(newPath, profile)) { 0270 KMessageBox::error(nullptr, i18n("Konsole does not have permission to save this profile to %1", newPath)); 0271 } 0272 0273 return newPath; 0274 } 0275 0276 void ProfileManager::changeProfile(Profile::Ptr profile, const Profile::PropertyMap &propertyMap, bool persistent) 0277 { 0278 Q_ASSERT(profile); 0279 0280 const QString origPath = profile->path(); 0281 const QKeySequence origShortcut = shortcut(profile); 0282 const bool isDefaultProfile = profile == defaultProfile(); 0283 0284 // Don't save a profile with an empty name on disk 0285 persistent = persistent && !profile->name().isEmpty(); 0286 0287 // Insert the changes into the existing Profile instance 0288 profile->assignProperties(propertyMap); 0289 0290 const bool isRenamed = std::any_of(propertyMap.cbegin(), propertyMap.cend(), [](const auto &pair) { 0291 Profile::Property prop = pair.first; 0292 return prop == Profile::Name || prop == Profile::UntranslatedName; 0293 }); 0294 0295 // when changing a group, iterate through the profiles 0296 // in the group and call changeProfile() on each of them 0297 // 0298 // this is so that each profile in the group, the profile is 0299 // applied, a change notification is emitted and the profile 0300 // is saved to disk 0301 ProfileGroup::Ptr group = profile->asGroup(); 0302 if (group) { 0303 const QList<Profile::Ptr> profiles = group->profiles(); 0304 for (const Profile::Ptr &groupProfile : profiles) { 0305 changeProfile(groupProfile, propertyMap, persistent); 0306 } 0307 return; 0308 } 0309 0310 // save changes to disk, unless the profile is hidden, in which case 0311 // it has no file on disk 0312 if (persistent && !profile->isHidden()) { 0313 profile->setProperty(Profile::Path, saveProfile(profile)); 0314 } 0315 0316 if (isRenamed) { 0317 // origPath is empty when saving a new profile 0318 if (!origPath.isEmpty()) { 0319 // Delete the old/redundant .profile from disk 0320 QFile::remove(origPath); 0321 0322 // Change the default profile name to the new one 0323 if (isDefaultProfile) { 0324 setDefaultProfile(profile); 0325 } 0326 0327 // If the profile had a shortcut, re-assign it to the profile 0328 if (!origShortcut.isEmpty()) { 0329 setShortcut(profile, origShortcut); 0330 } 0331 } 0332 0333 sortProfiles(); 0334 } 0335 0336 // Notify the world about the change 0337 Q_EMIT profileChanged(profile); 0338 } 0339 0340 void ProfileManager::addProfile(const Profile::Ptr &profile) 0341 { 0342 if (_profiles.empty()) { 0343 _defaultProfile = profile; 0344 } 0345 0346 if (findProfile(profile) == _profiles.cend()) { 0347 _profiles.push_back(profile); 0348 Q_EMIT profileAdded(profile); 0349 } 0350 } 0351 0352 bool ProfileManager::deleteProfile(Profile::Ptr profile) 0353 { 0354 bool wasDefault = (profile == defaultProfile()); 0355 0356 if (profile) { 0357 // try to delete the config file 0358 if (profile->isPropertySet(Profile::Path) && QFile::exists(profile->path())) { 0359 if (!QFile::remove(profile->path())) { 0360 qCDebug(KonsoleProfileDebug) << "Could not delete profile: " << profile->path() << "The file is most likely in a directory which is read-only."; 0361 0362 return false; 0363 } 0364 } 0365 0366 setShortcut(profile, QKeySequence()); 0367 if (auto it = findProfile(profile); it != _profiles.end()) { 0368 _profiles.erase(it); 0369 } 0370 0371 // mark the profile as hidden so that it does not show up in the 0372 // Manage Profiles dialog and is not saved to disk 0373 profile->setHidden(true); 0374 } 0375 0376 // If we just deleted the default profile, replace it with the first 0377 // profile in the list. 0378 if (wasDefault) { 0379 const QList<Profile::Ptr> existingProfiles = allProfiles(); 0380 setDefaultProfile(existingProfiles.at(0)); 0381 } 0382 0383 Q_EMIT profileRemoved(profile); 0384 0385 return true; 0386 } 0387 0388 void ProfileManager::setDefaultProfile(const Profile::Ptr &profile) 0389 { 0390 Q_ASSERT(findProfile(profile) != _profiles.cend()); 0391 0392 const auto oldDefault = _defaultProfile; 0393 _defaultProfile = profile; 0394 ProfileModel::instance()->setDefault(profile); 0395 0396 saveDefaultProfile(); 0397 0398 // Setting/unsetting a profile as the default is a sort of a 0399 // "profile change", useful for updating the icon/font of the 0400 // "default profile in e.g. 'File -> New Tab' menu. 0401 Q_EMIT profileChanged(oldDefault); 0402 Q_EMIT profileChanged(profile); 0403 } 0404 0405 void ProfileManager::saveDefaultProfile() 0406 { 0407 QString path = _defaultProfile->path(); 0408 ProfileWriter writer; 0409 0410 if (path.isEmpty()) { 0411 path = writer.getPath(_defaultProfile); 0412 } 0413 0414 KConfigGroup group = m_config->group(QStringLiteral("Desktop Entry")); 0415 group.writeEntry("DefaultProfile", QUrl::fromLocalFile(path).fileName()); 0416 m_config->sync(); 0417 } 0418 0419 void ProfileManager::loadShortcuts() 0420 { 0421 KConfigGroup shortcutGroup = m_config->group(QStringLiteral("Profile Shortcuts")); 0422 0423 const QLatin1String suffix(".profile"); 0424 auto findByName = [this, suffix](const QString &name) { 0425 return std::find_if(_profiles.cbegin(), _profiles.cend(), [&name, suffix](const Profile::Ptr &p) { 0426 return p->name() == name // 0427 || (p->name() + suffix) == name; // For backwards compatibility 0428 }); 0429 }; 0430 0431 const QMap<QString, QString> entries = shortcutGroup.entryMap(); 0432 for (auto it = entries.cbegin(), endIt = entries.cend(); it != endIt; ++it) { 0433 auto profileIt = findByName(it.value()); 0434 if (profileIt == _profiles.cend()) { 0435 continue; 0436 } 0437 0438 _shortcuts.push_back({*profileIt, QKeySequence::fromString(it.key())}); 0439 } 0440 } 0441 0442 void ProfileManager::saveShortcuts() 0443 { 0444 if (_profileShortcutsChanged) { 0445 _profileShortcutsChanged = false; 0446 0447 KConfigGroup shortcutGroup = m_config->group(QStringLiteral("Profile Shortcuts")); 0448 shortcutGroup.deleteGroup(QLatin1String()); 0449 0450 for (const auto &[profile, keySeq] : _shortcuts) { 0451 shortcutGroup.writeEntry(keySeq.toString(), profile->name()); 0452 } 0453 0454 m_config->sync(); 0455 } 0456 } 0457 0458 void ProfileManager::setShortcut(Profile::Ptr profile, const QKeySequence &keySequence) 0459 { 0460 _profileShortcutsChanged = true; 0461 QKeySequence existingShortcut = shortcut(profile); 0462 0463 auto profileIt = std::find_if(_shortcuts.begin(), _shortcuts.end(), [&profile](const ShortcutData &data) { 0464 return data.profileKey == profile; 0465 }); 0466 if (profileIt != _shortcuts.end()) { 0467 // There is a previous shortcut for this profile, replace it with the new one 0468 profileIt->keySeq = keySequence; 0469 Q_EMIT shortcutChanged(profileIt->profileKey, profileIt->keySeq); 0470 } else { 0471 // No previous shortcut for this profile 0472 const ShortcutData &newData = _shortcuts.emplace_back(ShortcutData{profile, keySequence}); 0473 Q_EMIT shortcutChanged(newData.profileKey, newData.keySeq); 0474 } 0475 0476 auto keySeqIt = std::find_if(_shortcuts.begin(), _shortcuts.end(), [&keySequence, &profile](const ShortcutData &data) { 0477 return data.profileKey != profile && data.keySeq == keySequence; 0478 }); 0479 if (keySeqIt != _shortcuts.end()) { 0480 // There is a profile with shortcut "keySequence" which has been 0481 // associated with another profile >>> unset it 0482 Q_EMIT shortcutChanged(keySeqIt->profileKey, {}); 0483 _shortcuts.erase(keySeqIt); 0484 } 0485 } 0486 0487 QKeySequence ProfileManager::shortcut(Profile::Ptr profile) const 0488 { 0489 auto it = std::find_if(_shortcuts.cbegin(), _shortcuts.cend(), [&profile](const ShortcutData &data) { 0490 return data.profileKey == profile; 0491 }); 0492 0493 return it != _shortcuts.cend() ? it->keySeq : QKeySequence{}; 0494 } 0495 0496 // ProfileTest is sourcing profile/ProfileManager.cpp to access helper methods 0497 // automoc though does not support/detect that scenario, 0498 // so only generates the moc file in the autogen include folder for the normal source build, but not for the test 0499 // TODO: move internal methods into separate sources and bundle that into the test, or statically export methods 0500 // #include "moc_ProfileManager.cpp"