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.
 
 
 
 
 

532 lines
16 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>
#include <KSharedConfig>
// 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 Default/fallback profile at the top
if (p1->isFallback()) {
return true;
} else if (p2->isFallback()) {
return false;
}
return stringLessThan(p1->name(), p2->name());
}
ProfileManager::ProfileManager()
{
// load fallback profile
initFallbackProfile();
_defaultProfile = _fallbackProfile;
// lookup the default profile specified in <App>rc
// for stand-alone Konsole, appConfig is just konsolerc
// for konsolepart, appConfig might be yakuakerc, dolphinrc, katerc...
KSharedConfigPtr appConfig = KSharedConfig::openConfig();
KConfigGroup group = appConfig->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);
Q_ASSERT(_profiles.size() > 0);
Q_ASSERT(_defaultProfile);
// get shortcuts and paths of profiles associated with
// them - this doesn't load the shortcuts themselves,
// that is done on-demand.
loadShortcuts();
}
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::initFallbackProfile()
{
_fallbackProfile = Profile::Ptr(new Profile());
_fallbackProfile->useFallback();
addProfile(_fallbackProfile);
}
Profile::Ptr ProfileManager::loadProfile(const QString &shortPath)
{
// the fallback profile has a 'special' path name, "FALLBACK/"
if (shortPath == _fallbackProfile->path()) {
return _fallbackProfile;
}
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 _fallbackProfile;
}
recursionGuard.push(path);
// load the profile
ProfileReader reader;
Profile::Ptr newProfile = Profile::Ptr(new Profile(fallbackProfile()));
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(defaultProfileFileName)) {
_defaultProfile = profile;
}
}
}
void ProfileManager::saveSettings()
{
// save default profile
saveDefaultProfile();
// save shortcuts
saveShortcuts();
// ensure default/shortcuts settings are synced into disk
KSharedConfigPtr appConfig = KSharedConfig::openConfig();
appConfig->sync();
}
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::fallbackProfile() const
{
return _fallbackProfile;
}
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::sorry(nullptr, i18n("Konsole does not have permission to save this profile to %1", newPath));
}
return newPath;
}
void ProfileManager::changeProfile(Profile::Ptr profile, QHash<Profile::Property, QVariant> propertyMap, bool persistent)
{
Q_ASSERT(profile);
const QString origPath = profile->path();
const QString uniqueProfileName = generateUniqueName();
// Don't save a profile with an empty name on disk
persistent = persistent && !profile->name().isEmpty();
bool messageShown = false;
bool isNameChanged = false;
// Insert the changes into the existing Profile instance
for (auto it = propertyMap.cbegin(); it != propertyMap.cend(); ++it) {
const auto property = it.key();
auto value = it.value();
isNameChanged = property == Profile::Name || property == Profile::UntranslatedName;
// "Default" is reserved for the fallback profile, override it;
// The message is only shown if the user manually typed "Default"
// in the name box in the edit profile dialog; i.e. saving the
// fallback profile where the user didn't change the name at all,
// the uniqueProfileName is used silently a couple of lines above.
if (isNameChanged && value == QLatin1String("Default")) {
value = uniqueProfileName;
if (!messageShown) {
KMessageBox::sorry(nullptr,
i18n("The name \"Default\" is reserved for the built-in"
" fallback profile;\nthe profile is going to be"
" saved as \"%1\"",
uniqueProfileName));
messageShown = true;
}
}
profile->setProperty(property, value);
}
// 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 the profile was renamed, after saving the new profile
// delete the old/redundant profile.
// only do this if origPath is not empty, because it's empty
// when creating a new profile, this works around a bug where
// the newly created profile appears twice in the ProfileSettings
// dialog
if (!origPath.isEmpty() && profile->path() != origPath) {
// this is needed to include the old profile too
const QList<Profile::Ptr> availableProfiles = ProfileManager::instance()->allProfiles();
for (const Profile::Ptr &oldProfile : availableProfiles) {
if (oldProfile->path() == origPath) {
// assign the same shortcut of the old profile to
// the newly renamed profile
const auto oldShortcut = shortcut(oldProfile);
if (deleteProfile(oldProfile)) {
setShortcut(profile, oldShortcut);
}
}
}
}
}
if (isNameChanged) {
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);
// 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);
}
QFileInfo fileInfo(path);
KSharedConfigPtr appConfig = KSharedConfig::openConfig();
KConfigGroup group = appConfig->group("Desktop Entry");
group.writeEntry("DefaultProfile", fileInfo.fileName());
}
void ProfileManager::loadShortcuts()
{
KSharedConfigPtr appConfig = KSharedConfig::openConfig();
KConfigGroup shortcutGroup = appConfig->group("Profile Shortcuts");
QMap<QString, QString> entries = shortcutGroup.entryMap();
QMapIterator<QString, QString> iter(entries);
while (iter.hasNext()) {
iter.next();
QKeySequence shortcut = QKeySequence::fromString(iter.key());
QString profilePath = iter.value();
ShortcutData data;
// if the file is not an absolute path, look it up
QFileInfo fileInfo(profilePath);
if (!fileInfo.isAbsolute()) {
profilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("konsole/") + profilePath);
}
data.profilePath = profilePath;
_shortcuts.insert(shortcut, data);
}
}
QString ProfileManager::normalizePath(const QString &path) const
{
QFileInfo fileInfo(path);
const QString location = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("konsole/") + fileInfo.fileName());
return (!fileInfo.isAbsolute()) || location.isEmpty() ? path : fileInfo.fileName();
}
void ProfileManager::saveShortcuts()
{
KSharedConfigPtr appConfig = KSharedConfig::openConfig();
KConfigGroup shortcutGroup = appConfig->group("Profile Shortcuts");
shortcutGroup.deleteGroup();
QMapIterator<QKeySequence, ShortcutData> iter(_shortcuts);
while (iter.hasNext()) {
iter.next();
QString shortcutString = iter.key().toString();
QString profileName = normalizePath(iter.value().profilePath);
shortcutGroup.writeEntry(shortcutString, profileName);
}
}
void ProfileManager::setShortcut(Profile::Ptr profile, const QKeySequence &keySequence)
{
QKeySequence existingShortcut = shortcut(profile);
_shortcuts.remove(existingShortcut);
if (keySequence.isEmpty()) {
return;
}
ShortcutData data;
data.profileKey = profile;
data.profilePath = profile->path();
// TODO - This won't work if the profile doesn't
// have a path yet
_shortcuts.insert(keySequence, data);
Q_EMIT shortcutChanged(profile, keySequence);
}
QKeySequence ProfileManager::shortcut(Profile::Ptr profile) const
{
QMapIterator<QKeySequence, ShortcutData> iter(_shortcuts);
while (iter.hasNext()) {
iter.next();
if (iter.value().profileKey == profile || iter.value().profilePath == profile->path()) {
return iter.key();
}
}
return QKeySequence();
}