You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
494 lines
14 KiB
494 lines
14 KiB
/* |
|
This source file is part of Konsole, a terminal emulator. |
|
|
|
SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
|
|
// Own |
|
#include "ProfileManager.h" |
|
#include "PopStackOnExit.h" |
|
|
|
#include "konsoledebug.h" |
|
|
|
// Qt |
|
#include <QDir> |
|
#include <QFileInfo> |
|
#include <QString> |
|
|
|
// KDE |
|
#include <KConfig> |
|
#include <KConfigGroup> |
|
#include <KLocalizedString> |
|
#include <KMessageBox> |
|
|
|
// Konsole |
|
#include "ProfileGroup.h" |
|
#include "ProfileModel.h" |
|
#include "ProfileReader.h" |
|
#include "ProfileWriter.h" |
|
|
|
using namespace Konsole; |
|
|
|
static bool stringLessThan(const QString &p1, const QString &p2) |
|
{ |
|
return QString::localeAwareCompare(p1, p2) < 0; |
|
} |
|
|
|
static bool profileNameLessThan(const Profile::Ptr &p1, const Profile::Ptr &p2) |
|
{ |
|
// Always put the built-in profile at the top |
|
if (p1->isBuiltin()) { |
|
return true; |
|
} else if (p2->isBuiltin()) { |
|
return false; |
|
} |
|
|
|
return stringLessThan(p1->name(), p2->name()); |
|
} |
|
|
|
ProfileManager::ProfileManager() |
|
: m_config(KSharedConfig::openConfig()) |
|
{ |
|
// ensure built-in profile is available |
|
initBuiltinProfile(); |
|
_defaultProfile = _builtinProfile; |
|
|
|
// lookup the default profile specified in <App>rc |
|
// For stand-alone Konsole, m_config is just "konsolerc" |
|
// For konsolepart, m_config might be "yakuakerc", "dolphinrc", "katerc"... |
|
KConfigGroup group = m_config->group("Desktop Entry"); |
|
QString defaultProfileFileName = group.readEntry("DefaultProfile", ""); |
|
|
|
// if the hosting application of konsolepart does not specify its own |
|
// default profile, use the default profile of stand-alone Konsole. |
|
if (defaultProfileFileName.isEmpty()) { |
|
KSharedConfigPtr konsoleConfig = KSharedConfig::openConfig(QStringLiteral("konsolerc")); |
|
group = konsoleConfig->group("Desktop Entry"); |
|
defaultProfileFileName = group.readEntry("DefaultProfile", ""); |
|
} |
|
|
|
loadAllProfiles(defaultProfileFileName); |
|
loadShortcuts(); |
|
|
|
Q_ASSERT(_profiles.size() > 0); |
|
Q_ASSERT(_defaultProfile); |
|
} |
|
|
|
ProfileManager::~ProfileManager() = default; |
|
|
|
Q_GLOBAL_STATIC(ProfileManager, theProfileManager) |
|
ProfileManager *ProfileManager::instance() |
|
{ |
|
return theProfileManager; |
|
} |
|
|
|
ProfileManager::Iterator ProfileManager::findProfile(const Profile::Ptr &profile) const |
|
{ |
|
return std::find(_profiles.cbegin(), _profiles.cend(), profile); |
|
} |
|
|
|
void ProfileManager::initBuiltinProfile() |
|
{ |
|
_builtinProfile = Profile::Ptr(new Profile()); |
|
_builtinProfile->useBuiltin(); |
|
addProfile(_builtinProfile); |
|
} |
|
|
|
Profile::Ptr ProfileManager::loadProfile(const QString &shortPath) |
|
{ |
|
if (shortPath == builtinProfile()->path()) { |
|
return builtinProfile(); |
|
} |
|
|
|
QString path = shortPath; |
|
|
|
// add a suggested suffix and relative prefix if missing |
|
QFileInfo fileInfo(path); |
|
|
|
if (fileInfo.isDir()) { |
|
return Profile::Ptr(); |
|
} |
|
|
|
if (fileInfo.suffix() != QLatin1String("profile")) { |
|
path.append(QLatin1String(".profile")); |
|
} |
|
if (fileInfo.path().isEmpty() || fileInfo.path() == QLatin1String(".")) { |
|
path.prepend(QLatin1String("konsole") + QDir::separator()); |
|
} |
|
|
|
// if the file is not an absolute path, look it up |
|
if (!fileInfo.isAbsolute()) { |
|
path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, path); |
|
} |
|
|
|
// if the file is not found, return immediately |
|
if (path.isEmpty()) { |
|
return Profile::Ptr(); |
|
} |
|
|
|
// check that we have not already loaded this profile |
|
for (const Profile::Ptr &profile : _profiles) { |
|
if (profile->path() == path) { |
|
return profile; |
|
} |
|
} |
|
|
|
// guard to prevent problems if a profile specifies itself as its parent |
|
// or if there is recursion in the "inheritance" chain |
|
// (eg. two profiles, A and B, specifying each other as their parents) |
|
static QStack<QString> recursionGuard; |
|
PopStackOnExit<QString> popGuardOnExit(recursionGuard); |
|
|
|
if (recursionGuard.contains(path)) { |
|
qCDebug(KonsoleDebug) << "Ignoring attempt to load profile recursively from" << path; |
|
return _builtinProfile; |
|
} |
|
recursionGuard.push(path); |
|
|
|
// load the profile |
|
ProfileReader reader; |
|
|
|
Profile::Ptr newProfile = Profile::Ptr(new Profile(builtinProfile())); |
|
newProfile->setProperty(Profile::Path, path); |
|
|
|
QString parentProfilePath; |
|
bool result = reader.readProfile(path, newProfile, parentProfilePath); |
|
|
|
if (!parentProfilePath.isEmpty()) { |
|
Profile::Ptr parentProfile = loadProfile(parentProfilePath); |
|
newProfile->setParent(parentProfile); |
|
} |
|
|
|
if (!result) { |
|
qCDebug(KonsoleDebug) << "Could not load profile from " << path; |
|
return Profile::Ptr(); |
|
} else if (newProfile->name().isEmpty()) { |
|
qCWarning(KonsoleDebug) << path << " does not have a valid name, ignoring."; |
|
return Profile::Ptr(); |
|
} else { |
|
addProfile(newProfile); |
|
return newProfile; |
|
} |
|
} |
|
QStringList ProfileManager::availableProfilePaths() const |
|
{ |
|
ProfileReader reader; |
|
|
|
QStringList paths; |
|
paths += reader.findProfiles(); |
|
|
|
std::stable_sort(paths.begin(), paths.end(), stringLessThan); |
|
|
|
return paths; |
|
} |
|
|
|
QStringList ProfileManager::availableProfileNames() const |
|
{ |
|
QStringList names; |
|
|
|
const QList<Profile::Ptr> allProfiles = ProfileManager::instance()->allProfiles(); |
|
for (const Profile::Ptr &profile : allProfiles) { |
|
if (!profile->isHidden()) { |
|
names.push_back(profile->name()); |
|
} |
|
} |
|
|
|
std::stable_sort(names.begin(), names.end(), stringLessThan); |
|
|
|
return names; |
|
} |
|
|
|
void ProfileManager::loadAllProfiles(const QString &defaultProfileFileName) |
|
{ |
|
const QStringList &paths = availableProfilePaths(); |
|
for (const QString &path : paths) { |
|
Profile::Ptr profile = loadProfile(path); |
|
if (profile && !defaultProfileFileName.isEmpty() && path.endsWith(QLatin1Char('/') + defaultProfileFileName)) { |
|
_defaultProfile = profile; |
|
} |
|
} |
|
} |
|
|
|
void ProfileManager::saveSettings() |
|
{ |
|
saveShortcuts(); |
|
} |
|
|
|
void ProfileManager::sortProfiles() |
|
{ |
|
std::sort(_profiles.begin(), _profiles.end(), profileNameLessThan); |
|
} |
|
|
|
QList<Profile::Ptr> ProfileManager::allProfiles() |
|
{ |
|
sortProfiles(); |
|
return loadedProfiles(); |
|
} |
|
|
|
QList<Profile::Ptr> ProfileManager::loadedProfiles() const |
|
{ |
|
return {_profiles.cbegin(), _profiles.cend()}; |
|
} |
|
|
|
Profile::Ptr ProfileManager::defaultProfile() const |
|
{ |
|
return _defaultProfile; |
|
} |
|
Profile::Ptr ProfileManager::builtinProfile() const |
|
{ |
|
return _builtinProfile; |
|
} |
|
|
|
QString ProfileManager::generateUniqueName() const |
|
{ |
|
const QStringList existingProfileNames = availableProfileNames(); |
|
int nameSuffix = 1; |
|
QString uniqueProfileName; |
|
do { |
|
uniqueProfileName = QStringLiteral("Profile ") + QString::number(nameSuffix); |
|
++nameSuffix; |
|
} while (existingProfileNames.contains(uniqueProfileName)); |
|
|
|
return uniqueProfileName; |
|
} |
|
|
|
QString ProfileManager::saveProfile(const Profile::Ptr &profile) |
|
{ |
|
ProfileWriter writer; |
|
|
|
QString newPath = writer.getPath(profile); |
|
|
|
if (!writer.writeProfile(newPath, profile)) { |
|
KMessageBox::error(nullptr, i18n("Konsole does not have permission to save this profile to %1", newPath)); |
|
} |
|
|
|
return newPath; |
|
} |
|
|
|
void ProfileManager::changeProfile(Profile::Ptr profile, Profile::PropertyMap propertyMap, bool persistent) |
|
{ |
|
Q_ASSERT(profile); |
|
|
|
const QString origPath = profile->path(); |
|
const QKeySequence origShortcut = shortcut(profile); |
|
const bool isDefaultProfile = profile == defaultProfile(); |
|
|
|
// Don't save a profile with an empty name on disk |
|
persistent = persistent && !profile->name().isEmpty(); |
|
|
|
bool isNameChanged = false; |
|
|
|
// Insert the changes into the existing Profile instance |
|
profile->assignProperties(propertyMap); |
|
|
|
// Check if the name changed |
|
for (auto it = propertyMap.cbegin(); it != propertyMap.cend(); ++it) { |
|
const auto property = it.key(); |
|
isNameChanged |= property == Profile::Name || property == Profile::UntranslatedName; |
|
if (isNameChanged) { |
|
break; |
|
} |
|
} |
|
|
|
// when changing a group, iterate through the profiles |
|
// in the group and call changeProfile() on each of them |
|
// |
|
// this is so that each profile in the group, the profile is |
|
// applied, a change notification is emitted and the profile |
|
// is saved to disk |
|
ProfileGroup::Ptr group = profile->asGroup(); |
|
if (group) { |
|
const QList<Profile::Ptr> profiles = group->profiles(); |
|
for (const Profile::Ptr &groupProfile : profiles) { |
|
changeProfile(groupProfile, propertyMap, persistent); |
|
} |
|
return; |
|
} |
|
|
|
// save changes to disk, unless the profile is hidden, in which case |
|
// it has no file on disk |
|
if (persistent && !profile->isHidden()) { |
|
profile->setProperty(Profile::Path, saveProfile(profile)); |
|
} |
|
|
|
if (isNameChanged) { // Renamed? |
|
// origPath is empty when saving a new profile |
|
if (!origPath.isEmpty()) { |
|
// Delete the old/redundant .profile from disk |
|
QFile::remove(origPath); |
|
|
|
// Change the default profile name to the new one |
|
if (isDefaultProfile) { |
|
setDefaultProfile(profile); |
|
} |
|
|
|
// If the profile had a shortcut, re-assign it to the profile |
|
if (!origShortcut.isEmpty()) { |
|
setShortcut(profile, origShortcut); |
|
} |
|
} |
|
|
|
sortProfiles(); |
|
} |
|
|
|
// Notify the world about the change |
|
Q_EMIT profileChanged(profile); |
|
} |
|
|
|
void ProfileManager::addProfile(const Profile::Ptr &profile) |
|
{ |
|
if (_profiles.empty()) { |
|
_defaultProfile = profile; |
|
} |
|
|
|
if (findProfile(profile) == _profiles.cend()) { |
|
_profiles.push_back(profile); |
|
Q_EMIT profileAdded(profile); |
|
} |
|
} |
|
|
|
bool ProfileManager::deleteProfile(Profile::Ptr profile) |
|
{ |
|
bool wasDefault = (profile == defaultProfile()); |
|
|
|
if (profile) { |
|
// try to delete the config file |
|
if (profile->isPropertySet(Profile::Path) && QFile::exists(profile->path())) { |
|
if (!QFile::remove(profile->path())) { |
|
qCDebug(KonsoleDebug) << "Could not delete profile: " << profile->path() << "The file is most likely in a directory which is read-only."; |
|
|
|
return false; |
|
} |
|
} |
|
|
|
setShortcut(profile, QKeySequence()); |
|
if (auto it = findProfile(profile); it != _profiles.end()) { |
|
_profiles.erase(it); |
|
} |
|
|
|
// mark the profile as hidden so that it does not show up in the |
|
// Manage Profiles dialog and is not saved to disk |
|
profile->setHidden(true); |
|
} |
|
|
|
// If we just deleted the default profile, replace it with the first |
|
// profile in the list. |
|
if (wasDefault) { |
|
const QList<Profile::Ptr> existingProfiles = allProfiles(); |
|
setDefaultProfile(existingProfiles.at(0)); |
|
} |
|
|
|
Q_EMIT profileRemoved(profile); |
|
|
|
return true; |
|
} |
|
|
|
void ProfileManager::setDefaultProfile(const Profile::Ptr &profile) |
|
{ |
|
Q_ASSERT(findProfile(profile) != _profiles.cend()); |
|
|
|
const auto oldDefault = _defaultProfile; |
|
_defaultProfile = profile; |
|
ProfileModel::instance()->setDefault(profile); |
|
|
|
saveDefaultProfile(); |
|
|
|
// Setting/unsetting a profile as the default is a sort of a |
|
// "profile change", useful for updating the icon/font of the |
|
// "default profile in e.g. 'File -> New Tab' menu. |
|
Q_EMIT profileChanged(oldDefault); |
|
Q_EMIT profileChanged(profile); |
|
} |
|
|
|
void ProfileManager::saveDefaultProfile() |
|
{ |
|
QString path = _defaultProfile->path(); |
|
ProfileWriter writer; |
|
|
|
if (path.isEmpty()) { |
|
path = writer.getPath(_defaultProfile); |
|
} |
|
|
|
KConfigGroup group = m_config->group("Desktop Entry"); |
|
group.writeEntry("DefaultProfile", QUrl::fromLocalFile(path).fileName()); |
|
m_config->sync(); |
|
} |
|
|
|
void ProfileManager::loadShortcuts() |
|
{ |
|
KConfigGroup shortcutGroup = m_config->group("Profile Shortcuts"); |
|
|
|
const QLatin1String suffix(".profile"); |
|
auto findByName = [this, suffix](const QString &name) { |
|
return std::find_if(_profiles.cbegin(), _profiles.cend(), [&name, suffix](const Profile::Ptr &p) { |
|
return p->name() == name // |
|
|| (p->name() + suffix) == name; // For backwards compatibility |
|
}); |
|
}; |
|
|
|
const QMap<QString, QString> entries = shortcutGroup.entryMap(); |
|
for (auto it = entries.cbegin(), endIt = entries.cend(); it != endIt; ++it) { |
|
auto profileIt = findByName(it.value()); |
|
if (profileIt == _profiles.cend()) { |
|
continue; |
|
} |
|
|
|
_shortcuts.push_back({*profileIt, QKeySequence::fromString(it.key())}); |
|
} |
|
} |
|
|
|
void ProfileManager::saveShortcuts() |
|
{ |
|
if (_profileShortcutsChanged) { |
|
_profileShortcutsChanged = false; |
|
|
|
KConfigGroup shortcutGroup = m_config->group("Profile Shortcuts"); |
|
shortcutGroup.deleteGroup(); |
|
|
|
for (const auto &[profile, keySeq] : _shortcuts) { |
|
shortcutGroup.writeEntry(keySeq.toString(), profile->name()); |
|
} |
|
|
|
m_config->sync(); |
|
} |
|
} |
|
|
|
void ProfileManager::setShortcut(Profile::Ptr profile, const QKeySequence &keySequence) |
|
{ |
|
_profileShortcutsChanged = true; |
|
QKeySequence existingShortcut = shortcut(profile); |
|
|
|
auto profileIt = std::find_if(_shortcuts.begin(), _shortcuts.end(), [&profile](const ShortcutData &data) { |
|
return data.profileKey == profile; |
|
}); |
|
if (profileIt != _shortcuts.end()) { |
|
// There is a previous shortcut for this profile, replace it with the new one |
|
profileIt->keySeq = keySequence; |
|
Q_EMIT shortcutChanged(profileIt->profileKey, profileIt->keySeq); |
|
} else { |
|
// No previous shortcut for this profile |
|
const ShortcutData &newData = _shortcuts.emplace_back(ShortcutData{profile, keySequence}); |
|
Q_EMIT shortcutChanged(newData.profileKey, newData.keySeq); |
|
} |
|
|
|
auto keySeqIt = std::find_if(_shortcuts.begin(), _shortcuts.end(), [&keySequence, &profile](const ShortcutData &data) { |
|
return data.profileKey != profile && data.keySeq == keySequence; |
|
}); |
|
if (keySeqIt != _shortcuts.end()) { |
|
// There is a profile with shortcut "keySequence" which has been |
|
// associated with another profile >>> unset it |
|
Q_EMIT shortcutChanged(keySeqIt->profileKey, {}); |
|
_shortcuts.erase(keySeqIt); |
|
} |
|
} |
|
|
|
QKeySequence ProfileManager::shortcut(Profile::Ptr profile) const |
|
{ |
|
auto it = std::find_if(_shortcuts.cbegin(), _shortcuts.cend(), [&profile](const ShortcutData &data) { |
|
return data.profileKey == profile; |
|
}); |
|
|
|
return it != _shortcuts.cend() ? it->keySeq : QKeySequence{}; |
|
}
|
|
|