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.
544 lines
21 KiB
544 lines
21 KiB
/* |
|
Copyright 2007-2008 by Robert Knight <robertknight@gmail.com> |
|
|
|
This program is free software; you can redistribute it and/or modify |
|
it under the terms of the GNU General Public License as published by |
|
the Free Software Foundation; either version 2 of the License, or |
|
(at your option) any later version. |
|
|
|
This program is distributed in the hope that it will be useful, |
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
GNU General Public License for more details. |
|
|
|
You should have received a copy of the GNU General Public License |
|
along with this program; if not, write to the Free Software |
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
|
02110-1301 USA. |
|
*/ |
|
|
|
// Own |
|
#include "ProfileSettings.h" |
|
|
|
// Qt |
|
#include <QFileInfo> |
|
#include <QStandardPaths> |
|
#include <QStandardItem> |
|
#include <QKeyEvent> |
|
|
|
// Konsole |
|
#include "EditProfileDialog.h" |
|
#include "ProfileManager.h" |
|
#include "Session.h" |
|
#include "TerminalDisplay.h" |
|
#include "SessionManager.h" |
|
#include "SessionController.h" |
|
|
|
using namespace Konsole; |
|
|
|
ProfileSettings::ProfileSettings(QWidget* parent) |
|
: QWidget(parent) |
|
, _sessionModel(new QStandardItemModel(this)) |
|
{ |
|
setupUi(this); |
|
|
|
profilesList->setItemDelegateForColumn(ShortcutColumn, new ShortcutItemDelegate(this)); |
|
|
|
// double clicking the profile name opens the profile edit dialog |
|
connect(profilesList, &QAbstractItemView::doubleClicked, this, &Konsole::ProfileSettings::doubleClicked); |
|
|
|
// populate the table with profiles |
|
populateTable(); |
|
|
|
// listen for changes to profiles |
|
connect(ProfileManager::instance(), &Konsole::ProfileManager::profileAdded, this, &Konsole::ProfileSettings::addItems); |
|
connect(ProfileManager::instance(), &Konsole::ProfileManager::profileRemoved, this, &Konsole::ProfileSettings::removeItems); |
|
connect(ProfileManager::instance(), &Konsole::ProfileManager::profileChanged, this, &Konsole::ProfileSettings::updateItems); |
|
connect(ProfileManager::instance(), &Konsole::ProfileManager::favoriteStatusChanged, this, &Konsole::ProfileSettings::updateFavoriteStatus); |
|
|
|
// setup buttons |
|
connect(newProfileButton, &QPushButton::clicked, this, &Konsole::ProfileSettings::createProfile); |
|
connect(editProfileButton, &QPushButton::clicked, this, &Konsole::ProfileSettings::editSelected); |
|
connect(deleteProfileButton, &QPushButton::clicked, this, &Konsole::ProfileSettings::deleteSelected); |
|
connect(setAsDefaultButton, &QPushButton::clicked, this, &Konsole::ProfileSettings::setSelectedAsDefault); |
|
} |
|
|
|
ProfileSettings::~ProfileSettings() = default; |
|
|
|
void ProfileSettings::slotAccepted() |
|
{ |
|
ProfileManager::instance()->saveSettings(); |
|
deleteLater(); |
|
} |
|
|
|
void ProfileSettings::itemDataChanged(QStandardItem* item) |
|
{ |
|
if (item->column() == ShortcutColumn) { |
|
QKeySequence sequence = QKeySequence::fromString(item->text()); |
|
QStandardItem *idItem = item->model()->item(item->row(), ProfileColumn); |
|
ProfileManager::instance()->setShortcut(idItem->data(ProfilePtrRole).value<Profile::Ptr>(), |
|
sequence); |
|
} else if (item->column() == FavoriteStatusColumn) { |
|
QStandardItem *idItem = item->model()->item(item->row(), ProfileColumn); |
|
const bool isFavorite = item->checkState() == Qt::Checked; |
|
ProfileManager::instance()->setFavorite(idItem->data(ProfilePtrRole).value<Profile::Ptr>(), |
|
isFavorite); |
|
updateShortcutField(item->model()->item(item->row(), ShortcutColumn), isFavorite); |
|
} |
|
} |
|
|
|
void ProfileSettings::updateShortcutField(QStandardItem *item, bool isFavorite) const |
|
{ |
|
if(isFavorite) { |
|
item->setToolTip(i18nc("@info:tooltip", "Double click to change shortcut")); |
|
item->setForeground(palette().color(QPalette::Normal, QPalette::Text)); |
|
} else { |
|
item->setToolTip(i18nc("@info:tooltip", "Shortcut won't work while the profile is not marked as visible.")); |
|
item->setForeground(palette().color(QPalette::Disabled, QPalette::Text)); |
|
} |
|
} |
|
|
|
int ProfileSettings::rowForProfile(const Profile::Ptr &profile) const |
|
{ |
|
const int rowCount = _sessionModel->rowCount(); |
|
for (int i = 0; i < rowCount; i++) { |
|
if (_sessionModel->item(i, ProfileColumn)->data(ProfilePtrRole) |
|
.value<Profile::Ptr>() == profile) { |
|
return i; |
|
} |
|
} |
|
return -1; |
|
} |
|
void ProfileSettings::removeItems(const Profile::Ptr &profile) |
|
{ |
|
int row = rowForProfile(profile); |
|
if (row < 0) { |
|
return; |
|
} |
|
|
|
_sessionModel->removeRow(row); |
|
} |
|
void ProfileSettings::updateItems(const Profile::Ptr &profile) |
|
{ |
|
const int row = rowForProfile(profile); |
|
if (row < 0) { |
|
return; |
|
} |
|
|
|
const auto items = QList<QStandardItem*> { |
|
_sessionModel->item(row, FavoriteStatusColumn), |
|
_sessionModel->item(row, ProfileNameColumn), |
|
_sessionModel->item(row, ShortcutColumn), |
|
_sessionModel->item(row, ProfileColumn), |
|
}; |
|
updateItemsForProfile(profile, items); |
|
} |
|
void ProfileSettings::updateItemsForProfile(const Profile::Ptr &profile, const QList<QStandardItem*>& items) const |
|
{ |
|
// "Enabled" checkbox |
|
const auto isEnabled = ProfileManager::instance()->findFavorites().contains(profile); |
|
items[FavoriteStatusColumn]->setCheckState(isEnabled ? Qt::Checked : Qt::Unchecked); |
|
items[FavoriteStatusColumn]->setCheckable(true); |
|
items[FavoriteStatusColumn]->setToolTip( |
|
i18nc("@info:tooltip List item's checkbox for making item (profile) visible in a menu", |
|
"Show profile in menu")); |
|
|
|
// Profile Name |
|
items[ProfileNameColumn]->setText(profile->name()); |
|
if (!profile->icon().isEmpty()) { |
|
items[ProfileNameColumn]->setIcon(QIcon::fromTheme(profile->icon())); |
|
} |
|
// only allow renaming the profile from the edit profile dialog |
|
// so as to use ProfileManager::checkProfileName() |
|
items[ProfileNameColumn]->setEditable(false); |
|
|
|
// Shortcut |
|
const auto shortcut = ProfileManager::instance()->shortcut(profile).toString(); |
|
items[ShortcutColumn]->setText(shortcut); |
|
updateShortcutField(items[ShortcutColumn], isEnabled); |
|
|
|
// Profile ID (pointer to profile) - intended to be hidden in a view |
|
items[ProfileColumn]->setData(QVariant::fromValue(profile), ProfilePtrRole); |
|
} |
|
|
|
void ProfileSettings::doubleClicked(const QModelIndex &index) |
|
{ |
|
QStandardItem *item = _sessionModel->itemFromIndex(index); |
|
if (item->column() == ProfileNameColumn) { |
|
editSelected(); |
|
} |
|
} |
|
|
|
void ProfileSettings::addItems(const Profile::Ptr &profile) |
|
{ |
|
if (profile->isHidden()) { |
|
return; |
|
} |
|
|
|
// each _sessionModel row has three items. |
|
const auto items = QList<QStandardItem*> { |
|
new QStandardItem(), |
|
new QStandardItem(), |
|
new QStandardItem(), |
|
new QStandardItem(), |
|
}; |
|
|
|
updateItemsForProfile(profile, items); |
|
_sessionModel->appendRow(items); |
|
} |
|
void ProfileSettings::populateTable() |
|
{ |
|
Q_ASSERT(!profilesList->model()); |
|
|
|
profilesList->setModel(_sessionModel); |
|
|
|
_sessionModel->clear(); |
|
// setup session table |
|
_sessionModel->setHorizontalHeaderLabels({ |
|
QString(), // set using header item below |
|
i18nc("@title:column Profile name", "Name"), |
|
i18nc("@title:column Profile keyboard shortcut", "Shortcut"), |
|
QString(), |
|
}); |
|
auto *favoriteColumnHeaderItem = new QStandardItem(); |
|
favoriteColumnHeaderItem->setIcon(QIcon::fromTheme(QStringLiteral("visibility"))); |
|
favoriteColumnHeaderItem->setToolTip( |
|
i18nc("@info:tooltip List item's checkbox for making item (profile) visible in a menu", |
|
"Show profile in menu")); |
|
_sessionModel->setHorizontalHeaderItem(FavoriteStatusColumn, favoriteColumnHeaderItem); |
|
|
|
// Calculate favorite column width. resizeColumnToContents() |
|
// is not used because it takes distance between checkbox and |
|
// text into account, but there is no text and it looks weird. |
|
const int headerMargin = style()->pixelMetric(QStyle::PM_HeaderMargin, nullptr, |
|
profilesList->header()); |
|
const int iconWidth = style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, |
|
profilesList->header()); |
|
const int favoriteHeaderWidth = headerMargin * 2 + iconWidth; |
|
QStyleOptionViewItem opt; |
|
opt.features = QStyleOptionViewItem::HasCheckIndicator | QStyleOptionViewItem::HasDecoration; |
|
const QRect checkBoxRect = style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, |
|
&opt, profilesList); |
|
// When right edge is at x < 0 it is assumed the checkbox is |
|
// placed on the right item's side and the margin between right |
|
// checkbox edge and right item edge should be used. |
|
const int checkBoxMargin = checkBoxRect.right() >= 0 ? checkBoxRect.x() |
|
: 0 - checkBoxRect.right(); |
|
const int favoriteItemWidth = checkBoxMargin * 2 + checkBoxRect.width(); |
|
auto *listHeader = profilesList->header(); |
|
|
|
profilesList->setColumnWidth(FavoriteStatusColumn, |
|
qMax(favoriteHeaderWidth, favoriteItemWidth)); |
|
profilesList->resizeColumnToContents(ProfileNameColumn); |
|
listHeader->setSectionResizeMode(FavoriteStatusColumn, QHeaderView::ResizeMode::Fixed); |
|
listHeader->setSectionResizeMode(ProfileNameColumn, QHeaderView::ResizeMode::Stretch); |
|
listHeader->setSectionResizeMode(ShortcutColumn, QHeaderView::ResizeMode::ResizeToContents); |
|
listHeader->setStretchLastSection(false); |
|
listHeader->setSectionsMovable(false); |
|
|
|
profilesList->hideColumn(ProfileColumn); |
|
|
|
QList<Profile::Ptr> profiles = ProfileManager::instance()->allProfiles(); |
|
ProfileManager::instance()->sortProfiles(profiles); |
|
|
|
for (const Profile::Ptr &profile : qAsConst(profiles)) { |
|
addItems(profile); |
|
} |
|
updateDefaultItem(); |
|
|
|
connect(_sessionModel, &QStandardItemModel::itemChanged, this, &Konsole::ProfileSettings::itemDataChanged); |
|
|
|
// listen for changes in the table selection and update the state of the form's buttons |
|
// accordingly. |
|
// |
|
// it appears that the selection model is changed when the model itself is replaced, |
|
// so the signals need to be reconnected each time the model is updated. |
|
connect(profilesList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Konsole::ProfileSettings::tableSelectionChanged); |
|
} |
|
void ProfileSettings::updateDefaultItem() |
|
{ |
|
Profile::Ptr defaultProfile = ProfileManager::instance()->defaultProfile(); |
|
|
|
const QString defaultItemSuffix = i18nc("@item:intable Default list item's name suffix (with separator)", " (default)"); |
|
|
|
const int rowCount = _sessionModel->rowCount(); |
|
for (int i = 0; i < rowCount; i++) { |
|
QStandardItem* item = _sessionModel->item(i, ProfileNameColumn); |
|
QFont itemFont = item->font(); |
|
QStandardItem* profileIdItem = _sessionModel->item(i, ProfileColumn); |
|
auto profile = profileIdItem->data().value<Profile::Ptr>(); |
|
const bool isDefault = (defaultProfile == profile); |
|
const QString cleanItemName = profile != nullptr ? profile->name() : QString(); |
|
|
|
if (isDefault) { |
|
itemFont.setItalic(true); |
|
item->setFont(itemFont); |
|
item->setText(cleanItemName + defaultItemSuffix); |
|
} else { |
|
// FIXME: use default font |
|
itemFont.setItalic(false); |
|
item->setFont(itemFont); |
|
item->setText(cleanItemName); |
|
} |
|
} |
|
} |
|
void ProfileSettings::tableSelectionChanged(const QItemSelection&) |
|
{ |
|
const ProfileManager* manager = ProfileManager::instance(); |
|
bool isNotDefault = true; |
|
bool isDeletable = true; |
|
|
|
const auto profiles = selectedProfiles(); |
|
for (const auto &profile: profiles) { |
|
isNotDefault = isNotDefault && (profile != manager->defaultProfile()); |
|
isDeletable = isDeletable && isProfileDeletable(profile); |
|
} |
|
|
|
newProfileButton->setEnabled(profiles.count() < 2); |
|
// FIXME: At some point editing 2+ profiles no longer works |
|
editProfileButton->setEnabled(profiles.count() == 1); |
|
// do not allow the default session type to be removed |
|
deleteProfileButton->setEnabled(isDeletable && isNotDefault && (profiles.count() > 0)); |
|
setAsDefaultButton->setEnabled(isNotDefault && (profiles.count() == 1)); |
|
} |
|
void ProfileSettings::deleteSelected() |
|
{ |
|
const QList<Profile::Ptr> profiles = selectedProfiles(); |
|
for (const Profile::Ptr &profile : profiles) { |
|
if (profile != ProfileManager::instance()->defaultProfile()) { |
|
ProfileManager::instance()->deleteProfile(profile); |
|
} |
|
} |
|
} |
|
void ProfileSettings::setSelectedAsDefault() |
|
{ |
|
ProfileManager::instance()->setDefaultProfile(currentProfile()); |
|
// do not allow the new default session type to be removed |
|
deleteProfileButton->setEnabled(false); |
|
setAsDefaultButton->setEnabled(false); |
|
|
|
// update font of new default item |
|
updateDefaultItem(); |
|
} |
|
|
|
void ProfileSettings::createProfile() |
|
{ |
|
// setup a temporary profile which is a clone of the selected profile |
|
// or the default if no profile is selected |
|
Profile::Ptr sourceProfile = currentProfile() ? currentProfile() : ProfileManager::instance()->defaultProfile(); |
|
|
|
Q_ASSERT(sourceProfile); |
|
|
|
auto newProfile = Profile::Ptr(new Profile(ProfileManager::instance()->fallbackProfile())); |
|
newProfile->clone(sourceProfile, true); |
|
// TODO: add number suffix when the name is taken |
|
newProfile->setProperty(Profile::Name, i18nc("@item This will be used as part of the file name", "New Profile")); |
|
newProfile->setProperty(Profile::UntranslatedName, QStringLiteral("New Profile")); |
|
newProfile->setProperty(Profile::MenuIndex, QStringLiteral("0")); |
|
|
|
// Consider https://blogs.kde.org/2009/03/26/how-crash-almost-every-qtkde-application-and-how-fix-it-0 before changing the below |
|
QPointer<EditProfileDialog> dialog = new EditProfileDialog(this); |
|
dialog.data()->setProfile(newProfile); |
|
dialog.data()->selectProfileName(); |
|
|
|
if (dialog.data()->exec() == QDialog::Accepted) { |
|
ProfileManager::instance()->addProfile(newProfile); |
|
ProfileManager::instance()->setFavorite(newProfile, true); |
|
ProfileManager::instance()->changeProfile(newProfile, newProfile->setProperties()); |
|
} |
|
delete dialog.data(); |
|
} |
|
void ProfileSettings::editSelected() |
|
{ |
|
const QList<Profile::Ptr> profiles = selectedProfiles(); |
|
EditProfileDialog *profileDialog = nullptr; |
|
// sessions() returns a const QList |
|
for (const Session *session : SessionManager::instance()->sessions()) { |
|
const QList<TerminalDisplay *> viewsList = session->views(); |
|
for (TerminalDisplay *terminalDisplay : viewsList) { |
|
// Searching for opened profiles |
|
profileDialog = terminalDisplay->sessionController()->profileDialogPointer(); |
|
if (profileDialog != nullptr) { |
|
for (const Profile::Ptr &profile : profiles) { |
|
if (profile->name() == profileDialog->lookupProfile()->name() |
|
&& profileDialog->isVisible()) { |
|
// close opened edit dialog |
|
profileDialog->close(); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
EditProfileDialog dialog(this); |
|
// the dialog will delete the profile group when it is destroyed |
|
ProfileGroup* group = new ProfileGroup; |
|
for (const Profile::Ptr &profile : profiles) { |
|
group->addProfile(profile); |
|
} |
|
group->updateValues(); |
|
|
|
dialog.setProfile(Profile::Ptr(group)); |
|
dialog.exec(); |
|
} |
|
QList<Profile::Ptr> ProfileSettings::selectedProfiles() const |
|
{ |
|
QList<Profile::Ptr> list; |
|
QItemSelectionModel* selection = profilesList->selectionModel(); |
|
if (selection == nullptr) { |
|
return list; |
|
} |
|
|
|
const QList<QModelIndex> selectedIndexes = selection->selectedIndexes(); |
|
for (const QModelIndex &index : selectedIndexes) { |
|
if (index.column() == ProfileColumn) { |
|
list << index.data(ProfilePtrRole).value<Profile::Ptr>(); |
|
} |
|
} |
|
|
|
return list; |
|
} |
|
Profile::Ptr ProfileSettings::currentProfile() const |
|
{ |
|
QItemSelectionModel* selection = profilesList->selectionModel(); |
|
|
|
if ((selection == nullptr) || selection->selectedRows().count() != 1) { |
|
return Profile::Ptr(); |
|
} |
|
|
|
return selection-> |
|
selectedIndexes().at(ProfileColumn).data(ProfilePtrRole).value<Profile::Ptr>(); |
|
} |
|
bool ProfileSettings::isProfileDeletable(Profile::Ptr profile) const |
|
{ |
|
if (!profile) { |
|
return false; |
|
} |
|
|
|
const QFileInfo fileInfo(profile->path()); |
|
if (!fileInfo.exists()) { |
|
return false; |
|
} |
|
|
|
const QFileInfo dirInfo(fileInfo.path()); |
|
return dirInfo.isWritable(); |
|
} |
|
void ProfileSettings::updateFavoriteStatus(const Profile::Ptr &profile, bool favorite) |
|
{ |
|
Q_ASSERT(_sessionModel); |
|
|
|
const int rowCount = _sessionModel->rowCount(); |
|
for (int i = 0; i < rowCount; i++) { |
|
auto *item = _sessionModel->item(i, ProfileColumn); |
|
if (item->data(ProfilePtrRole).value<Profile::Ptr>() == profile) { |
|
auto *favoriteItem = _sessionModel->item(i, FavoriteStatusColumn); |
|
favoriteItem->setCheckState(favorite ? Qt::Checked : Qt::Unchecked); |
|
break; |
|
} |
|
} |
|
} |
|
void ProfileSettings::setShortcutEditorVisible(bool visible) |
|
{ |
|
profilesList->setColumnHidden(ShortcutColumn, !visible); |
|
} |
|
void StyledBackgroundPainter::drawBackground(QPainter* painter, const QStyleOptionViewItem& option, |
|
const QModelIndex&) |
|
{ |
|
const auto* opt = qstyleoption_cast<const QStyleOptionViewItem*>(&option); |
|
const QWidget* widget = opt != nullptr ? opt->widget : nullptr; |
|
|
|
QStyle* style = widget != nullptr ? widget->style() : QApplication::style(); |
|
|
|
style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, widget); |
|
} |
|
|
|
ShortcutItemDelegate::ShortcutItemDelegate(QObject* parent) |
|
: QStyledItemDelegate(parent), |
|
_modifiedEditors(QSet<QWidget *>()), |
|
_itemsBeingEdited(QSet<QModelIndex>()) |
|
{ |
|
} |
|
void ShortcutItemDelegate::editorModified() |
|
{ |
|
auto* editor = qobject_cast<FilteredKeySequenceEdit*>(sender()); |
|
Q_ASSERT(editor); |
|
_modifiedEditors.insert(editor); |
|
emit commitData(editor); |
|
emit closeEditor(editor); |
|
} |
|
void ShortcutItemDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, |
|
const QModelIndex& index) const |
|
{ |
|
_itemsBeingEdited.remove(index); |
|
|
|
if (!_modifiedEditors.contains(editor)) { |
|
return; |
|
} |
|
|
|
QString shortcut = qobject_cast<FilteredKeySequenceEdit *>(editor)->keySequence().toString(); |
|
model->setData(index, shortcut, Qt::DisplayRole); |
|
|
|
_modifiedEditors.remove(editor); |
|
} |
|
|
|
QWidget* ShortcutItemDelegate::createEditor(QWidget* aParent, const QStyleOptionViewItem&, const QModelIndex& index) const |
|
{ |
|
_itemsBeingEdited.insert(index); |
|
|
|
auto editor = new FilteredKeySequenceEdit(aParent); |
|
QString shortcutString = index.data(Qt::DisplayRole).toString(); |
|
editor->setKeySequence(QKeySequence::fromString(shortcutString)); |
|
connect(editor, &QKeySequenceEdit::editingFinished, this, &Konsole::ShortcutItemDelegate::editorModified); |
|
editor->setFocus(Qt::FocusReason::MouseFocusReason); |
|
return editor; |
|
} |
|
void ShortcutItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, |
|
const QModelIndex& index) const |
|
{ |
|
if (_itemsBeingEdited.contains(index)) { |
|
StyledBackgroundPainter::drawBackground(painter, option, index); |
|
} else { |
|
QStyledItemDelegate::paint(painter, option, index); |
|
} |
|
} |
|
|
|
QSize Konsole::ShortcutItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const |
|
{ |
|
const QString shortcutString = index.data(Qt::DisplayRole).toString(); |
|
QFontMetrics fm = option.fontMetrics; |
|
|
|
static const int editorMargins = 16; // chosen empirically |
|
const int width = fm.boundingRect(shortcutString + QStringLiteral(", ...")).width() |
|
+ editorMargins; |
|
|
|
return {width, QStyledItemDelegate::sizeHint(option, index).height()}; |
|
} |
|
|
|
void Konsole::ShortcutItemDelegate::destroyEditor(QWidget *editor, const QModelIndex &index) const |
|
{ |
|
_itemsBeingEdited.remove(index); |
|
_modifiedEditors.remove(editor); |
|
editor->deleteLater(); |
|
} |
|
|
|
void Konsole::FilteredKeySequenceEdit::keyPressEvent(QKeyEvent *event) |
|
{ |
|
if(event->modifiers() == Qt::NoModifier) { |
|
switch(event->key()) { |
|
case Qt::Key_Enter: |
|
case Qt::Key_Return: |
|
emit editingFinished(); |
|
return; |
|
case Qt::Key_Backspace: |
|
case Qt::Key_Delete: |
|
clear(); |
|
emit editingFinished(); |
|
event->accept(); |
|
return; |
|
default: |
|
event->accept(); |
|
return; |
|
} |
|
} |
|
QKeySequenceEdit::keyPressEvent(event); |
|
}
|
|
|