diff --git a/src/ColorSchemeEditor.cpp b/src/ColorSchemeEditor.cpp index 43a909c9..809238cc 100644 --- a/src/ColorSchemeEditor.cpp +++ b/src/ColorSchemeEditor.cpp @@ -1,347 +1,347 @@ /* Copyright 2007-2008 by Robert Knight 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 "ColorSchemeEditor.h" // Qt #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include // Konsole #include "ui_ColorSchemeEditor.h" #include "ColorScheme.h" #include "CharacterColor.h" #include "Shortcut_p.h" using namespace Konsole; // colorTable is half the length of _table in ColorScheme class // since intense colors are in a separated column const int COLOR_TABLE_ROW_LENGTH = TABLE_COLORS / 3; const int NAME_COLUMN = 0; // column 0 : color names const int COLOR_COLUMN = 1; // column 1 : actual colors const int INTENSE_COLOR_COLUMN = 2; // column 2 : intense colors const int FAINT_COLOR_COLUMN = 3; // column 2 : faint colors ColorSchemeEditor::ColorSchemeEditor(QWidget *parent) : QDialog(parent), _isNewScheme(false), _ui(nullptr), _colors(nullptr) { auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::Apply); auto mainWidget = new QWidget(this); auto mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); connect(buttonBox, &QDialogButtonBox::accepted, this, &ColorSchemeEditor::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &ColorSchemeEditor::reject); mainLayout->addWidget(buttonBox); connect(buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &Konsole::ColorSchemeEditor::saveColorScheme); connect(okButton, &QPushButton::clicked, this, &Konsole::ColorSchemeEditor::saveColorScheme); // ui _ui = new Ui::ColorSchemeEditor(); _ui->setupUi(mainWidget); // description edit _ui->descriptionEdit->setClearButtonEnabled(true); connect(_ui->descriptionEdit, &QLineEdit::textChanged, this, &Konsole::ColorSchemeEditor::setDescription); // transparency slider QFontMetrics metrics(font()); - _ui->transparencyPercentLabel->setMinimumWidth(metrics.width(QStringLiteral("100%"))); + _ui->transparencyPercentLabel->setMinimumWidth(metrics.boundingRect(QStringLiteral("100%")).width()); connect(_ui->transparencySlider, &QSlider::valueChanged, this, &Konsole::ColorSchemeEditor::setTransparencyPercentLabel); // blur behind window connect(_ui->blurCheckBox, &QCheckBox::toggled, this, &Konsole::ColorSchemeEditor::setBlur); // randomized background connect(_ui->randomizedBackgroundCheck, &QCheckBox::toggled, this, &Konsole::ColorSchemeEditor::setRandomizedBackgroundColor); // wallpaper stuff auto dirModel = new QFileSystemModel(this); dirModel->setFilter(QDir::AllEntries); dirModel->setRootPath(QStringLiteral("/")); auto completer = new QCompleter(this); completer->setModel(dirModel); _ui->wallpaperPath->setCompleter(completer); _ui->wallpaperPath->setClearButtonEnabled(true); _ui->wallpaperSelectButton->setIcon(QIcon::fromTheme(QStringLiteral("image-x-generic"))); connect(_ui->wallpaperSelectButton, &QToolButton::clicked, this, &Konsole::ColorSchemeEditor::selectWallpaper); connect(_ui->wallpaperPath, &QLineEdit::textChanged, this, &Konsole::ColorSchemeEditor::wallpaperPathChanged); // color table _ui->colorTable->setColumnCount(4); _ui->colorTable->setRowCount(COLOR_TABLE_ROW_LENGTH); QStringList labels; labels << i18nc("@label:listbox Column header text for color names", "Name") << i18nc("@label:listbox Column header text for the actual colors", "Color") << i18nc("@label:listbox Column header text for the actual intense colors", "Intense color") << i18nc("@label:listbox Column header text for the actual faint colors", "Faint color"); _ui->colorTable->setHorizontalHeaderLabels(labels); // Set resize mode for colorTable columns _ui->colorTable->horizontalHeader()->setSectionResizeMode(NAME_COLUMN, QHeaderView::ResizeToContents); _ui->colorTable->horizontalHeader()->setSectionResizeMode(COLOR_COLUMN, QHeaderView::Stretch); _ui->colorTable->horizontalHeader()->setSectionResizeMode(INTENSE_COLOR_COLUMN, QHeaderView::Stretch); _ui->colorTable->horizontalHeader()->setSectionResizeMode(FAINT_COLOR_COLUMN, QHeaderView::Stretch); QTableWidgetItem *item = new QTableWidgetItem(QStringLiteral("Test")); _ui->colorTable->setItem(0, 0, item); _ui->colorTable->verticalHeader()->hide(); connect(_ui->colorTable, &QTableWidget::itemClicked, this, &Konsole::ColorSchemeEditor::editColorItem); // warning label when transparency is not available _ui->transparencyWarningWidget->setWordWrap(true); _ui->transparencyWarningWidget->setCloseButtonVisible(false); _ui->transparencyWarningWidget->setMessageType(KMessageWidget::Warning); if (KWindowSystem::compositingActive()) { _ui->transparencyWarningWidget->setVisible(false); } else { _ui->transparencyWarningWidget->setText(i18nc("@info:status", "The background transparency setting will not" " be used because your desktop does not appear to support" " transparent windows.")); } } ColorSchemeEditor::~ColorSchemeEditor() { delete _colors; delete _ui; } void ColorSchemeEditor::editColorItem(QTableWidgetItem *item) { // ignore if this is not a color column if (item->column() != COLOR_COLUMN && item->column() != INTENSE_COLOR_COLUMN && item->column() != FAINT_COLOR_COLUMN) { return; } QColor color = item->background().color(); color = QColorDialog::getColor(color); if (color.isValid()) { item->setBackground(color); int colorSchemeRow = item->row(); // Intense colors row are in the middle third of the color table if (item->column() == INTENSE_COLOR_COLUMN) { colorSchemeRow += COLOR_TABLE_ROW_LENGTH; } // and the faint color rows are in the middle third of the color table if (item->column() == FAINT_COLOR_COLUMN) { colorSchemeRow += 2*COLOR_TABLE_ROW_LENGTH; } ColorEntry entry(_colors->colorEntry(colorSchemeRow)); entry = color; _colors->setColorTableEntry(colorSchemeRow, entry); emit colorsChanged(_colors); } } void ColorSchemeEditor::selectWallpaper() { // Get supported image formats and convert to QString for getOpenFileName() const QList mimeTypes = QImageReader::supportedImageFormats(); QString fileFormats = QStringLiteral("("); for (const QByteArray &mime : mimeTypes) { fileFormats += QStringLiteral("*.%1 ").arg(QLatin1String(mime)); } fileFormats += QLatin1String(")"); const QString fileName = QFileDialog::getOpenFileName(this, i18nc("@title:window", "Select wallpaper image file"), _ui->wallpaperPath->text(), i18nc("@label:textbox Filter in file open dialog", "Supported Images") + fileFormats); if (!fileName.isEmpty()) { _ui->wallpaperPath->setText(fileName); } } void ColorSchemeEditor::wallpaperPathChanged(const QString &path) { if (path.isEmpty()) { _colors->setWallpaper(path); } else { QFileInfo i(path); if (i.exists() && i.isFile() && i.isReadable()) { _colors->setWallpaper(path); } } } void ColorSchemeEditor::setDescription(const QString &description) { if (_colors != nullptr) { _colors->setDescription(description); } if (_ui->descriptionEdit->text() != description) { _ui->descriptionEdit->setText(description); } } void ColorSchemeEditor::setTransparencyPercentLabel(int percent) { _ui->transparencyPercentLabel->setText(QStringLiteral("%1%").arg(percent)); const qreal opacity = (100.0 - percent) / 100.0; _colors->setOpacity(opacity); } void ColorSchemeEditor::setBlur(bool blur) { _colors->setBlur(blur); } void ColorSchemeEditor::setRandomizedBackgroundColor(bool randomized) { _colors->setRandomizedBackgroundColor(randomized); } void ColorSchemeEditor::setup(const ColorScheme *scheme, bool isNewScheme) { _isNewScheme = isNewScheme; delete _colors; _colors = new ColorScheme(*scheme); if (_isNewScheme) { setWindowTitle(i18nc("@title:window", "New Color Scheme")); setDescription(QStringLiteral("New Color Scheme")); } else { setWindowTitle(i18nc("@title:window", "Edit Color Scheme")); } // setup description edit _ui->descriptionEdit->setText(_colors->description()); // setup color table setupColorTable(_colors); // setup transparency slider const int transparencyPercent = qRound((1 - _colors->opacity()) * 100); _ui->transparencySlider->setValue(transparencyPercent); setTransparencyPercentLabel(transparencyPercent); // blur behind window checkbox _ui->blurCheckBox->setChecked(scheme->blur()); // randomized background color checkbox _ui->randomizedBackgroundCheck->setChecked(scheme->randomizedBackgroundColor()); // wallpaper stuff _ui->wallpaperPath->setText(scheme->wallpaper()->path()); } void ColorSchemeEditor::setupColorTable(const ColorScheme *colors) { ColorEntry table[TABLE_COLORS]; colors->getColorTable(table); for (int row = 0; row < COLOR_TABLE_ROW_LENGTH; row++) { QTableWidgetItem *nameItem = new QTableWidgetItem(ColorScheme::translatedColorNameForIndex(row)); nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable); auto colorItem = new QTableWidgetItem(); colorItem->setBackground(table[row]); colorItem->setFlags(colorItem->flags() & ~Qt::ItemIsEditable & ~Qt::ItemIsSelectable); colorItem->setToolTip(i18nc("@info:tooltip", "Click to choose color")); auto colorItemIntense = new QTableWidgetItem(); colorItemIntense->setBackground(table[COLOR_TABLE_ROW_LENGTH + row]); colorItemIntense->setFlags(colorItem->flags() & ~Qt::ItemIsEditable & ~Qt::ItemIsSelectable); colorItemIntense->setToolTip(i18nc("@info:tooltip", "Click to choose intense color")); auto colorItemFaint = new QTableWidgetItem(); colorItemFaint->setBackground(table[2*COLOR_TABLE_ROW_LENGTH + row]); colorItemFaint->setFlags(colorItem->flags() & ~Qt::ItemIsEditable & ~Qt::ItemIsSelectable); colorItemFaint->setToolTip(i18nc("@info:tooltip", "Click to choose Faint color")); _ui->colorTable->setItem(row, NAME_COLUMN, nameItem); _ui->colorTable->setItem(row, COLOR_COLUMN, colorItem); _ui->colorTable->setItem(row, INTENSE_COLOR_COLUMN, colorItemIntense); _ui->colorTable->setItem(row, FAINT_COLOR_COLUMN, colorItemFaint); } // ensure that color names are as fully visible as possible _ui->colorTable->resizeColumnToContents(0); } ColorScheme &ColorSchemeEditor::colorScheme() const { return *_colors; } bool ColorSchemeEditor::isNewScheme() const { return _isNewScheme; } void ColorSchemeEditor::saveColorScheme() { emit colorSchemeSaveRequested(colorScheme(), _isNewScheme); } diff --git a/src/EditProfileDialog.cpp b/src/EditProfileDialog.cpp index 0df9a56b..ed56e0cc 100644 --- a/src/EditProfileDialog.cpp +++ b/src/EditProfileDialog.cpp @@ -1,1955 +1,1955 @@ /* Copyright 2007-2008 by Robert Knight Copyright 2018 by Harald Sitter 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 "EditProfileDialog.h" // Standard #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include // Konsole #include "ui_EditProfileGeneralPage.h" #include "ui_EditProfileTabsPage.h" #include "ui_EditProfileAppearancePage.h" #include "ui_EditProfileScrollingPage.h" #include "ui_EditProfileKeyboardPage.h" #include "ui_EditProfileMousePage.h" #include "ui_EditProfileAdvancedPage.h" #include "ColorSchemeManager.h" #include "KeyBindingEditor.h" #include "KeyboardTranslator.h" #include "KeyboardTranslatorManager.h" #include "ProfileManager.h" #include "ShellCommand.h" #include "WindowSystemInfo.h" #include "Shortcut_p.h" #include "FontDialog.h" using namespace Konsole; EditProfileDialog::EditProfileDialog(QWidget *parent) : KPageDialog(parent) , _generalUi(nullptr) , _tabsUi(nullptr) , _appearanceUi(nullptr) , _scrollingUi(nullptr) , _keyboardUi(nullptr) , _mouseUi(nullptr) , _advancedUi(nullptr) , _tempProfile(nullptr) , _profile(nullptr) , _previewedProperties(QHash()) , _delayedPreviewProperties(QHash()) , _delayedPreviewTimer(new QTimer(this)) , _colorDialog(nullptr) , _buttonBox(nullptr) , _fontDialog(nullptr) { setWindowTitle(i18n("Edit Profile")); setFaceType(KPageDialog::List); _buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply); setButtonBox(_buttonBox); QPushButton *okButton = _buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); connect(_buttonBox, &QDialogButtonBox::accepted, this, &Konsole::EditProfileDialog::accept); connect(_buttonBox, &QDialogButtonBox::rejected, this, &Konsole::EditProfileDialog::reject); // disable the apply button , since no modification has been made _buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); connect(_buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &Konsole::EditProfileDialog::apply); connect(_delayedPreviewTimer, &QTimer::timeout, this, &Konsole::EditProfileDialog::delayedPreviewActivate); // Set a fallback icon for non-plasma desktops as this dialog looks // terrible without all the icons on the left sidebar. On GTK related // desktops, this dialog look good enough without installing // oxygen-icon-theme, qt5ct and setting export QT_QPA_PLATFORMTHEME=qt5ct // Plain Xorg desktops still look terrible as there are no icons // visible. const auto defaultIcon = QIcon::fromTheme(QStringLiteral("utilities-terminal")); // General page const QString generalPageName = i18nc("@title:tab Generic, common options", "General"); auto *generalPageWidget = new QWidget(this); _generalUi = new Ui::EditProfileGeneralPage(); _generalUi->setupUi(generalPageWidget); auto *generalPageItem = addPage(generalPageWidget, generalPageName); generalPageItem->setHeader(generalPageName); generalPageItem->setIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal"))); _pages[generalPageItem] = Page(&EditProfileDialog::setupGeneralPage); // Tabs page const QString tabsPageName = i18n("Tabs"); auto *tabsPageWidget = new QWidget(this); _tabsUi = new Ui::EditProfileTabsPage(); _tabsUi->setupUi(tabsPageWidget); auto *tabsPageItem = addPage(tabsPageWidget, tabsPageName); tabsPageItem->setHeader(tabsPageName); tabsPageItem->setIcon(QIcon::fromTheme(QStringLiteral("tab-duplicate"), defaultIcon)); _pages[tabsPageItem] = Page(&EditProfileDialog::setupTabsPage); LabelsAligner tabsAligner(tabsPageWidget); tabsAligner.addLayout(dynamic_cast(_tabsUi->tabMonitoringGroup->layout())); tabsAligner.addLayout(dynamic_cast(_tabsUi->renameTabWidget->layout())); tabsAligner.updateLayouts(); tabsAligner.align(); // Appearance page const QString appearancePageName = i18n("Appearance"); auto *appearancePageWidget = new QWidget(this); _appearanceUi = new Ui::EditProfileAppearancePage(); _appearanceUi->setupUi(appearancePageWidget); auto *appearancePageItem = addPage(appearancePageWidget, appearancePageName); appearancePageItem->setHeader(appearancePageName); appearancePageItem->setIcon(QIcon::fromTheme(QStringLiteral("kcolorchooser"), defaultIcon)); _pages[appearancePageItem] = Page(&EditProfileDialog::setupAppearancePage); LabelsAligner appearanceAligner(appearancePageWidget); appearanceAligner.addLayout(dynamic_cast(_appearanceUi->miscTabLayout)); appearanceAligner.addLayout(dynamic_cast(_appearanceUi->contentsGroup->layout())); appearanceAligner.updateLayouts(); appearanceAligner.align(); // Scrolling page const QString scrollingPageName = i18n("Scrolling"); auto *scrollingPageWidget = new QWidget(this); _scrollingUi = new Ui::EditProfileScrollingPage(); _scrollingUi->setupUi(scrollingPageWidget); auto *scrollingPageItem = addPage(scrollingPageWidget, scrollingPageName); scrollingPageItem->setHeader(scrollingPageName); scrollingPageItem->setIcon(QIcon::fromTheme(QStringLiteral("transform-move-vertical"), defaultIcon)); _pages[scrollingPageItem] = Page(&EditProfileDialog::setupScrollingPage); // adjust "history size" label height to match history size widget's first radio button _scrollingUi->historySizeLabel->setFixedHeight(_scrollingUi->historySizeWidget->preferredLabelHeight()); // Keyboard page const QString keyboardPageName = i18n("Keyboard"); const QString keyboardPageTitle = i18n("Key bindings"); auto *keyboardPageWidget = new QWidget(this); _keyboardUi = new Ui::EditProfileKeyboardPage(); _keyboardUi->setupUi(keyboardPageWidget); auto *keyboardPageItem = addPage(keyboardPageWidget, keyboardPageName); keyboardPageItem->setHeader(keyboardPageTitle); keyboardPageItem->setIcon(QIcon::fromTheme(QStringLiteral("input-keyboard"), defaultIcon)); _pages[keyboardPageItem] = Page(&EditProfileDialog::setupKeyboardPage); // Mouse page const QString mousePageName = i18n("Mouse"); auto *mousePageWidget = new QWidget(this); _mouseUi = new Ui::EditProfileMousePage(); _mouseUi->setupUi(mousePageWidget); auto *mousePageItem = addPage(mousePageWidget, mousePageName); mousePageItem->setHeader(mousePageName); mousePageItem->setIcon(QIcon::fromTheme(QStringLiteral("input-mouse"), defaultIcon)); _pages[mousePageItem] = Page(&EditProfileDialog::setupMousePage); // Advanced page const QString advancedPageName = i18nc("@title:tab Complex options", "Advanced"); auto *advancedPageWidget = new QWidget(this); _advancedUi = new Ui::EditProfileAdvancedPage(); _advancedUi->setupUi(advancedPageWidget); auto *advancedPageItem = addPage(advancedPageWidget, advancedPageName); advancedPageItem->setHeader(advancedPageName); advancedPageItem->setIcon(QIcon::fromTheme(QStringLiteral("configure"), defaultIcon)); _pages[advancedPageItem] = Page(&EditProfileDialog::setupAdvancedPage); // there are various setupXYZPage() methods to load the items // for each page and update their states to match the profile // being edited. // // these are only called when needed ( ie. when the user clicks // the tab to move to that page ). // // the _pageNeedsUpdate vector keeps track of the pages that have // not been updated since the last profile change and will need // to be refreshed when the user switches to them connect(this, &KPageDialog::currentPageChanged, this, &Konsole::EditProfileDialog::preparePage); createTempProfile(); } EditProfileDialog::~EditProfileDialog() { delete _generalUi; delete _tabsUi; delete _appearanceUi; delete _scrollingUi; delete _keyboardUi; delete _mouseUi; delete _advancedUi; } void EditProfileDialog::save() { if (_tempProfile->isEmpty()) { return; } ProfileManager::instance()->changeProfile(_profile, _tempProfile->setProperties()); // ensure that these settings are not undone by a call // to unpreview() QHashIterator iter(_tempProfile->setProperties()); while (iter.hasNext()) { iter.next(); _previewedProperties.remove(iter.key()); } createTempProfile(); _buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); } void EditProfileDialog::reject() { unpreviewAll(); QDialog::reject(); } void EditProfileDialog::accept() { // if the Apply button is disabled then no settings were changed // or the changes have already been saved by apply() if (_buttonBox->button(QDialogButtonBox::Apply)->isEnabled()) { if (!isValidProfileName()) { return; } save(); } unpreviewAll(); QDialog::accept(); } void EditProfileDialog::apply() { if (!isValidProfileName()) { return; } save(); } bool EditProfileDialog::isValidProfileName() { Q_ASSERT(_profile); Q_ASSERT(_tempProfile); // check whether the user has enough permissions to save the profile QFileInfo fileInfo(_profile->path()); if (fileInfo.exists() && !fileInfo.isWritable()) { if (!_tempProfile->isPropertySet(Profile::Name) || (_tempProfile->name() == _profile->name())) { KMessageBox::sorry(this, i18n("

Konsole does not have permission to save this profile to:
\"%1\"

" "

To be able to save settings you can either change the permissions " "of the profile configuration file or change the profile name to save " "the settings to a new profile.

", _profile->path())); return false; } } const QList existingProfiles = ProfileManager::instance()->allProfiles(); QStringList otherExistingProfileNames; foreach(auto existingProfile, existingProfiles) { if (existingProfile->name() != _profile->name()) { otherExistingProfileNames.append(existingProfile->name()); } } if ((_tempProfile->isPropertySet(Profile::Name) && _tempProfile->name().isEmpty()) || (_profile->name().isEmpty() && _tempProfile->name().isEmpty())) { KMessageBox::sorry(this, i18n("

Each profile must have a name before it can be saved " "into disk.

")); // revert the name in the dialog _generalUi->profileNameEdit->setText(_profile->name()); selectProfileName(); return false; } else if (!_tempProfile->name().isEmpty() && otherExistingProfileNames.contains(_tempProfile->name())) { KMessageBox::sorry(this, i18n("

A profile with this name already exists.

")); // revert the name in the dialog _generalUi->profileNameEdit->setText(_profile->name()); selectProfileName(); return false; } else { return true; } } QString EditProfileDialog::groupProfileNames(const ProfileGroup::Ptr &group, int maxLength) { QString caption; int count = group->profiles().count(); for (int i = 0; i < count; i++) { caption += group->profiles()[i]->name(); if (i < (count - 1)) { caption += QLatin1Char(','); // limit caption length to prevent very long window titles if (maxLength > 0 && caption.length() > maxLength) { caption += QLatin1String("..."); break; } } } return caption; } void EditProfileDialog::updateCaption(const Profile::Ptr &profile) { const int MAX_GROUP_CAPTION_LENGTH = 25; ProfileGroup::Ptr group = profile->asGroup(); if (group && group->profiles().count() > 1) { QString caption = groupProfileNames(group, MAX_GROUP_CAPTION_LENGTH); setWindowTitle(i18np("Editing profile: %2", "Editing %1 profiles: %2", group->profiles().count(), caption)); } else { setWindowTitle(i18n("Edit Profile \"%1\"", profile->name())); } } void EditProfileDialog::setProfile(const Konsole::Profile::Ptr &profile) { Q_ASSERT(profile); _profile = profile; // update caption updateCaption(profile); // mark each page of the dialog as out of date // and force an update of the currently visible page // // the other pages will be updated as necessary for (Page &page: _pages) { page.needsUpdate = true; } preparePage(currentPage()); if (_tempProfile) { createTempProfile(); } } const Profile::Ptr EditProfileDialog::lookupProfile() const { return _profile; } const QString EditProfileDialog::currentColorSchemeName() const { const QString ¤tColorSchemeName = lookupProfile()->colorScheme(); return currentColorSchemeName; } void EditProfileDialog::preparePage(KPageWidgetItem *current, KPageWidgetItem *before) { Q_UNUSED(before); Q_ASSERT(current); Q_ASSERT(_pages.contains(current)); const Profile::Ptr profile = lookupProfile(); auto setupPage = _pages[current].setupPage; Q_ASSERT(profile); Q_ASSERT(setupPage); if (_pages[current].needsUpdate) { (*this.*setupPage)(profile); _pages[current].needsUpdate = false; } } void Konsole::EditProfileDialog::selectProfileName() { _generalUi->profileNameEdit->setFocus(); _generalUi->profileNameEdit->selectAll(); } void EditProfileDialog::setupGeneralPage(const Profile::Ptr &profile) { // basic profile options { ProfileGroup::Ptr group = profile->asGroup(); if (!group || group->profiles().count() < 2) { _generalUi->profileNameEdit->setText(profile->name()); _generalUi->profileNameEdit->setClearButtonEnabled(true); } else { _generalUi->profileNameEdit->setText(groupProfileNames(group, -1)); _generalUi->profileNameEdit->setEnabled(false); } } ShellCommand command(profile->command(), profile->arguments()); _generalUi->commandEdit->setText(command.fullCommand()); // If a "completion" is requested, consider changing this to KLineEdit // and using KCompletion. _generalUi->initialDirEdit->setText(profile->defaultWorkingDirectory()); _generalUi->initialDirEdit->setClearButtonEnabled(true); _generalUi->initialDirEdit->setPlaceholderText(QStandardPaths::standardLocations(QStandardPaths::HomeLocation).value(0)); _generalUi->dirSelectButton->setIcon(QIcon::fromTheme(QStringLiteral("folder-open"))); _generalUi->iconSelectButton->setIcon(QIcon::fromTheme(profile->icon())); _generalUi->startInSameDirButton->setChecked(profile->startInCurrentSessionDir()); // initial terminal size const auto colsSuffix = ki18ncp("Suffix of the number of columns (N columns)", " column", " columns"); const auto rowsSuffix = ki18ncp("Suffix of the number of rows (N rows)", " row", " rows"); _generalUi->terminalColumnsEntry->setValue(profile->terminalColumns()); _generalUi->terminalRowsEntry->setValue(profile->terminalRows()); _generalUi->terminalColumnsEntry->setSuffix(colsSuffix); _generalUi->terminalRowsEntry->setSuffix(rowsSuffix); // make width of initial terminal size spinboxes equal const int sizeEntryWidth = qMax(maxSpinBoxWidth(_generalUi->terminalColumnsEntry, colsSuffix), maxSpinBoxWidth(_generalUi->terminalRowsEntry, rowsSuffix)); _generalUi->terminalColumnsEntry->setFixedWidth(sizeEntryWidth); _generalUi->terminalRowsEntry->setFixedWidth(sizeEntryWidth); // signals and slots connect(_generalUi->dirSelectButton, &QToolButton::clicked, this, &Konsole::EditProfileDialog::selectInitialDir); connect(_generalUi->iconSelectButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::selectIcon); connect(_generalUi->startInSameDirButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::startInSameDir); connect(_generalUi->profileNameEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::profileNameChanged); connect(_generalUi->initialDirEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::initialDirChanged); connect(_generalUi->commandEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::commandChanged); connect(_generalUi->environmentEditButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::showEnvironmentEditor); connect(_generalUi->terminalColumnsEntry, QOverload::of(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::terminalColumnsEntryChanged); connect(_generalUi->terminalRowsEntry, QOverload::of(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::terminalRowsEntryChanged); } void EditProfileDialog::showEnvironmentEditor() { bool ok; const Profile::Ptr profile = lookupProfile(); QStringList currentEnvironment; // The user could re-open the environment editor before clicking // OK/Apply in the parent edit profile dialog, so we make sure // to show the new environment vars if (_tempProfile->isPropertySet(Profile::Environment)) { currentEnvironment = _tempProfile->environment(); } else { currentEnvironment = profile->environment(); } QString text = QInputDialog::getMultiLineText(this, i18n("Edit Environment"), i18n("One environment variable per line"), currentEnvironment.join(QStringLiteral("\n")), &ok); QStringList newEnvironment; if (ok) { if(!text.isEmpty()) { newEnvironment = text.split(QLatin1Char('\n')); updateTempProfileProperty(Profile::Environment, newEnvironment); } else { // the user could have removed all entries so we return an empty list updateTempProfileProperty(Profile::Environment, newEnvironment); } } } void EditProfileDialog::setupTabsPage(const Profile::Ptr &profile) { // tab title format _tabsUi->renameTabWidget->setTabTitleText(profile->localTabTitleFormat()); _tabsUi->renameTabWidget->setRemoteTabTitleText(profile->remoteTabTitleFormat()); connect(_tabsUi->renameTabWidget, &Konsole::RenameTabWidget::tabTitleFormatChanged, this, &Konsole::EditProfileDialog::tabTitleFormatChanged); connect(_tabsUi->renameTabWidget, &Konsole::RenameTabWidget::remoteTabTitleFormatChanged, this, &Konsole::EditProfileDialog::remoteTabTitleFormatChanged); // tab monitoring const int silenceSeconds = profile->silenceSeconds(); _tabsUi->silenceSecondsSpinner->setValue(silenceSeconds); auto suffix = ki18ncp("Unit of time", " second", " seconds"); _tabsUi->silenceSecondsSpinner->setSuffix(suffix); int silenceCheckBoxWidth = maxSpinBoxWidth(_generalUi->terminalColumnsEntry, suffix); _tabsUi->silenceSecondsSpinner->setFixedWidth(silenceCheckBoxWidth); connect(_tabsUi->silenceSecondsSpinner, QOverload::of(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::silenceSecondsChanged); } void EditProfileDialog::terminalColumnsEntryChanged(int value) { updateTempProfileProperty(Profile::TerminalColumns, value); } void EditProfileDialog::terminalRowsEntryChanged(int value) { updateTempProfileProperty(Profile::TerminalRows, value); } void EditProfileDialog::showTerminalSizeHint(bool value) { updateTempProfileProperty(Profile::ShowTerminalSizeHint, value); } void EditProfileDialog::setDimWhenInactive(bool value) { updateTempProfileProperty(Profile::DimWhenInactive, value); } void EditProfileDialog::tabTitleFormatChanged(const QString &format) { updateTempProfileProperty(Profile::LocalTabTitleFormat, format); } void EditProfileDialog::remoteTabTitleFormatChanged(const QString &format) { updateTempProfileProperty(Profile::RemoteTabTitleFormat, format); } void EditProfileDialog::silenceSecondsChanged(int seconds) { updateTempProfileProperty(Profile::SilenceSeconds, seconds); } void EditProfileDialog::selectIcon() { const QString &icon = KIconDialog::getIcon(KIconLoader::Desktop, KIconLoader::Application, false, 0, false, this); if (!icon.isEmpty()) { _generalUi->iconSelectButton->setIcon(QIcon::fromTheme(icon)); updateTempProfileProperty(Profile::Icon, icon); } } void EditProfileDialog::profileNameChanged(const QString &name) { updateTempProfileProperty(Profile::Name, name); updateTempProfileProperty(Profile::UntranslatedName, name); updateCaption(_tempProfile); } void EditProfileDialog::startInSameDir(bool sameDir) { updateTempProfileProperty(Profile::StartInCurrentSessionDir, sameDir); } void EditProfileDialog::initialDirChanged(const QString &dir) { updateTempProfileProperty(Profile::Directory, dir); } void EditProfileDialog::commandChanged(const QString &command) { ShellCommand shellCommand(command); updateTempProfileProperty(Profile::Command, shellCommand.command()); updateTempProfileProperty(Profile::Arguments, shellCommand.arguments()); } void EditProfileDialog::selectInitialDir() { const QUrl url = QFileDialog::getExistingDirectoryUrl(this, i18n("Select Initial Directory"), QUrl::fromUserInput(_generalUi->initialDirEdit->text())); if (!url.isEmpty()) { _generalUi->initialDirEdit->setText(url.path()); } } void EditProfileDialog::setupAppearancePage(const Profile::Ptr &profile) { auto delegate = new ColorSchemeViewDelegate(this); _appearanceUi->colorSchemeList->setItemDelegate(delegate); _appearanceUi->transparencyWarningWidget->setVisible(false); _appearanceUi->transparencyWarningWidget->setWordWrap(true); _appearanceUi->transparencyWarningWidget->setCloseButtonVisible(false); _appearanceUi->transparencyWarningWidget->setMessageType(KMessageWidget::Warning); _appearanceUi->colorSchemeMessageWidget->setVisible(false); _appearanceUi->colorSchemeMessageWidget->setWordWrap(true); _appearanceUi->colorSchemeMessageWidget->setCloseButtonVisible(false); _appearanceUi->colorSchemeMessageWidget->setMessageType(KMessageWidget::Warning); _appearanceUi->editColorSchemeButton->setEnabled(false); _appearanceUi->removeColorSchemeButton->setEnabled(false); _appearanceUi->resetColorSchemeButton->setEnabled(false); _appearanceUi->downloadColorSchemeButton->setConfigFile(QStringLiteral("konsole.knsrc")); // setup color list // select the colorScheme used in the current profile updateColorSchemeList(currentColorSchemeName()); _appearanceUi->colorSchemeList->setMouseTracking(true); _appearanceUi->colorSchemeList->installEventFilter(this); _appearanceUi->colorSchemeList->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(_appearanceUi->colorSchemeList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Konsole::EditProfileDialog::colorSchemeSelected); connect(_appearanceUi->colorSchemeList, &QListView::entered, this, &Konsole::EditProfileDialog::previewColorScheme); updateColorSchemeButtons(); connect(_appearanceUi->editColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::editColorScheme); connect(_appearanceUi->removeColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::removeColorScheme); connect(_appearanceUi->newColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::newColorScheme); connect(_appearanceUi->downloadColorSchemeButton, &KNS3::Button::dialogFinished, this, &Konsole::EditProfileDialog::gotNewColorSchemes); connect(_appearanceUi->resetColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::resetColorScheme); connect(_appearanceUi->chooseFontButton, &QAbstractButton::clicked, this, &EditProfileDialog::showFontDialog); // setup font preview const bool antialias = profile->antiAliasFonts(); QFont profileFont = profile->font(); profileFont.setStyleStrategy(antialias ? QFont::PreferAntialias : QFont::NoAntialias); _appearanceUi->fontPreview->setFont(profileFont); _appearanceUi->fontPreview->setText(QStringLiteral("%1 %2pt").arg(profileFont.family()).arg(profileFont.pointSize())); // setup font smoothing _appearanceUi->antialiasTextButton->setChecked(antialias); connect(_appearanceUi->antialiasTextButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::setAntialiasText); _appearanceUi->boldIntenseButton->setChecked(profile->boldIntense()); connect(_appearanceUi->boldIntenseButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::setBoldIntense); _appearanceUi->useFontLineCharactersButton->setChecked(profile->useFontLineCharacters()); connect(_appearanceUi->useFontLineCharactersButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::useFontLineCharacters); _mouseUi->enableMouseWheelZoomButton->setChecked(profile->mouseWheelZoomEnabled()); connect(_mouseUi->enableMouseWheelZoomButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::toggleMouseWheelZoom); // cursor options const auto options = QVector{ { _appearanceUi->enableBlinkingCursorButton, Profile::BlinkingCursorEnabled, SLOT(toggleBlinkingCursor(bool)) }, }; setupCheckBoxes(options, profile); if (profile->useCustomCursorColor()) { _appearanceUi->customCursorColorButton->setChecked(true); } else { _appearanceUi->autoCursorColorButton->setChecked(true); } _appearanceUi->customColorSelectButton->setColor(profile->customCursorColor()); connect(_appearanceUi->customCursorColorButton, &QRadioButton::clicked, this, &Konsole::EditProfileDialog::customCursorColor); connect(_appearanceUi->autoCursorColorButton, &QRadioButton::clicked, this, &Konsole::EditProfileDialog::autoCursorColor); connect(_appearanceUi->customColorSelectButton, &KColorButton::changed, this, &Konsole::EditProfileDialog::customCursorColorChanged); // Make radio buttons height equal int cursorColorRadioHeight = qMax(_appearanceUi->autoCursorColorButton->minimumSizeHint().height(), _appearanceUi->customColorSelectButton->minimumSizeHint().height()); _appearanceUi->autoCursorColorButton->setMinimumHeight(cursorColorRadioHeight); _appearanceUi->customCursorColorButton->setMinimumHeight(cursorColorRadioHeight); const ButtonGroupOptions cursorShapeOptions = { _appearanceUi->cursorShape, // group Profile::CursorShape, // profileProperty true, // preview { // buttons {_appearanceUi->cursorShapeBlock, Enum::BlockCursor}, {_appearanceUi->cursorShapeIBeam, Enum::IBeamCursor}, {_appearanceUi->cursorShapeUnderline, Enum::UnderlineCursor}, }, }; setupButtonGroup(cursorShapeOptions, profile); _appearanceUi->marginsSpinner->setValue(profile->terminalMargin()); connect(_appearanceUi->marginsSpinner, QOverload::of(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::terminalMarginChanged); _appearanceUi->lineSpacingSpinner->setValue(profile->lineSpacing()); connect(_appearanceUi->lineSpacingSpinner, QOverload::of(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::lineSpacingChanged); _appearanceUi->alignToCenterButton->setChecked(profile->terminalCenter()); connect(_appearanceUi->alignToCenterButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::setTerminalCenter); _appearanceUi->showTerminalSizeHintButton->setChecked(profile->showTerminalSizeHint()); connect(_appearanceUi->showTerminalSizeHintButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::showTerminalSizeHint); _appearanceUi->dimWhenInactiveCheckbox->setChecked(profile->dimWhenInactive()); connect(_appearanceUi->dimWhenInactiveCheckbox, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::setDimWhenInactive); } void EditProfileDialog::setAntialiasText(bool enable) { preview(Profile::AntiAliasFonts, enable); updateTempProfileProperty(Profile::AntiAliasFonts, enable); const QFont font = _profile->font(); updateFontPreview(font); } void EditProfileDialog::setBoldIntense(bool enable) { preview(Profile::BoldIntense, enable); updateTempProfileProperty(Profile::BoldIntense, enable); } void EditProfileDialog::useFontLineCharacters(bool enable) { preview(Profile::UseFontLineCharacters, enable); updateTempProfileProperty(Profile::UseFontLineCharacters, enable); } void EditProfileDialog::toggleBlinkingCursor(bool enable) { preview(Profile::BlinkingCursorEnabled, enable); updateTempProfileProperty(Profile::BlinkingCursorEnabled, enable); } void EditProfileDialog::setCursorShape(int index) { preview(Profile::CursorShape, index); updateTempProfileProperty(Profile::CursorShape, index); } void EditProfileDialog::autoCursorColor() { preview(Profile::UseCustomCursorColor, false); updateTempProfileProperty(Profile::UseCustomCursorColor, false); } void EditProfileDialog::customCursorColor() { preview(Profile::UseCustomCursorColor, true); updateTempProfileProperty(Profile::UseCustomCursorColor, true); } void EditProfileDialog::customCursorColorChanged(const QColor &color) { preview(Profile::CustomCursorColor, color); updateTempProfileProperty(Profile::CustomCursorColor, color); // ensure that custom cursor colors are enabled _appearanceUi->customCursorColorButton->click(); } void EditProfileDialog::terminalMarginChanged(int margin) { preview(Profile::TerminalMargin, margin); updateTempProfileProperty(Profile::TerminalMargin, margin); } void EditProfileDialog::lineSpacingChanged(int spacing) { preview(Profile::LineSpacing, spacing); updateTempProfileProperty(Profile::LineSpacing, spacing); } void EditProfileDialog::setTerminalCenter(bool enable) { preview(Profile::TerminalCenter, enable); updateTempProfileProperty(Profile::TerminalCenter, enable); } void EditProfileDialog::toggleMouseWheelZoom(bool enable) { updateTempProfileProperty(Profile::MouseWheelZoomEnabled, enable); } void EditProfileDialog::toggleAlternateScrolling(bool enable) { updateTempProfileProperty(Profile::AlternateScrolling, enable); } void EditProfileDialog::updateColorSchemeList(const QString &selectedColorSchemeName) { if (_appearanceUi->colorSchemeList->model() == nullptr) { _appearanceUi->colorSchemeList->setModel(new QStandardItemModel(this)); } const ColorScheme *selectedColorScheme = ColorSchemeManager::instance()->findColorScheme(selectedColorSchemeName); auto *model = qobject_cast(_appearanceUi->colorSchemeList->model()); Q_ASSERT(model); model->clear(); QStandardItem *selectedItem = nullptr; QList schemeList = ColorSchemeManager::instance()->allColorSchemes(); foreach (const ColorScheme *scheme, schemeList) { QStandardItem *item = new QStandardItem(scheme->description()); item->setData(QVariant::fromValue(scheme), Qt::UserRole + 1); item->setData(QVariant::fromValue(_profile->font()), Qt::UserRole + 2); item->setFlags(item->flags()); // if selectedColorSchemeName is not empty then select that scheme // after saving the changes in the colorScheme editor if (selectedColorScheme == scheme) { selectedItem = item; } model->appendRow(item); } model->sort(0); if (selectedItem != nullptr) { _appearanceUi->colorSchemeList->updateGeometry(); _appearanceUi->colorSchemeList->selectionModel()->setCurrentIndex(selectedItem->index(), QItemSelectionModel::Select); // update transparency warning label updateTransparencyWarning(); } } void EditProfileDialog::updateKeyBindingsList(const QString &selectKeyBindingsName) { if (_keyboardUi->keyBindingList->model() == nullptr) { _keyboardUi->keyBindingList->setModel(new QStandardItemModel(this)); } auto *model = qobject_cast(_keyboardUi->keyBindingList->model()); Q_ASSERT(model); model->clear(); QStandardItem *selectedItem = nullptr; const QStringList &translatorNames = _keyManager->allTranslators(); for (const QString &translatorName : translatorNames) { const KeyboardTranslator *translator = _keyManager->findTranslator(translatorName); if (translator == nullptr) { continue; } QStandardItem *item = new QStandardItem(translator->description()); item->setEditable(false); item->setData(QVariant::fromValue(translator), Qt::UserRole + 1); item->setData(QVariant::fromValue(_keyManager->findTranslatorPath(translatorName)), Qt::ToolTipRole); item->setData(QVariant::fromValue(_profile->font()), Qt::UserRole + 2); item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-keyboard"))); if (selectKeyBindingsName == translatorName) { selectedItem = item; } model->appendRow(item); } model->sort(0); if (selectedItem != nullptr) { _keyboardUi->keyBindingList->selectionModel()->setCurrentIndex(selectedItem->index(), QItemSelectionModel::Select); } } bool EditProfileDialog::eventFilter(QObject *watched, QEvent *event) { if (watched == _appearanceUi->colorSchemeList && event->type() == QEvent::Leave) { if (_tempProfile->isPropertySet(Profile::ColorScheme)) { preview(Profile::ColorScheme, _tempProfile->colorScheme()); } else { unpreview(Profile::ColorScheme); } } return QDialog::eventFilter(watched, event); } QSize EditProfileDialog::sizeHint() const { QFontMetrics fm(font()); - const int ch = fm.width(QLatin1Char('0')); + const int ch = fm.boundingRect(QLatin1Char('0')).width(); // By default minimum size is used. Increase it to make text inputs // on "tabs" page wider and to add some whitespace on right side // of other pages. The window will not be wider than 2/3 of // the screen width (unless necessary to fit everything) return QDialog::sizeHint() + QSize(10*ch, 0); } void EditProfileDialog::unpreviewAll() { _delayedPreviewTimer->stop(); _delayedPreviewProperties.clear(); QHash map; QHashIterator iter(_previewedProperties); while (iter.hasNext()) { iter.next(); map.insert(static_cast(iter.key()), iter.value()); } // undo any preview changes if (!map.isEmpty()) { ProfileManager::instance()->changeProfile(_profile, map, false); } } void EditProfileDialog::unpreview(int property) { _delayedPreviewProperties.remove(property); if (!_previewedProperties.contains(property)) { return; } QHash map; map.insert(static_cast(property), _previewedProperties[property]); ProfileManager::instance()->changeProfile(_profile, map, false); _previewedProperties.remove(property); } void EditProfileDialog::delayedPreview(int property, const QVariant &value) { _delayedPreviewProperties.insert(property, value); _delayedPreviewTimer->stop(); _delayedPreviewTimer->start(300); } void EditProfileDialog::delayedPreviewActivate() { Q_ASSERT(qobject_cast(sender())); QMutableHashIterator iter(_delayedPreviewProperties); if (iter.hasNext()) { iter.next(); preview(iter.key(), iter.value()); } } void EditProfileDialog::preview(int property, const QVariant &value) { QHash map; map.insert(static_cast(property), value); _delayedPreviewProperties.remove(property); const Profile::Ptr original = lookupProfile(); // skip previews for profile groups if the profiles in the group // have conflicting original values for the property // // TODO - Save the original values for each profile and use to unpreview properties ProfileGroup::Ptr group = original->asGroup(); if (group && group->profiles().count() > 1 && original->property(static_cast(property)).isNull()) { return; } if (!_previewedProperties.contains(property)) { _previewedProperties.insert(property, original->property(static_cast(property))); } // temporary change to color scheme ProfileManager::instance()->changeProfile(_profile, map, false); } void EditProfileDialog::previewColorScheme(const QModelIndex &index) { const QString &name = index.data(Qt::UserRole + 1).value()->name(); delayedPreview(Profile::ColorScheme, name); } void EditProfileDialog::showFontDialog() { if (_fontDialog == nullptr) { _fontDialog = new FontDialog(this); connect(_fontDialog, &FontDialog::fontChanged, this, [this](const QFont &font) { preview(Profile::Font, font); updateFontPreview(font); }); connect(_fontDialog, &FontDialog::accepted, this, [this]() { const QFont font = _fontDialog->font(); preview(Profile::Font, font); updateTempProfileProperty(Profile::Font, font); updateFontPreview(font); }); connect(_fontDialog, &FontDialog::rejected, this, [this]() { unpreview(Profile::Font); const QFont font = _profile->font(); updateFontPreview(font); }); } const QFont font = _profile->font(); _fontDialog->setFont(font); _fontDialog->exec(); } void EditProfileDialog::updateFontPreview(QFont font) { bool aa = _profile->antiAliasFonts(); font.setStyleStrategy(aa ? QFont::PreferAntialias : QFont::NoAntialias); _appearanceUi->fontPreview->setFont(font); _appearanceUi->fontPreview->setText(QStringLiteral("%1 %2pt").arg(font.family()).arg(font.pointSize())); } void EditProfileDialog::removeColorScheme() { QModelIndexList selected = _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes(); if (selected.isEmpty()) { return; } // The actual delete runs async because we need to on-demand query // files managed by KNS. Deleting files managed by KNS screws up the // KNS states (entry gets shown as installed when in fact we deleted it). auto *manager = new KNSCore::DownloadManager(QStringLiteral("konsole.knsrc"), this); connect(manager, &KNSCore::DownloadManager::searchResult, this, [=](const KNSCore::EntryInternal::List &entries) { const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); Q_ASSERT(!name.isEmpty()); bool uninstalled = false; // Check if the theme was installed by KNS, if so uninstall it through // there and unload it. for (auto &entry : entries) { for (const auto &file : entry.installedFiles()) { if (ColorSchemeManager::colorSchemeNameFromPath(file) != name) { continue; } // Make sure the manager can unload it before uninstalling it. if (ColorSchemeManager::instance()->unloadColorScheme(file)) { manager->uninstallEntry(entry); uninstalled = true; } } if (uninstalled) { break; } } // If KNS wasn't able to remove it is a custom theme and we'll drop // it manually. if (!uninstalled) { uninstalled = ColorSchemeManager::instance()->deleteColorScheme(name); } if (uninstalled) { _appearanceUi->colorSchemeList->model()->removeRow(selected.first().row()); } manager->deleteLater(); }); manager->checkForInstalled(); } void EditProfileDialog::gotNewColorSchemes(const KNS3::Entry::List &changedEntries) { int failures = 0; for (auto &entry : qAsConst(changedEntries)) { switch (entry.status()) { case KNS3::Entry::Installed: for (const auto &file : entry.installedFiles()) { if (ColorSchemeManager::instance()->loadColorScheme(file)) { continue; } qWarning() << "Failed to load file" << file; ++failures; } if (failures == entry.installedFiles().size()) { _appearanceUi->colorSchemeMessageWidget->setText( xi18nc("@info", "Scheme %1 failed to load.", entry.name())); _appearanceUi->colorSchemeMessageWidget->animatedShow(); QTimer::singleShot(8000, _appearanceUi->colorSchemeMessageWidget, &KMessageWidget::animatedHide); } break; case KNS3::Entry::Deleted: for (const auto &file : entry.uninstalledFiles()) { if (ColorSchemeManager::instance()->unloadColorScheme(file)) { continue; } qWarning() << "Failed to unload file" << file; // If unloading fails we do not care. Iff the scheme failed here // it either wasn't loaded or was invalid to begin with. } break; case KNS3::Entry::Invalid: case KNS3::Entry::Installing: case KNS3::Entry::Downloadable: case KNS3::Entry::Updateable: case KNS3::Entry::Updating: // Not interesting. break; } } updateColorSchemeList(currentColorSchemeName()); } void EditProfileDialog::resetColorScheme() { QModelIndexList selected = _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); ColorSchemeManager::instance()->deleteColorScheme(name); // select the colorScheme used in the current profile updateColorSchemeList(currentColorSchemeName()); } } void EditProfileDialog::showColorSchemeEditor(bool isNewScheme) { // Finding selected ColorScheme QModelIndexList selected = _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes(); QAbstractItemModel *model = _appearanceUi->colorSchemeList->model(); const ColorScheme *colors = nullptr; if (!selected.isEmpty()) { colors = model->data(selected.first(), Qt::UserRole + 1).value(); } else { colors = ColorSchemeManager::instance()->defaultColorScheme(); } Q_ASSERT(colors); // Setting up ColorSchemeEditor ui // close any running ColorSchemeEditor if (_colorDialog != nullptr) { closeColorSchemeEditor(); } _colorDialog = new ColorSchemeEditor(this); connect(_colorDialog, &Konsole::ColorSchemeEditor::colorSchemeSaveRequested, this, &Konsole::EditProfileDialog::saveColorScheme); _colorDialog->setup(colors, isNewScheme); _colorDialog->show(); } void EditProfileDialog::closeColorSchemeEditor() { if (_colorDialog != nullptr) { _colorDialog->close(); delete _colorDialog; } } void EditProfileDialog::newColorScheme() { showColorSchemeEditor(true); } void EditProfileDialog::editColorScheme() { showColorSchemeEditor(false); } void EditProfileDialog::saveColorScheme(const ColorScheme &scheme, bool isNewScheme) { auto newScheme = new ColorScheme(scheme); // if this is a new color scheme, pick a name based on the description if (isNewScheme) { newScheme->setName(newScheme->description()); } ColorSchemeManager::instance()->addColorScheme(newScheme); const QString &selectedColorSchemeName = newScheme->name(); // select the edited or the new colorScheme after saving the changes updateColorSchemeList(selectedColorSchemeName); preview(Profile::ColorScheme, newScheme->name()); } void EditProfileDialog::colorSchemeSelected() { QModelIndexList selected = _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { QAbstractItemModel *model = _appearanceUi->colorSchemeList->model(); const auto *colors = model->data(selected.first(), Qt::UserRole + 1).value(); if (colors != nullptr) { updateTempProfileProperty(Profile::ColorScheme, colors->name()); previewColorScheme(selected.first()); updateTransparencyWarning(); } } updateColorSchemeButtons(); } void EditProfileDialog::updateColorSchemeButtons() { enableIfNonEmptySelection(_appearanceUi->editColorSchemeButton, _appearanceUi->colorSchemeList->selectionModel()); QModelIndexList selected = _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); bool isResettable = ColorSchemeManager::instance()->canResetColorScheme(name); _appearanceUi->resetColorSchemeButton->setEnabled(isResettable); bool isDeletable = ColorSchemeManager::instance()->isColorSchemeDeletable(name); // if a colorScheme can be restored then it can't be deleted _appearanceUi->removeColorSchemeButton->setEnabled(isDeletable && !isResettable); } else { _appearanceUi->removeColorSchemeButton->setEnabled(false); _appearanceUi->resetColorSchemeButton->setEnabled(false); } } void EditProfileDialog::updateKeyBindingsButtons() { QModelIndexList selected = _keyboardUi->keyBindingList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { _keyboardUi->editKeyBindingsButton->setEnabled(true); const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); bool isResettable = _keyManager->isTranslatorResettable(name); _keyboardUi->resetKeyBindingsButton->setEnabled(isResettable); bool isDeletable = _keyManager->isTranslatorDeletable(name); // if a key bindings scheme can be reset then it can't be deleted _keyboardUi->removeKeyBindingsButton->setEnabled(isDeletable && !isResettable); } } void EditProfileDialog::enableIfNonEmptySelection(QWidget *widget, QItemSelectionModel *selectionModel) { widget->setEnabled(selectionModel->hasSelection()); } void EditProfileDialog::updateTransparencyWarning() { // zero or one indexes can be selected foreach (const QModelIndex &index, _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes()) { bool needTransparency = index.data(Qt::UserRole + 1).value()->opacity() < 1.0; if (!needTransparency) { _appearanceUi->transparencyWarningWidget->setHidden(true); } else if (!KWindowSystem::compositingActive()) { _appearanceUi->transparencyWarningWidget->setText(i18n( "This color scheme uses a transparent background" " which does not appear to be supported on your" " desktop")); _appearanceUi->transparencyWarningWidget->setHidden(false); } else if (!WindowSystemInfo::HAVE_TRANSPARENCY) { _appearanceUi->transparencyWarningWidget->setText(i18n( "Konsole was started before desktop effects were enabled." " You need to restart Konsole to see transparent background.")); _appearanceUi->transparencyWarningWidget->setHidden(false); } } } void EditProfileDialog::createTempProfile() { _tempProfile = Profile::Ptr(new Profile); _tempProfile->setHidden(true); } void EditProfileDialog::updateTempProfileProperty(Profile::Property property, const QVariant &value) { _tempProfile->setProperty(property, value); updateButtonApply(); } void EditProfileDialog::updateButtonApply() { bool userModified = false; QHashIterator iter(_tempProfile->setProperties()); while (iter.hasNext()) { iter.next(); Profile::Property property = iter.key(); QVariant value = iter.value(); // for previewed property if (_previewedProperties.contains(static_cast(property))) { if (value != _previewedProperties.value(static_cast(property))) { userModified = true; break; } // for not-previewed property // // for the Profile::KeyBindings property, if it's set in the _tempProfile // then the user opened the edit key bindings dialog and clicked // OK, and could have add/removed a key bindings rule } else if (property == Profile::KeyBindings || (value != _profile->property(property))) { userModified = true; break; } } _buttonBox->button(QDialogButtonBox::Apply)->setEnabled(userModified); } void EditProfileDialog::setupKeyboardPage(const Profile::Ptr &/* profile */) { // setup translator list updateKeyBindingsList(lookupProfile()->keyBindings()); connect(_keyboardUi->keyBindingList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Konsole::EditProfileDialog::keyBindingSelected); connect(_keyboardUi->newKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::newKeyBinding); _keyboardUi->editKeyBindingsButton->setEnabled(false); _keyboardUi->removeKeyBindingsButton->setEnabled(false); _keyboardUi->resetKeyBindingsButton->setEnabled(false); updateKeyBindingsButtons(); connect(_keyboardUi->editKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::editKeyBinding); connect(_keyboardUi->removeKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::removeKeyBinding); connect(_keyboardUi->resetKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::resetKeyBindings); } void EditProfileDialog::keyBindingSelected() { QModelIndexList selected = _keyboardUi->keyBindingList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { QAbstractItemModel *model = _keyboardUi->keyBindingList->model(); const auto *translator = model->data(selected.first(), Qt::UserRole + 1) .value(); if (translator != nullptr) { updateTempProfileProperty(Profile::KeyBindings, translator->name()); } } updateKeyBindingsButtons(); } void EditProfileDialog::removeKeyBinding() { QModelIndexList selected = _keyboardUi->keyBindingList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); if (KeyboardTranslatorManager::instance()->deleteTranslator(name)) { _keyboardUi->keyBindingList->model()->removeRow(selected.first().row()); } } } void EditProfileDialog::showKeyBindingEditor(bool isNewTranslator) { QModelIndexList selected = _keyboardUi->keyBindingList->selectionModel()->selectedIndexes(); QAbstractItemModel *model = _keyboardUi->keyBindingList->model(); const KeyboardTranslator *translator = nullptr; if (!selected.isEmpty()) { translator = model->data(selected.first(), Qt::UserRole + 1).value(); } else { translator = _keyManager->defaultTranslator(); } Q_ASSERT(translator); auto editor = new KeyBindingEditor(this); if (translator != nullptr) { editor->setup(translator, lookupProfile()->keyBindings(), isNewTranslator); } connect(editor, &Konsole::KeyBindingEditor::updateKeyBindingsListRequest, this, &Konsole::EditProfileDialog::updateKeyBindingsList); connect(editor, &Konsole::KeyBindingEditor::updateTempProfileKeyBindingsRequest, this, &Konsole::EditProfileDialog::updateTempProfileProperty); editor->exec(); } void EditProfileDialog::newKeyBinding() { showKeyBindingEditor(true); } void EditProfileDialog::editKeyBinding() { showKeyBindingEditor(false); } void EditProfileDialog::resetKeyBindings() { QModelIndexList selected = _keyboardUi->keyBindingList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); _keyManager->deleteTranslator(name); // find and load the translator _keyManager->findTranslator(name); updateKeyBindingsList(name); } } void EditProfileDialog::setupCheckBoxes(const QVector& options, const Profile::Ptr &profile) { for(const auto& option : options) { option.button->setChecked(profile->property(option.property)); connect(option.button, SIGNAL(toggled(bool)), this, option.slot); } } void EditProfileDialog::setupRadio(const QVector& possibilities, int actual) { for(const auto& possibility : possibilities) { possibility.button->setChecked(possibility.value == actual); connect(possibility.button, SIGNAL(clicked()), this, possibility.slot); } } void EditProfileDialog::setupButtonGroup(const ButtonGroupOptions &options, const Profile::Ptr &profile) { auto currentValue = profile->property(options.profileProperty); for (auto option: options.buttons) { options.group->setId(option.button, option.value); } Q_ASSERT(options.buttons.count() > 0); auto *activeButton = options.group->button(currentValue); if (activeButton == nullptr) { activeButton = options.buttons[0].button; } activeButton->setChecked(true); connect(options.group, QOverload::of(&QButtonGroup::buttonClicked), this, [this, options](int value) { if (options.preview) { preview(options.profileProperty, value); } updateTempProfileProperty(options.profileProperty, value); }); } void EditProfileDialog::setupScrollingPage(const Profile::Ptr &profile) { // setup scrollbar radio const ButtonGroupOptions scrollBarPositionOptions = { _scrollingUi->scrollBarPosition, // group Profile::ScrollBarPosition, // profileProperty false, // preview { // buttons {_scrollingUi->scrollBarRightButton, Enum::ScrollBarRight}, {_scrollingUi->scrollBarLeftButton, Enum::ScrollBarLeft}, {_scrollingUi->scrollBarHiddenButton, Enum::ScrollBarHidden}, }, }; setupButtonGroup(scrollBarPositionOptions, profile); // setup scrollback type radio auto scrollBackType = profile->property(Profile::HistoryMode); _scrollingUi->historySizeWidget->setMode(Enum::HistoryModeEnum(scrollBackType)); connect(_scrollingUi->historySizeWidget, &Konsole::HistorySizeWidget::historyModeChanged, this, &Konsole::EditProfileDialog::historyModeChanged); // setup scrollback line count spinner const int historySize = profile->historySize(); _scrollingUi->historySizeWidget->setLineCount(historySize); // setup scrollpageamount type radio auto scrollFullPage = profile->property(Profile::ScrollFullPage); const auto pageamounts = QVector{ {_scrollingUi->scrollHalfPage, Enum::ScrollPageHalf, SLOT(scrollHalfPage())}, {_scrollingUi->scrollFullPage, Enum::ScrollPageFull, SLOT(scrollFullPage())} }; setupRadio(pageamounts, scrollFullPage); // signals and slots connect(_scrollingUi->historySizeWidget, &Konsole::HistorySizeWidget::historySizeChanged, this, &Konsole::EditProfileDialog::historySizeChanged); } void EditProfileDialog::historySizeChanged(int lineCount) { updateTempProfileProperty(Profile::HistorySize, lineCount); } void EditProfileDialog::historyModeChanged(Enum::HistoryModeEnum mode) { updateTempProfileProperty(Profile::HistoryMode, mode); } void EditProfileDialog::scrollFullPage() { updateTempProfileProperty(Profile::ScrollFullPage, Enum::ScrollPageFull); } void EditProfileDialog::scrollHalfPage() { updateTempProfileProperty(Profile::ScrollFullPage, Enum::ScrollPageHalf); } void EditProfileDialog::setupMousePage(const Profile::Ptr &profile) { const auto options = QVector{ { _mouseUi->underlineLinksButton, Profile::UnderlineLinksEnabled, SLOT(toggleUnderlineLinks(bool)) }, { _mouseUi->underlineFilesButton, Profile::UnderlineFilesEnabled, SLOT(toggleUnderlineFiles(bool)) }, { _mouseUi->ctrlRequiredForDragButton, Profile::CtrlRequiredForDrag, SLOT(toggleCtrlRequiredForDrag(bool)) }, { _mouseUi->copyTextAsHTMLButton, Profile::CopyTextAsHTML, SLOT(toggleCopyTextAsHTML(bool)) }, { _mouseUi->copyTextToClipboardButton, Profile::AutoCopySelectedText, SLOT(toggleCopyTextToClipboard(bool)) }, { _mouseUi->trimLeadingSpacesButton, Profile::TrimLeadingSpacesInSelectedText, SLOT(toggleTrimLeadingSpacesInSelectedText(bool)) }, { _mouseUi->trimTrailingSpacesButton, Profile::TrimTrailingSpacesInSelectedText, SLOT(toggleTrimTrailingSpacesInSelectedText(bool)) }, { _mouseUi->openLinksByDirectClickButton, Profile::OpenLinksByDirectClickEnabled, SLOT(toggleOpenLinksByDirectClick(bool)) }, { _mouseUi->dropUrlsAsText, Profile::DropUrlsAsText, SLOT(toggleDropUrlsAsText(bool)) }, { _mouseUi->enableAlternateScrollingButton, Profile::AlternateScrolling, SLOT(toggleAlternateScrolling(bool)) } }; setupCheckBoxes(options, profile); // setup middle click paste mode const auto middleClickPasteMode = profile->property(Profile::MiddleClickPasteMode); const auto pasteModes = QVector { {_mouseUi->pasteFromX11SelectionButton, Enum::PasteFromX11Selection, SLOT(pasteFromX11Selection())}, {_mouseUi->pasteFromClipboardButton, Enum::PasteFromClipboard, SLOT(pasteFromClipboard())} }; setupRadio(pasteModes, middleClickPasteMode); // interaction options _mouseUi->wordCharacterEdit->setText(profile->wordCharacters()); connect(_mouseUi->wordCharacterEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::wordCharactersChanged); const ButtonGroupOptions tripleClickModeOptions = { _mouseUi->tripleClickMode, // group Profile::TripleClickMode, // profileProperty false, // preview { // buttons {_mouseUi->tripleClickSelectsTheWholeLine, Enum::SelectWholeLine}, {_mouseUi->tripleClickSelectsFromMousePosition, Enum::SelectForwardsFromCursor}, }, }; setupButtonGroup(tripleClickModeOptions, profile); _mouseUi->openLinksByDirectClickButton->setEnabled(_mouseUi->underlineLinksButton->isChecked() || _mouseUi->underlineFilesButton->isChecked()); _mouseUi->enableMouseWheelZoomButton->setChecked(profile->mouseWheelZoomEnabled()); connect(_mouseUi->enableMouseWheelZoomButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::toggleMouseWheelZoom); } void EditProfileDialog::setupAdvancedPage(const Profile::Ptr &profile) { const auto options = QVector{ { _advancedUi->enableBlinkingTextButton, Profile::BlinkingTextEnabled, SLOT(toggleBlinkingText(bool)) }, { _advancedUi->enableFlowControlButton, Profile::FlowControlEnabled, SLOT(toggleFlowControl(bool)) }, { _appearanceUi->enableBlinkingCursorButton, Profile::BlinkingCursorEnabled, SLOT(toggleBlinkingCursor(bool)) }, { _advancedUi->enableBidiRenderingButton, Profile::BidiRenderingEnabled, SLOT(togglebidiRendering(bool)) }, { _advancedUi->enableReverseUrlHints, Profile::ReverseUrlHints, SLOT(toggleReverseUrlHints(bool)) } }; setupCheckBoxes(options, profile); // Setup the URL hints modifier checkboxes { auto modifiers = profile->property(Profile::UrlHintsModifiers); _advancedUi->urlHintsModifierShift->setChecked((modifiers &Qt::ShiftModifier) != 0u); _advancedUi->urlHintsModifierCtrl->setChecked((modifiers &Qt::ControlModifier) != 0u); _advancedUi->urlHintsModifierAlt->setChecked((modifiers &Qt::AltModifier) != 0u); _advancedUi->urlHintsModifierMeta->setChecked((modifiers &Qt::MetaModifier) != 0u); connect(_advancedUi->urlHintsModifierShift, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); connect(_advancedUi->urlHintsModifierCtrl, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); connect(_advancedUi->urlHintsModifierAlt, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); connect(_advancedUi->urlHintsModifierMeta, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); } // encoding options auto codecAction = new KCodecAction(this); _advancedUi->selectEncodingButton->setMenu(codecAction->menu()); connect(codecAction, QOverload::of(&KCodecAction::triggered), this, &Konsole::EditProfileDialog::setDefaultCodec); _advancedUi->selectEncodingButton->setText(profile->defaultEncoding()); } int EditProfileDialog::maxSpinBoxWidth(const KPluralHandlingSpinBox *spinBox, const KLocalizedString &suffix) { static const int cursorWidth = 2; const QFontMetrics fm(spinBox->fontMetrics()); const QString plural = suffix.subs(2).toString(); const QString singular = suffix.subs(1).toString(); const QString min = QString::number(spinBox->minimum()); const QString max = QString::number(spinBox->maximum()); - const int pluralWidth = fm.width(plural); - const int singularWidth = fm.width(singular); - const int minWidth = fm.width(min); - const int maxWidth = fm.width(max); + const int pluralWidth = fm.boundingRect(plural).width(); + const int singularWidth = fm.boundingRect(singular).width(); + const int minWidth = fm.boundingRect(min).width(); + const int maxWidth = fm.boundingRect(max).width(); const int width = qMax(pluralWidth, singularWidth) + qMax(minWidth, maxWidth) + cursorWidth; // Based on QAbstractSpinBox::initStyleOption() from Qt QStyleOptionSpinBox opt; opt.initFrom(spinBox); opt.activeSubControls = QStyle::SC_None; opt.buttonSymbols = spinBox->buttonSymbols(); // Assume all spinboxes have buttons opt.subControls = QStyle::SC_SpinBoxFrame | QStyle::SC_SpinBoxEditField | QStyle::SC_SpinBoxUp | QStyle::SC_SpinBoxDown; opt.frame = spinBox->hasFrame(); const QSize hint(width, spinBox->sizeHint().height()); const QSize spinBoxSize = style()->sizeFromContents(QStyle::CT_SpinBox, &opt, hint, spinBox) .expandedTo(QApplication::globalStrut()); return spinBoxSize.width(); } void EditProfileDialog::setDefaultCodec(QTextCodec *codec) { QString name = QString::fromLocal8Bit(codec->name()); updateTempProfileProperty(Profile::DefaultEncoding, name); _advancedUi->selectEncodingButton->setText(name); } void EditProfileDialog::wordCharactersChanged(const QString &text) { updateTempProfileProperty(Profile::WordCharacters, text); } void EditProfileDialog::togglebidiRendering(bool enable) { updateTempProfileProperty(Profile::BidiRenderingEnabled, enable); } void EditProfileDialog::toggleUnderlineLinks(bool enable) { updateTempProfileProperty(Profile::UnderlineLinksEnabled, enable); bool enableClick = _mouseUi->underlineFilesButton->isChecked() || enable; _mouseUi->openLinksByDirectClickButton->setEnabled(enableClick); } void EditProfileDialog::toggleUnderlineFiles(bool enable) { updateTempProfileProperty(Profile::UnderlineFilesEnabled, enable); bool enableClick = _mouseUi->underlineLinksButton->isChecked() || enable; _mouseUi->openLinksByDirectClickButton->setEnabled(enableClick); } void EditProfileDialog::toggleCtrlRequiredForDrag(bool enable) { updateTempProfileProperty(Profile::CtrlRequiredForDrag, enable); } void EditProfileDialog::toggleDropUrlsAsText(bool enable) { updateTempProfileProperty(Profile::DropUrlsAsText, enable); } void EditProfileDialog::toggleOpenLinksByDirectClick(bool enable) { updateTempProfileProperty(Profile::OpenLinksByDirectClickEnabled, enable); } void EditProfileDialog::toggleCopyTextAsHTML(bool enable) { updateTempProfileProperty(Profile::CopyTextAsHTML, enable); } void EditProfileDialog::toggleCopyTextToClipboard(bool enable) { updateTempProfileProperty(Profile::AutoCopySelectedText, enable); } void EditProfileDialog::toggleTrimLeadingSpacesInSelectedText(bool enable) { updateTempProfileProperty(Profile::TrimLeadingSpacesInSelectedText, enable); } void EditProfileDialog::toggleTrimTrailingSpacesInSelectedText(bool enable) { updateTempProfileProperty(Profile::TrimTrailingSpacesInSelectedText, enable); } void EditProfileDialog::pasteFromX11Selection() { updateTempProfileProperty(Profile::MiddleClickPasteMode, Enum::PasteFromX11Selection); } void EditProfileDialog::pasteFromClipboard() { updateTempProfileProperty(Profile::MiddleClickPasteMode, Enum::PasteFromClipboard); } void EditProfileDialog::TripleClickModeChanged(int newValue) { updateTempProfileProperty(Profile::TripleClickMode, newValue); } void EditProfileDialog::updateUrlHintsModifier(bool) { Qt::KeyboardModifiers modifiers; if (_advancedUi->urlHintsModifierShift->isChecked()) { modifiers |= Qt::ShiftModifier; } if (_advancedUi->urlHintsModifierCtrl->isChecked()) { modifiers |= Qt::ControlModifier; } if (_advancedUi->urlHintsModifierAlt->isChecked()) { modifiers |= Qt::AltModifier; } if (_advancedUi->urlHintsModifierMeta->isChecked()) { modifiers |= Qt::MetaModifier; } updateTempProfileProperty(Profile::UrlHintsModifiers, int(modifiers)); } void EditProfileDialog::toggleReverseUrlHints(bool enable) { updateTempProfileProperty(Profile::ReverseUrlHints, enable); } void EditProfileDialog::toggleBlinkingText(bool enable) { updateTempProfileProperty(Profile::BlinkingTextEnabled, enable); } void EditProfileDialog::toggleFlowControl(bool enable) { updateTempProfileProperty(Profile::FlowControlEnabled, enable); } ColorSchemeViewDelegate::ColorSchemeViewDelegate(QObject *parent) : QAbstractItemDelegate(parent) { } void ColorSchemeViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { const auto *scheme = index.data(Qt::UserRole + 1).value(); QFont profileFont = index.data(Qt::UserRole + 2).value(); Q_ASSERT(scheme); if (scheme == nullptr) { return; } painter->setRenderHint(QPainter::Antialiasing); // Draw background QStyle *style = option.widget != nullptr ? option.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, option.widget); // Draw name QPalette::ColorRole textColor = ((option.state & QStyle::State_Selected) != 0) ? QPalette::HighlightedText : QPalette::Text; painter->setPen(option.palette.color(textColor)); painter->setFont(option.font); // Determine width of sample text using profile's font const QString sampleText = i18n("AaZz09..."); QFontMetrics profileFontMetrics(profileFont); - const int sampleTextWidth = profileFontMetrics.width(sampleText); + const int sampleTextWidth = profileFontMetrics.boundingRect(sampleText).width(); painter->drawText(option.rect.adjusted(sampleTextWidth + 15, 0, 0, 0), Qt::AlignLeft | Qt::AlignVCenter, index.data(Qt::DisplayRole).toString()); // Draw the preview const int x = option.rect.left(); const int y = option.rect.top(); QRect previewRect(x + 4, y + 4, sampleTextWidth + 8, option.rect.height() - 8); bool transparencyAvailable = KWindowSystem::compositingActive(); if (transparencyAvailable) { painter->save(); QColor color = scheme->backgroundColor(); color.setAlphaF(scheme->opacity()); painter->setPen(Qt::NoPen); painter->setCompositionMode(QPainter::CompositionMode_Source); painter->setBrush(color); painter->drawRect(previewRect); painter->restore(); } else { painter->setPen(Qt::NoPen); painter->setBrush(scheme->backgroundColor()); painter->drawRect(previewRect); } // draw color scheme name using scheme's foreground color QPen pen(scheme->foregroundColor()); painter->setPen(pen); // TODO: respect antialias setting painter->setFont(profileFont); painter->drawText(previewRect, Qt::AlignCenter, sampleText); } QSize ColorSchemeViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex & /*index*/) const { const int width = 200; const int margin = 5; const int colorWidth = width / TABLE_COLORS; const int heightForWidth = (colorWidth * 2) + option.fontMetrics.height() + margin; // temporary return {width, heightForWidth}; } diff --git a/src/TerminalDisplay.cpp b/src/TerminalDisplay.cpp index 05741920..9c4e0135 100644 --- a/src/TerminalDisplay.cpp +++ b/src/TerminalDisplay.cpp @@ -1,3957 +1,3957 @@ /* This file is part of Konsole, a terminal emulator for KDE. Copyright 2006-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle 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 "TerminalDisplay.h" // Config #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include // Konsole #include "Filter.h" #include "konsoledebug.h" #include "TerminalCharacterDecoder.h" #include "Screen.h" #include "SessionController.h" #include "ExtendedCharTable.h" #include "TerminalDisplayAccessible.h" #include "SessionManager.h" #include "Session.h" #include "WindowSystemInfo.h" #include "IncrementalSearchBar.h" #include "Profile.h" #include "ViewManager.h" // for colorSchemeForProfile. // TODO: Rewrite this. #include "LineBlockCharacters.h" using namespace Konsole; #define REPCHAR "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "abcdefgjijklmnopqrstuvwxyz" \ "0123456789./+@" // we use this to force QPainter to display text in LTR mode // more information can be found in: http://unicode.org/reports/tr9/ const QChar LTR_OVERRIDE_CHAR(0x202D); inline int TerminalDisplay::loc(int x, int y) const { Q_ASSERT(y >= 0 && y < _lines); Q_ASSERT(x >= 0 && x < _columns); x = qBound(0, x, _columns - 1); y = qBound(0, y, _lines - 1); return y * _columns + x; } /* ------------------------------------------------------------------------- */ /* */ /* Colors */ /* */ /* ------------------------------------------------------------------------- */ /* Note that we use ANSI color order (bgr), while IBMPC color order is (rgb) Code 0 1 2 3 4 5 6 7 ----------- ------- ------- ------- ------- ------- ------- ------- ------- ANSI (bgr) Black Red Green Yellow Blue Magenta Cyan White IBMPC (rgb) Black Blue Green Cyan Red Magenta Yellow White */ ScreenWindow* TerminalDisplay::screenWindow() const { return _screenWindow; } void TerminalDisplay::setScreenWindow(ScreenWindow* window) { // disconnect existing screen window if any if (!_screenWindow.isNull()) { disconnect(_screenWindow , nullptr , this , nullptr); } _screenWindow = window; if (!_screenWindow.isNull()) { connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateLineProperties); connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateImage); connect(_screenWindow.data() , &Konsole::ScreenWindow::currentResultLineChanged , this , &Konsole::TerminalDisplay::updateImage); connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, [this]() { _filterUpdateRequired = true; }); connect(_screenWindow.data(), &Konsole::ScreenWindow::scrolled, this, [this]() { _filterUpdateRequired = true; }); _screenWindow->setWindowLines(_lines); } } const ColorEntry* TerminalDisplay::colorTable() const { return _colorTable; } void TerminalDisplay::onColorsChanged() { // Mostly just fix the scrollbar // this is a workaround to add some readability to old themes like Fusion // changing the light value for button a bit makes themes like fusion, windows and oxygen way more readable and pleasing QPalette p = QApplication::palette(); QColor buttonTextColor = _colorTable[DEFAULT_FORE_COLOR]; QColor backgroundColor = _colorTable[DEFAULT_BACK_COLOR]; backgroundColor.setAlphaF(_opacity); QColor buttonColor = backgroundColor.toHsv(); if (buttonColor.valueF() < 0.5) { buttonColor = buttonColor.lighter(); } else { buttonColor = buttonColor.darker(); } p.setColor(QPalette::Button, buttonColor); p.setColor(QPalette::Window, backgroundColor); p.setColor(QPalette::Base, backgroundColor); p.setColor(QPalette::WindowText, buttonTextColor); p.setColor(QPalette::ButtonText, buttonTextColor); setPalette(p); _scrollBar->setPalette(p); update(); } void TerminalDisplay::setBackgroundColor(const QColor& color) { _colorTable[DEFAULT_BACK_COLOR] = color; onColorsChanged(); } QColor TerminalDisplay::getBackgroundColor() const { return _colorTable[DEFAULT_BACK_COLOR]; } void TerminalDisplay::setForegroundColor(const QColor& color) { _colorTable[DEFAULT_FORE_COLOR] = color; onColorsChanged(); } void TerminalDisplay::setColorTable(const ColorEntry table[]) { for (int i = 0; i < TABLE_COLORS; i++) { _colorTable[i] = table[i]; } setBackgroundColor(_colorTable[DEFAULT_BACK_COLOR]); onColorsChanged(); } /* ------------------------------------------------------------------------- */ /* */ /* Font */ /* */ /* ------------------------------------------------------------------------- */ static inline bool isLineCharString(const QString& string) { if (string.length() == 0) { return false; } return LineBlockCharacters::canDraw(string.at(0).unicode()); } void TerminalDisplay::fontChange(const QFont&) { QFontMetrics fm(font()); _fontHeight = fm.height() + _lineSpacing; Q_ASSERT(_fontHeight > 0); // waba TerminalDisplay 1.123: // "Base character width on widest ASCII character. This prevents too wide // characters in the presence of double wide (e.g. Japanese) characters." // Get the width from representative normal width characters _fontWidth = qRound((static_cast(fm.width(QStringLiteral(REPCHAR))) / static_cast(qstrlen(REPCHAR)))); _fixedFont = true; const int fw = fm.width(QLatin1Char(REPCHAR[0])); for (unsigned int i = 1; i < qstrlen(REPCHAR); i++) { if (fw != fm.width(QLatin1Char(REPCHAR[i]))) { _fixedFont = false; break; } } if (_fontWidth < 1) { _fontWidth = 1; } _fontAscent = fm.ascent(); emit changedFontMetricSignal(_fontHeight, _fontWidth); propagateSize(); update(); } void TerminalDisplay::setVTFont(const QFont& f) { QFont newFont(f); int strategy = 0; // hint that text should be drawn with- or without anti-aliasing. // depending on the user's font configuration, this may not be respected strategy |= _antialiasText ? QFont::PreferAntialias : QFont::NoAntialias; // Konsole cannot handle non-integer font metrics strategy |= QFont::ForceIntegerMetrics; // In case the provided font doesn't have some specific characters it should // fall back to a Monospace fonts. newFont.setStyleHint(QFont::TypeWriter, QFont::StyleStrategy(strategy)); // Try to check that a good font has been loaded. // For some fonts, ForceIntegerMetrics causes height() == 0 which // will cause Konsole to crash later. QFontMetrics fontMetrics2(newFont); if (fontMetrics2.height() < 1) { qCDebug(KonsoleDebug)<<"The font "<(fontInfo.styleHint()), fontInfo.weight(), static_cast(fontInfo.style()), static_cast(fontInfo.underline()), static_cast(fontInfo.strikeOut()), // Intentional newFont use - fixedPitch is bugged, see comment above static_cast(newFont.fixedPitch()), static_cast(fontInfo.rawMode())); qCDebug(KonsoleDebug) << "The font to use in the terminal can not be matched exactly on your system."; qCDebug(KonsoleDebug) << " Selected: " << newFont.toString(); qCDebug(KonsoleDebug) << " System : " << nonMatching; } QWidget::setFont(newFont); fontChange(newFont); } void TerminalDisplay::increaseFontSize() { QFont font = getVTFont(); font.setPointSizeF(font.pointSizeF() + 1); setVTFont(font); } void TerminalDisplay::decreaseFontSize() { const qreal MinimumFontSize = 6; QFont font = getVTFont(); font.setPointSizeF(qMax(font.pointSizeF() - 1, MinimumFontSize)); setVTFont(font); } void TerminalDisplay::resetFontSize() { const qreal MinimumFontSize = 6; QFont font = getVTFont(); Profile::Ptr currentProfile = SessionManager::instance()->sessionProfile(_sessionController->session()); const qreal defaultFontSize = currentProfile->font().pointSizeF(); font.setPointSizeF(qMax(defaultFontSize, MinimumFontSize)); setVTFont(font); } uint TerminalDisplay::lineSpacing() const { return _lineSpacing; } void TerminalDisplay::setLineSpacing(uint i) { _lineSpacing = i; fontChange(font()); // Trigger an update. } /* ------------------------------------------------------------------------- */ /* */ /* Accessibility */ /* */ /* ------------------------------------------------------------------------- */ namespace Konsole { #ifndef QT_NO_ACCESSIBILITY /** * This function installs the factory function which lets Qt instantiate the QAccessibleInterface * for the TerminalDisplay. */ QAccessibleInterface* accessibleInterfaceFactory(const QString &key, QObject *object) { Q_UNUSED(key) if (auto *display = qobject_cast(object)) { return new TerminalDisplayAccessible(display); } return nullptr; } #endif } /* ------------------------------------------------------------------------- */ /* */ /* Constructor / Destructor */ /* */ /* ------------------------------------------------------------------------- */ TerminalDisplay::TerminalDisplay(QWidget* parent) : QWidget(parent) , _screenWindow(nullptr) , _bellMasked(false) , _verticalLayout(new QVBoxLayout(this)) , _fixedFont(true) , _fontHeight(1) , _fontWidth(1) , _fontAscent(1) , _boldIntense(true) , _lines(1) , _columns(1) , _usedLines(1) , _usedColumns(1) , _contentRect(QRect()) , _image(nullptr) , _imageSize(0) , _lineProperties(QVector()) , _randomSeed(0) , _resizing(false) , _showTerminalSizeHint(true) , _bidiEnabled(false) , _usesMouseTracking(false) , _alternateScrolling(true) , _bracketedPasteMode(false) , _iPntSel(QPoint()) , _pntSel(QPoint()) , _tripleSelBegin(QPoint()) , _actSel(0) , _wordSelectionMode(false) , _lineSelectionMode(false) , _preserveLineBreaks(true) , _columnSelectionMode(false) , _autoCopySelectedText(false) , _copyTextAsHTML(true) , _middleClickPasteMode(Enum::PasteFromX11Selection) , _scrollBar(nullptr) , _scrollbarLocation(Enum::ScrollBarRight) , _scrollFullPage(false) , _wordCharacters(QStringLiteral(":@-./_~")) , _bellMode(Enum::NotifyBell) , _allowBlinkingText(true) , _allowBlinkingCursor(false) , _textBlinking(false) , _cursorBlinking(false) , _hasTextBlinker(false) , _urlHintsModifiers(Qt::NoModifier) , _showUrlHint(false) , _reverseUrlHints(false) , _openLinksByDirectClick(false) , _ctrlRequiredForDrag(true) , _dropUrlsAsText(false) , _tripleClickMode(Enum::SelectWholeLine) , _possibleTripleClick(false) , _resizeWidget(nullptr) , _resizeTimer(nullptr) , _flowControlWarningEnabled(false) , _outputSuspendedMessageWidget(nullptr) , _lineSpacing(0) , _size(QSize()) , _blendColor(qRgba(0, 0, 0, 0xff)) , _wallpaper(nullptr) , _filterChain(new TerminalImageFilterChain()) , _mouseOverHotspotArea(QRegion()) , _filterUpdateRequired(true) , _cursorShape(Enum::BlockCursor) , _cursorColor(QColor()) , _antialiasText(true) , _useFontLineCharacters(false) , _printerFriendly(false) , _sessionController(nullptr) , _trimLeadingSpaces(false) , _trimTrailingSpaces(false) , _mouseWheelZoom(false) , _margin(1) , _centerContents(false) , _readOnlyMessageWidget(nullptr) , _readOnly(false) , _opacity(1.0) , _dimWhenInactive(false) , _scrollWheelState(ScrollState()) , _searchBar(new IncrementalSearchBar(this)) , _headerBar(new TerminalHeaderBar(this)) , _searchResultRect(QRect()) , _drawOverlay(false) { // terminal applications are not designed with Right-To-Left in mind, // so the layout is forced to Left-To-Right setLayoutDirection(Qt::LeftToRight); _contentRect = QRect(_margin, _margin, 1, 1); // create scroll bar for scrolling output up and down _scrollBar = new QScrollBar(this); _scrollBar->setAutoFillBackground(false); // set the scroll bar's slider to occupy the whole area of the scroll bar initially setScroll(0, 0); _scrollBar->setCursor(Qt::ArrowCursor); _headerBar->setCursor(Qt::ArrowCursor); connect(_headerBar, &TerminalHeaderBar::requestToggleExpansion, this, &Konsole::TerminalDisplay::requestToggleExpansion); connect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged); connect(_scrollBar, &QScrollBar::sliderMoved, this, &Konsole::TerminalDisplay::viewScrolledByUser); // setup timers for blinking text _blinkTextTimer = new QTimer(this); _blinkTextTimer->setInterval(TEXT_BLINK_DELAY); connect(_blinkTextTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkTextEvent); // setup timers for blinking cursor _blinkCursorTimer = new QTimer(this); _blinkCursorTimer->setInterval(QApplication::cursorFlashTime() / 2); connect(_blinkCursorTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkCursorEvent); // hide mouse cursor on keystroke or idle KCursor::setAutoHideCursor(this, true); setMouseTracking(true); setUsesMouseTracking(false); setBracketedPasteMode(false); setColorTable(ColorScheme::defaultTable); // Enable drag and drop support setAcceptDrops(true); _dragInfo.state = diNone; setFocusPolicy(Qt::WheelFocus); // enable input method support setAttribute(Qt::WA_InputMethodEnabled, true); // this is an important optimization, it tells Qt // that TerminalDisplay will handle repainting its entire area. setAttribute(Qt::WA_OpaquePaintEvent); // Add the stretch item once, the KMessageWidgets are inserted at index 0. _verticalLayout->addWidget(_headerBar); _verticalLayout->addStretch(); _verticalLayout->setSpacing(0); _verticalLayout->setMargin(0); setLayout(_verticalLayout); new AutoScrollHandler(this); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(Konsole::accessibleInterfaceFactory); #endif } TerminalDisplay::~TerminalDisplay() { disconnect(_blinkTextTimer); disconnect(_blinkCursorTimer); delete _readOnlyMessageWidget; delete _outputSuspendedMessageWidget; delete[] _image; delete _filterChain; _readOnlyMessageWidget = nullptr; _outputSuspendedMessageWidget = nullptr; } void TerminalDisplay::hideDragTarget() { _drawOverlay = false; update(); } void TerminalDisplay::showDragTarget() { auto cursorPos = mapFromGlobal(QCursor::pos()); using EdgeDistance = std::pair; auto closerToEdge = std::min( { {cursorPos.x(), Qt::LeftEdge}, {cursorPos.y(), Qt::TopEdge}, {width() - cursorPos.x(), Qt::RightEdge}, {height() - cursorPos.y(), Qt::BottomEdge} }, [](const EdgeDistance& left, const EdgeDistance& right) -> bool { return left.first < right.first; } ); if (_overlayEdge == closerToEdge.second) { return; } _overlayEdge = closerToEdge.second; _drawOverlay = true; update(); } /* ------------------------------------------------------------------------- */ /* */ /* Display Operations */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::drawLineCharString(QPainter& painter, int x, int y, const QString& str, const Character* attributes) { const bool useBoldPen = (attributes->rendition & RE_BOLD) != 0 && _boldIntense; QRect cellRect = {x, y, _fontWidth, _fontHeight}; for (int i = 0 ; i < str.length(); i++) { LineBlockCharacters::draw(painter, cellRect.translated(i * _fontWidth, 0), str[i], useBoldPen); } } void TerminalDisplay::setKeyboardCursorShape(Enum::CursorShapeEnum shape) { _cursorShape = shape; } Enum::CursorShapeEnum TerminalDisplay::keyboardCursorShape() const { return _cursorShape; } void TerminalDisplay::setCursorStyle(Enum::CursorShapeEnum shape, bool isBlinking) { setKeyboardCursorShape(shape); setBlinkingCursorEnabled(isBlinking); // when the cursor shape and blinking state are changed via the // Set Cursor Style (DECSCUSR) escape sequences in vim, and if the // cursor isn't set to blink, the cursor shape doesn't actually // change until the cursor is moved by the user; calling update() // makes the cursor shape get updated sooner. if (!isBlinking) { update(); } } void TerminalDisplay::resetCursorStyle() { Q_ASSERT(_sessionController != nullptr); Q_ASSERT(!_sessionController->session().isNull()); Profile::Ptr currentProfile = SessionManager::instance()->sessionProfile(_sessionController->session()); if (currentProfile != nullptr) { auto shape = static_cast(currentProfile->property(Profile::CursorShape)); setKeyboardCursorShape(shape); setBlinkingCursorEnabled(currentProfile->blinkingCursorEnabled()); } } void TerminalDisplay::setKeyboardCursorColor(const QColor& color) { _cursorColor = color; } QColor TerminalDisplay::keyboardCursorColor() const { return _cursorColor; } void TerminalDisplay::setOpacity(qreal opacity) { QColor color(_blendColor); color.setAlphaF(opacity); _opacity = opacity; _blendColor = color.rgba(); onColorsChanged(); } void TerminalDisplay::setWallpaper(const ColorSchemeWallpaper::Ptr &p) { _wallpaper = p; } void TerminalDisplay::drawBackground(QPainter& painter, const QRect& rect, const QColor& backgroundColor, bool useOpacitySetting) { // the area of the widget showing the contents of the terminal display is drawn // using the background color from the color scheme set with setColorTable() // // the area of the widget behind the scroll-bar is drawn using the background // brush from the scroll-bar's palette, to give the effect of the scroll-bar // being outside of the terminal display and visual consistency with other KDE // applications. if (useOpacitySetting && !_wallpaper->isNull() && _wallpaper->draw(painter, rect, _opacity)) { } else if (qAlpha(_blendColor) < 0xff && useOpacitySetting) { #if defined(Q_OS_MACOS) // TODO - On MacOS, using CompositionMode doesn't work. Altering the // transparency in the color scheme alters the brightness. painter.fillRect(rect, backgroundColor); #else QColor color(backgroundColor); color.setAlpha(qAlpha(_blendColor)); const QPainter::CompositionMode originalMode = painter.compositionMode(); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(rect, color); painter.setCompositionMode(originalMode); #endif } else { painter.fillRect(rect, backgroundColor); } } void TerminalDisplay::drawCursor(QPainter& painter, const QRect& rect, const QColor& foregroundColor, const QColor& /*backgroundColor*/, bool& invertCharacterColor) { // don't draw cursor which is currently blinking if (_cursorBlinking) { return; } // shift rectangle top down one pixel to leave some space // between top and bottom QRectF cursorRect = rect.adjusted(0, 1, 0, 0); QColor cursorColor = _cursorColor.isValid() ? _cursorColor : foregroundColor; painter.setPen(cursorColor); if (_cursorShape == Enum::BlockCursor) { // draw the cursor outline, adjusting the area so that // it is draw entirely inside 'rect' int penWidth = qMax(1, painter.pen().width()); painter.drawRect(cursorRect.adjusted(int(penWidth / 2) + 0.5, int(penWidth / 2) + 0.5, - int(penWidth / 2) - penWidth % 2 + 0.5, - int(penWidth / 2) - penWidth % 2 + 0.5)); // draw the cursor body only when the widget has focus if (hasFocus()) { painter.fillRect(cursorRect, cursorColor); if (!_cursorColor.isValid()) { // invert the color used to draw the text to ensure that the character at // the cursor position is readable invertCharacterColor = true; } } } else if (_cursorShape == Enum::UnderlineCursor) { QLineF line(cursorRect.left() + 0.5, cursorRect.bottom() - 0.5, cursorRect.right() - 0.5, cursorRect.bottom() - 0.5); painter.drawLine(line); } else if (_cursorShape == Enum::IBeamCursor) { QLineF line(cursorRect.left() + 0.5, cursorRect.top() + 0.5, cursorRect.left() + 0.5, cursorRect.bottom() - 0.5); painter.drawLine(line); } } void TerminalDisplay::drawCharacters(QPainter& painter, const QRect& rect, const QString& text, const Character* style, bool invertCharacterColor) { // don't draw text which is currently blinking if (_textBlinking && ((style->rendition & RE_BLINK) != 0)) { return; } // don't draw concealed characters if ((style->rendition & RE_CONCEAL) != 0) { return; } const int normalWeight = font().weight(); // +26 makes "bold" from "normal", "normal" from "light", etc. It is 26 instead of not 25 to prefer // bolder weight when 25 falls in the middle between two weights. See QFont::Weight const int boldWeight = normalWeight + 26; const auto isBold = [boldWeight](const QFont &font) { return font.weight() >= boldWeight; }; const bool useBold = (((style->rendition & RE_BOLD) != 0) && _boldIntense); const bool useUnderline = ((style->rendition & RE_UNDERLINE) != 0) || font().underline(); const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || font().italic(); const bool useStrikeOut = ((style->rendition & RE_STRIKEOUT) != 0) || font().strikeOut(); const bool useOverline = ((style->rendition & RE_OVERLINE) != 0) || font().overline(); QFont currentFont = painter.font(); if (isBold(currentFont) != useBold || currentFont.underline() != useUnderline || currentFont.italic() != useItalic || currentFont.strikeOut() != useStrikeOut || currentFont.overline() != useOverline) { currentFont.setWeight(useBold ? boldWeight : normalWeight); currentFont.setUnderline(useUnderline); currentFont.setItalic(useItalic); currentFont.setStrikeOut(useStrikeOut); currentFont.setOverline(useOverline); painter.setFont(currentFont); } // setup pen const CharacterColor& textColor = (invertCharacterColor ? style->backgroundColor : style->foregroundColor); const QColor color = textColor.color(_colorTable); QPen pen = painter.pen(); if (pen.color() != color) { pen.setColor(color); painter.setPen(color); } const bool origClipping = painter.hasClipping(); const auto origClipRegion = painter.clipRegion(); painter.setClipRect(rect); // draw text if (isLineCharString(text) && !_useFontLineCharacters) { drawLineCharString(painter, rect.x(), rect.y(), text, style); } else { // Force using LTR as the document layout for the terminal area, because // there is no use cases for RTL emulator and RTL terminal application. // // This still allows RTL characters to be rendered in the RTL way. painter.setLayoutDirection(Qt::LeftToRight); if (_bidiEnabled) { painter.drawText(rect.x(), rect.y() + _fontAscent + _lineSpacing, text); } else { painter.drawText(rect.x(), rect.y() + _fontAscent + _lineSpacing, LTR_OVERRIDE_CHAR + text); } } painter.setClipRegion(origClipRegion); painter.setClipping(origClipping); } void TerminalDisplay::drawTextFragment(QPainter& painter , const QRect& rect, const QString& text, const Character* style) { // setup painter const QColor foregroundColor = style->foregroundColor.color(_colorTable); const QColor backgroundColor = style->backgroundColor.color(_colorTable); // draw background if different from the display's background color if (backgroundColor != getBackgroundColor()) { drawBackground(painter, rect, backgroundColor, false /* do not use transparency */); } // draw cursor shape if the current character is the cursor // this may alter the foreground and background colors bool invertCharacterColor = false; if ((style->rendition & RE_CURSOR) != 0) { drawCursor(painter, rect, foregroundColor, backgroundColor, invertCharacterColor); } // draw text drawCharacters(painter, rect, text, style, invertCharacterColor); } void TerminalDisplay::drawPrinterFriendlyTextFragment(QPainter& painter, const QRect& rect, const QString& text, const Character* style) { // Set the colors used to draw to black foreground and white // background for printer friendly output when printing Character print_style = *style; print_style.foregroundColor = CharacterColor(COLOR_SPACE_RGB, 0x00000000); print_style.backgroundColor = CharacterColor(COLOR_SPACE_RGB, 0xFFFFFFFF); // draw text drawCharacters(painter, rect, text, &print_style, false); } void TerminalDisplay::setRandomSeed(uint randomSeed) { _randomSeed = randomSeed; } uint TerminalDisplay::randomSeed() const { return _randomSeed; } // scrolls the image by 'lines', down if lines > 0 or up otherwise. // // the terminal emulation keeps track of the scrolling of the character // image as it receives input, and when the view is updated, it calls scrollImage() // with the final scroll amount. this improves performance because scrolling the // display is much cheaper than re-rendering all the text for the // part of the image which has moved up or down. // Instead only new lines have to be drawn void TerminalDisplay::scrollImage(int lines , const QRect& screenWindowRegion) { // return if there is nothing to do if ((lines == 0) || (_image == nullptr)) { return; } // if the flow control warning is enabled this will interfere with the // scrolling optimizations and cause artifacts. the simple solution here // is to just disable the optimization whilst it is visible if ((_outputSuspendedMessageWidget != nullptr) && _outputSuspendedMessageWidget->isVisible()) { return; } if ((_readOnlyMessageWidget != nullptr) && _readOnlyMessageWidget->isVisible()) { return; } // constrain the region to the display // the bottom of the region is capped to the number of lines in the display's // internal image - 2, so that the height of 'region' is strictly less // than the height of the internal image. QRect region = screenWindowRegion; region.setBottom(qMin(region.bottom(), _lines - 2)); // return if there is nothing to do if (!region.isValid() || (region.top() + abs(lines)) >= region.bottom() || _lines <= region.height()) { return; } // hide terminal size label to prevent it being scrolled if ((_resizeWidget != nullptr) && _resizeWidget->isVisible()) { _resizeWidget->hide(); } // Note: With Qt 4.4 the left edge of the scrolled area must be at 0 // to get the correct (newly exposed) part of the widget repainted. // // The right edge must be before the left edge of the scroll bar to // avoid triggering a repaint of the entire widget, the distance is // given by SCROLLBAR_CONTENT_GAP // // Set the QT_FLUSH_PAINT environment variable to '1' before starting the // application to monitor repainting. // const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->width(); const int SCROLLBAR_CONTENT_GAP = 1; QRect scrollRect; if (_scrollbarLocation == Enum::ScrollBarLeft) { scrollRect.setLeft(scrollBarWidth + SCROLLBAR_CONTENT_GAP); scrollRect.setRight(width()); } else { scrollRect.setLeft(0); scrollRect.setRight(width() - scrollBarWidth - SCROLLBAR_CONTENT_GAP); } void* firstCharPos = &_image[ region.top() * _columns ]; void* lastCharPos = &_image[(region.top() + abs(lines)) * _columns ]; const int top = _contentRect.top() + (region.top() * _fontHeight); const int linesToMove = region.height() - abs(lines); const int bytesToMove = linesToMove * _columns * sizeof(Character); Q_ASSERT(linesToMove > 0); Q_ASSERT(bytesToMove > 0); scrollRect.setTop( lines > 0 ? top : top + abs(lines) * _fontHeight); scrollRect.setHeight(linesToMove * _fontHeight); if (!scrollRect.isValid() || scrollRect.isEmpty()) { return; } //scroll internal image if (lines > 0) { // check that the memory areas that we are going to move are valid Q_ASSERT((char*)lastCharPos + bytesToMove < (char*)(_image + (_lines * _columns))); Q_ASSERT((lines * _columns) < _imageSize); //scroll internal image down memmove(firstCharPos , lastCharPos , bytesToMove); } else { // check that the memory areas that we are going to move are valid Q_ASSERT((char*)firstCharPos + bytesToMove < (char*)(_image + (_lines * _columns))); //scroll internal image up memmove(lastCharPos , firstCharPos , bytesToMove); } //scroll the display vertically to match internal _image scroll(0 , _fontHeight * (-lines) , scrollRect); } QRegion TerminalDisplay::hotSpotRegion() const { QRegion region; for (const Filter::HotSpot *hotSpot : _filterChain->hotSpots()) { QRect r; r.setLeft(hotSpot->startColumn()); r.setTop(hotSpot->startLine()); if (hotSpot->startLine() == hotSpot->endLine()) { r.setRight(hotSpot->endColumn()); r.setBottom(hotSpot->endLine()); region |= imageToWidget(r); } else { r.setRight(_columns); r.setBottom(hotSpot->startLine()); region |= imageToWidget(r); r.setLeft(0); for (int line = hotSpot->startLine() + 1 ; line < hotSpot->endLine(); line++) { r.moveTop(line); region |= imageToWidget(r); } r.moveTop(hotSpot->endLine()); r.setRight(hotSpot->endColumn()); region |= imageToWidget(r); } } return region; } void TerminalDisplay::processFilters() { if (_screenWindow.isNull()) { return; } if (!_filterUpdateRequired) { return; } QRegion preUpdateHotSpots = hotSpotRegion(); // use _screenWindow->getImage() here rather than _image because // other classes may call processFilters() when this display's // ScreenWindow emits a scrolled() signal - which will happen before // updateImage() is called on the display and therefore _image is // out of date at this point _filterChain->setImage(_screenWindow->getImage(), _screenWindow->windowLines(), _screenWindow->windowColumns(), _screenWindow->getLineProperties()); _filterChain->process(); QRegion postUpdateHotSpots = hotSpotRegion(); update(preUpdateHotSpots | postUpdateHotSpots); _filterUpdateRequired = false; } void TerminalDisplay::updateImage() { if (_screenWindow.isNull()) { return; } // optimization - scroll the existing image where possible and // avoid expensive text drawing for parts of the image that // can simply be moved up or down // disable this shortcut for transparent konsole with scaled pixels, otherwise we get rendering artifacts, see BUG 350651 if (!(WindowSystemInfo::HAVE_TRANSPARENCY && (qApp->devicePixelRatio() > 1.0)) && _wallpaper->isNull() && !_searchBar->isVisible()) { scrollImage(_screenWindow->scrollCount() , _screenWindow->scrollRegion()); } if (_image == nullptr) { // Create _image. // The emitted changedContentSizeSignal also leads to getImage being recreated, so do this first. updateImageSize(); } Character* const newimg = _screenWindow->getImage(); const int lines = _screenWindow->windowLines(); const int columns = _screenWindow->windowColumns(); setScroll(_screenWindow->currentLine() , _screenWindow->lineCount()); Q_ASSERT(_usedLines <= _lines); Q_ASSERT(_usedColumns <= _columns); int y, x, len; const QPoint tL = contentsRect().topLeft(); const int tLx = tL.x(); const int tLy = tL.y(); _hasTextBlinker = false; CharacterColor cf; // undefined const int linesToUpdate = qMin(_lines, qMax(0, lines)); const int columnsToUpdate = qMin(_columns, qMax(0, columns)); auto dirtyMask = new char[columnsToUpdate + 2]; QRegion dirtyRegion; // debugging variable, this records the number of lines that are found to // be 'dirty' ( ie. have changed from the old _image to the new _image ) and // which therefore need to be repainted int dirtyLineCount = 0; for (y = 0; y < linesToUpdate; ++y) { const Character* currentLine = &_image[y * _columns]; const Character* const newLine = &newimg[y * columns]; bool updateLine = false; // The dirty mask indicates which characters need repainting. We also // mark surrounding neighbors dirty, in case the character exceeds // its cell boundaries memset(dirtyMask, 0, columnsToUpdate + 2); for (x = 0 ; x < columnsToUpdate ; ++x) { if (newLine[x] != currentLine[x]) { dirtyMask[x] = 1; } } if (!_resizing) { // not while _resizing, we're expecting a paintEvent for (x = 0; x < columnsToUpdate; ++x) { _hasTextBlinker |= (newLine[x].rendition & RE_BLINK); // Start drawing if this character or the next one differs. // We also take the next one into account to handle the situation // where characters exceed their cell width. if (dirtyMask[x] != 0) { if (newLine[x + 0].character == 0u) { continue; } const bool lineDraw = LineBlockCharacters::canDraw(newLine[x + 0].character); const bool doubleWidth = (x + 1 == columnsToUpdate) ? false : (newLine[x + 1].character == 0); const RenditionFlags cr = newLine[x].rendition; const CharacterColor clipboard = newLine[x].backgroundColor; if (newLine[x].foregroundColor != cf) { cf = newLine[x].foregroundColor; } const int lln = columnsToUpdate - x; for (len = 1; len < lln; ++len) { const Character& ch = newLine[x + len]; if (ch.character == 0u) { continue; // Skip trailing part of multi-col chars. } const bool nextIsDoubleWidth = (x + len + 1 == columnsToUpdate) ? false : (newLine[x + len + 1].character == 0); if (ch.foregroundColor != cf || ch.backgroundColor != clipboard || (ch.rendition & ~RE_EXTENDED_CHAR) != (cr & ~RE_EXTENDED_CHAR) || (dirtyMask[x + len] == 0) || LineBlockCharacters::canDraw(ch.character) != lineDraw || nextIsDoubleWidth != doubleWidth) { break; } } const bool saveFixedFont = _fixedFont; if (lineDraw) { _fixedFont = false; } if (doubleWidth) { _fixedFont = false; } updateLine = true; _fixedFont = saveFixedFont; x += len - 1; } } } //both the top and bottom halves of double height _lines must always be redrawn //although both top and bottom halves contain the same characters, only //the top one is actually //drawn. if (_lineProperties.count() > y) { updateLine |= (_lineProperties[y] & LINE_DOUBLEHEIGHT); } // if the characters on the line are different in the old and the new _image // then this line must be repainted. if (updateLine) { dirtyLineCount++; // add the area occupied by this line to the region which needs to be // repainted QRect dirtyRect = QRect(_contentRect.left() + tLx , _contentRect.top() + tLy + _fontHeight * y , _fontWidth * columnsToUpdate , _fontHeight); dirtyRegion |= dirtyRect; } // replace the line of characters in the old _image with the // current line of the new _image memcpy((void*)currentLine, (const void*)newLine, columnsToUpdate * sizeof(Character)); } // if the new _image is smaller than the previous _image, then ensure that the area // outside the new _image is cleared if (linesToUpdate < _usedLines) { dirtyRegion |= QRect(_contentRect.left() + tLx , _contentRect.top() + tLy + _fontHeight * linesToUpdate , _fontWidth * _columns , _fontHeight * (_usedLines - linesToUpdate)); } _usedLines = linesToUpdate; if (columnsToUpdate < _usedColumns) { dirtyRegion |= QRect(_contentRect.left() + tLx + columnsToUpdate * _fontWidth , _contentRect.top() + tLy , _fontWidth * (_usedColumns - columnsToUpdate) , _fontHeight * _lines); } _usedColumns = columnsToUpdate; dirtyRegion |= _inputMethodData.previousPreeditRect; if ((_screenWindow->currentResultLine() != -1) && (_screenWindow->scrollCount() != 0)) { // De-highlight previous result region dirtyRegion |= _searchResultRect; // Highlight new result region dirtyRegion |= QRect(0, _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _fontHeight, _columns * _fontWidth, _fontHeight); } _screenWindow->resetScrollCount(); // update the parts of the display which have changed update(dirtyRegion); if (_allowBlinkingText && _hasTextBlinker && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!_hasTextBlinker && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } delete[] dirtyMask; #ifndef QT_NO_ACCESSIBILITY QAccessibleEvent dataChangeEvent(this, QAccessible::VisibleDataChanged); QAccessible::updateAccessibility(&dataChangeEvent); QAccessibleTextCursorEvent cursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX()); QAccessible::updateAccessibility(&cursorEvent); #endif } void TerminalDisplay::showResizeNotification() { if (_showTerminalSizeHint && isVisible()) { if (_resizeWidget == nullptr) { _resizeWidget = new QLabel(i18n("Size: XXX x XXX"), this); - _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().width(i18n("Size: XXX x XXX"))); + _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().boundingRect(i18n("Size: XXX x XXX")).width()); _resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height()); _resizeWidget->setAlignment(Qt::AlignCenter); _resizeWidget->setStyleSheet(QStringLiteral("background-color:palette(window);border-style:solid;border-width:1px;border-color:palette(dark)")); _resizeTimer = new QTimer(this); _resizeTimer->setInterval(SIZE_HINT_DURATION); _resizeTimer->setSingleShot(true); connect(_resizeTimer, &QTimer::timeout, _resizeWidget, &QLabel::hide); } QString sizeStr = i18n("Size: %1 x %2", _columns, _lines); _resizeWidget->setText(sizeStr); _resizeWidget->move((width() - _resizeWidget->width()) / 2, (height() - _resizeWidget->height()) / 2 + 20); _resizeWidget->show(); _resizeTimer->start(); } } void TerminalDisplay::paintEvent(QPaintEvent* pe) { QPainter paint(this); // Determine which characters should be repainted (1 region unit = 1 character) QRegion dirtyImageRegion; foreach(const QRect & rect, (pe->region() & contentsRect())) { dirtyImageRegion += widgetToImage(rect); drawBackground(paint, rect, getBackgroundColor(), true /* use opacity setting */); } paint.setRenderHint(QPainter::Antialiasing, _antialiasText); foreach(const QRect & rect, dirtyImageRegion) { drawContents(paint, rect); } drawCurrentResultRect(paint); drawInputMethodPreeditString(paint, preeditRect()); paintFilters(paint); const bool drawDimmed = _dimWhenInactive && !hasFocus(); const QColor dimColor(0, 0, 0, 128); foreach(const QRect & rect, (pe->region() & contentsRect())) { if (drawDimmed) { paint.fillRect(rect, dimColor); } } if (_drawOverlay) { const auto y = _headerBar->isVisible() ? _headerBar->height() : 0; const auto rect = _overlayEdge == Qt::LeftEdge ? QRect(0, y, width() / 2, height()) : _overlayEdge == Qt::TopEdge ? QRect(0, y, width(), height() / 2) : _overlayEdge == Qt::RightEdge ? QRect(width() - width() / 2, y, width() / 2, height()) : QRect(0, height() - height() / 2, width(), height() / 2); paint.setRenderHint(QPainter::Antialiasing); paint.setPen(Qt::NoPen); paint.setBrush(QColor(100,100,100, 127)); paint.drawRect(rect); } } void TerminalDisplay::printContent(QPainter& painter, bool friendly) { // Reinitialize the font with the printers paint device so the font // measurement calculations will be done correctly QFont savedFont = getVTFont(); QFont font(savedFont, painter.device()); painter.setFont(font); setVTFont(font); QRect rect(0, 0, _usedColumns, _usedLines); _printerFriendly = friendly; if (!friendly) { drawBackground(painter, rect, getBackgroundColor(), true /* use opacity setting */); } drawContents(painter, rect); _printerFriendly = false; setVTFont(savedFont); } QPoint TerminalDisplay::cursorPosition() const { if (!_screenWindow.isNull()) { return _screenWindow->cursorPosition(); } else { return {0, 0}; } } inline bool TerminalDisplay::isCursorOnDisplay() const { return cursorPosition().x() < _columns && cursorPosition().y() < _lines; } FilterChain* TerminalDisplay::filterChain() const { return _filterChain; } void TerminalDisplay::paintFilters(QPainter& painter) { if (_filterUpdateRequired) { return; } // get color of character under mouse and use it to draw // lines for filters QPoint cursorPos = mapFromGlobal(QCursor::pos()); int cursorLine; int cursorColumn; getCharacterPosition(cursorPos, cursorLine, cursorColumn, false); Character cursorCharacter = _image[loc(qMin(cursorColumn, _columns - 1), cursorLine)]; painter.setPen(QPen(cursorCharacter.foregroundColor.color(_colorTable))); // iterate over hotspots identified by the display's currently active filters // and draw appropriate visuals to indicate the presence of the hotspot QList spots = _filterChain->hotSpots(); int urlNumber, urlNumInc; if (_reverseUrlHints) { urlNumber = spots.size() + 1; urlNumInc = -1; } else { urlNumber = 0; urlNumInc = 1; } foreach(Filter::HotSpot* spot, spots) { urlNumber += urlNumInc; QRegion region; if (spot->type() == Filter::HotSpot::Link) { QRect r; if (spot->startLine() == spot->endLine()) { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } else { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) { r.setCoords(0 * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } r.setCoords(0 * _fontWidth + _contentRect.left(), spot->endLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } if (_showUrlHint && urlNumber < 10) { // Position at the beginning of the URL QRect hintRect(*region.begin()); hintRect.setWidth(r.height()); painter.fillRect(hintRect, QColor(0, 0, 0, 128)); painter.setPen(Qt::white); painter.drawRect(hintRect.adjusted(0, 0, -1, -1)); painter.drawText(hintRect, Qt::AlignCenter, QString::number(urlNumber)); } } for (int line = spot->startLine() ; line <= spot->endLine() ; line++) { int startColumn = 0; int endColumn = _columns - 1; // TODO use number of _columns which are actually // occupied on this line rather than the width of the // display in _columns // Check image size so _image[] is valid (see makeImage) if (endColumn >= _columns || line >= _lines) { break; } // ignore whitespace at the end of the lines while (_image[loc(endColumn, line)].isSpace() && endColumn > 0) { endColumn--; } // increment here because the column which we want to set 'endColumn' to // is the first whitespace character at the end of the line endColumn++; if (line == spot->startLine()) { startColumn = spot->startColumn(); } if (line == spot->endLine()) { endColumn = spot->endColumn(); } // TODO: resolve this comment with the new margin/center code // subtract one pixel from // the right and bottom so that // we do not overdraw adjacent // hotspots // // subtracting one pixel from all sides also prevents an edge case where // moving the mouse outside a link could still leave it underlined // because the check below for the position of the cursor // finds it on the border of the target area QRect r; r.setCoords(startColumn * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), endColumn * _fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); // Underline link hotspots if (spot->type() == Filter::HotSpot::Link) { QFontMetrics metrics(font()); // find the baseline (which is the invisible line that the characters in the font sit on, // with some having tails dangling below) const int baseline = r.bottom() - metrics.descent(); // find the position of the underline below that const int underlinePos = baseline + metrics.underlinePos(); if (_showUrlHint || region.contains(mapFromGlobal(QCursor::pos()))) { painter.drawLine(r.left() , underlinePos , r.right() , underlinePos); } // Marker hotspots simply have a transparent rectangular shape // drawn on top of them } else if (spot->type() == Filter::HotSpot::Marker) { //TODO - Do not use a hardcoded color for this const bool isCurrentResultLine = (_screenWindow->currentResultLine() == (spot->startLine() + _screenWindow->currentLine())); QColor color = isCurrentResultLine ? QColor(255, 255, 0, 120) : QColor(255, 0, 0, 120); painter.fillRect(r, color); } } } } inline static bool isRtl(const Character &chr) { uint c = 0; if ((chr.rendition & RE_EXTENDED_CHAR) == 0) { c = chr.character; } else { ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(chr.character, extendedCharLength); if (chars != nullptr) { c = chars[0]; } } switch(QChar::direction(c)) { case QChar::DirR: case QChar::DirAL: case QChar::DirRLE: case QChar::DirRLI: case QChar::DirRLO: return true; default: return false; } } void TerminalDisplay::drawContents(QPainter& paint, const QRect& rect) { const int numberOfColumns = _usedColumns; QVector univec; univec.reserve(numberOfColumns); for (int y = rect.y(); y <= rect.bottom(); y++) { int x = rect.x(); if ((_image[loc(rect.x(), y)].character == 0u) && (x != 0)) { x--; // Search for start of multi-column character } for (; x <= rect.right(); x++) { int len = 1; int p = 0; // reset our buffer to the number of columns int bufferSize = numberOfColumns; univec.resize(bufferSize); uint *disstrU = univec.data(); // is this a single character or a sequence of characters ? if ((_image[loc(x, y)].rendition & RE_EXTENDED_CHAR) != 0) { // sequence of characters ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(_image[loc(x, y)].character, extendedCharLength); if (chars != nullptr) { Q_ASSERT(extendedCharLength > 1); bufferSize += extendedCharLength - 1; univec.resize(bufferSize); disstrU = univec.data(); for (int index = 0 ; index < extendedCharLength ; index++) { Q_ASSERT(p < bufferSize); disstrU[p++] = chars[index]; } } } else { // single character const uint c = _image[loc(x, y)].character; if (c != 0u) { Q_ASSERT(p < bufferSize); disstrU[p++] = c; } } const bool lineDraw = LineBlockCharacters::canDraw(_image[loc(x, y)].character); const bool doubleWidth = (_image[qMin(loc(x, y) + 1, _imageSize - 1)].character == 0); const CharacterColor currentForeground = _image[loc(x, y)].foregroundColor; const CharacterColor currentBackground = _image[loc(x, y)].backgroundColor; const RenditionFlags currentRendition = _image[loc(x, y)].rendition; const bool rtl = isRtl(_image[loc(x, y)]); const auto isInsideDrawArea = [&](int column) { return column <= rect.right(); }; const auto hasSameColors = [&](int column) { return _image[loc(column, y)].foregroundColor == currentForeground && _image[loc(column, y)].backgroundColor == currentBackground; }; const auto hasSameRendition = [&](int column) { return (_image[loc(column, y)].rendition & ~RE_EXTENDED_CHAR) == (currentRendition & ~RE_EXTENDED_CHAR); }; const auto hasSameWidth = [&](int column) { const int characterLoc = qMin(loc(column, y) + 1, _imageSize - 1); return (_image[characterLoc].character == 0) == doubleWidth; }; const auto canBeGrouped = [&](int column) { return _image[loc(column, y)].character <= 0x7e || rtl; }; if (canBeGrouped(x)) { while (isInsideDrawArea(x + len) && hasSameColors(x + len) && hasSameRendition(x + len) && hasSameWidth(x + len) && canBeGrouped(x + len)) { const uint c = _image[loc(x + len, y)].character; if ((_image[loc(x + len, y)].rendition & RE_EXTENDED_CHAR) != 0) { // sequence of characters ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(c, extendedCharLength); if (chars != nullptr) { Q_ASSERT(extendedCharLength > 1); bufferSize += extendedCharLength - 1; univec.resize(bufferSize); disstrU = univec.data(); for (int index = 0 ; index < extendedCharLength ; index++) { Q_ASSERT(p < bufferSize); disstrU[p++] = chars[index]; } } } else { // single character if (c != 0u) { Q_ASSERT(p < bufferSize); disstrU[p++] = c; } } if (doubleWidth) { // assert((_image[loc(x+len,y)+1].character == 0)), see above if condition len++; // Skip trailing part of multi-column character } len++; } } else { // Group spaces following any non-wide character with the character. This allows for // rendering ambiguous characters with wide glyphs without clipping them. while (!doubleWidth && isInsideDrawArea(x + len) && _image[loc(x + len, y)].character == ' ' && hasSameColors(x + len) && hasSameRendition(x + len)) { // disstrU intentionally not modified - trailing spaces are meaningless len++; } } if ((x + len < _usedColumns) && (_image[loc(x + len, y)].character == 0u)) { len++; // Adjust for trailing part of multi-column character } const bool save__fixedFont = _fixedFont; if (lineDraw) { _fixedFont = false; } if (doubleWidth) { _fixedFont = false; } univec.resize(p); // Create a text scaling matrix for double width and double height lines. QMatrix textScale; if (y < _lineProperties.size()) { if ((_lineProperties[y] & LINE_DOUBLEWIDTH) != 0) { textScale.scale(2, 1); } if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) { textScale.scale(1, 2); } } //Apply text scaling matrix. paint.setWorldMatrix(textScale, true); //calculate the area in which the text will be drawn QRect textArea = QRect(_contentRect.left() + contentsRect().left() + _fontWidth * x, _contentRect.top() + contentsRect().top() + _fontHeight * y, _fontWidth * len, _fontHeight); //move the calculated area to take account of scaling applied to the painter. //the position of the area from the origin (0,0) is scaled //by the opposite of whatever //transformation has been applied to the painter. this ensures that //painting does actually start from textArea.topLeft() //(instead of textArea.topLeft() * painter-scale) textArea.moveTopLeft(textScale.inverted().map(textArea.topLeft())); QString unistr = QString::fromUcs4(univec.data(), univec.length()); //paint text fragment if (_printerFriendly) { drawPrinterFriendlyTextFragment(paint, textArea, unistr, &_image[loc(x, y)]); } else { drawTextFragment(paint, textArea, unistr, &_image[loc(x, y)]); } _fixedFont = save__fixedFont; //reset back to single-width, single-height _lines paint.setWorldMatrix(textScale.inverted(), true); if (y < _lineProperties.size() - 1) { //double-height _lines are represented by two adjacent _lines //containing the same characters //both _lines will have the LINE_DOUBLEHEIGHT attribute. //If the current line has the LINE_DOUBLEHEIGHT attribute, //we can therefore skip the next line if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) { y++; } } x += len - 1; } } } void TerminalDisplay::drawCurrentResultRect(QPainter& painter) { if(_screenWindow->currentResultLine() == -1) { return; } _searchResultRect.setRect(0, _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _fontHeight, _columns * _fontWidth, _fontHeight); painter.fillRect(_searchResultRect, QColor(0, 0, 255, 80)); } QRect TerminalDisplay::imageToWidget(const QRect& imageArea) const { QRect result; result.setLeft(_contentRect.left() + _fontWidth * imageArea.left()); result.setTop(_contentRect.top() + _fontHeight * imageArea.top()); result.setWidth(_fontWidth * imageArea.width()); result.setHeight(_fontHeight * imageArea.height()); return result; } QRect TerminalDisplay::widgetToImage(const QRect &widgetArea) const { QRect result; result.setLeft( qMin(_usedColumns - 1, qMax(0, (widgetArea.left() - contentsRect().left() - _contentRect.left()) / _fontWidth ))); result.setTop( qMin(_usedLines - 1, qMax(0, (widgetArea.top() - contentsRect().top() - _contentRect.top() ) / _fontHeight))); result.setRight( qMin(_usedColumns - 1, qMax(0, (widgetArea.right() - contentsRect().left() - _contentRect.left()) / _fontWidth ))); result.setBottom(qMin(_usedLines - 1, qMax(0, (widgetArea.bottom() - contentsRect().top() - _contentRect.top() ) / _fontHeight))); return result; } /* ------------------------------------------------------------------------- */ /* */ /* Blinking Text & Cursor */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setBlinkingCursorEnabled(bool blink) { _allowBlinkingCursor = blink; if (blink && !_blinkCursorTimer->isActive()) { _blinkCursorTimer->start(); } if (!blink && _blinkCursorTimer->isActive()) { _blinkCursorTimer->stop(); if (_cursorBlinking) { // if cursor is blinking(hidden), blink it again to make it show _cursorBlinking = false; updateCursor(); } Q_ASSERT(!_cursorBlinking); } } void TerminalDisplay::setBlinkingTextEnabled(bool blink) { _allowBlinkingText = blink; if (blink && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!blink && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } } void TerminalDisplay::focusOutEvent(QFocusEvent*) { // trigger a repaint of the cursor so that it is both: // // * visible (in case it was hidden during blinking) // * drawn in a focused out state _cursorBlinking = false; updateCursor(); // suppress further cursor blinking _blinkCursorTimer->stop(); Q_ASSERT(!_cursorBlinking); // if text is blinking (hidden), blink it again to make it shown if (_textBlinking) { blinkTextEvent(); } // suppress further text blinking _blinkTextTimer->stop(); Q_ASSERT(!_textBlinking); _showUrlHint = false; _headerBar->terminalFocusOut(); emit focusLost(); } void TerminalDisplay::focusInEvent(QFocusEvent*) { if (_allowBlinkingCursor) { _blinkCursorTimer->start(); } updateCursor(); if (_allowBlinkingText && _hasTextBlinker) { _blinkTextTimer->start(); } _headerBar->terminalFocusIn(); emit focusGained(); } void TerminalDisplay::blinkTextEvent() { Q_ASSERT(_allowBlinkingText); _textBlinking = !_textBlinking; // TODO: Optimize to only repaint the areas of the widget where there is // blinking text rather than repainting the whole widget. _headerBar->terminalFocusOut(); update(); } void TerminalDisplay::blinkCursorEvent() { Q_ASSERT(_allowBlinkingCursor); _cursorBlinking = !_cursorBlinking; updateCursor(); } void TerminalDisplay::updateCursor() { if (!isCursorOnDisplay()){ return; } const int cursorLocation = loc(cursorPosition().x(), cursorPosition().y()); Q_ASSERT(cursorLocation < _imageSize); int charWidth = _image[cursorLocation].width(); QRect cursorRect = imageToWidget(QRect(cursorPosition(), QSize(charWidth, 1))); update(cursorRect); } /* ------------------------------------------------------------------------- */ /* */ /* Geometry & Resizing */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::resizeEvent(QResizeEvent *event) { const auto width = event->size().width() - _scrollBar->geometry().width(); const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0; _searchBar->correctPosition(QSize(width, headerHeight)); if (contentsRect().isValid()) { updateImageSize(); updateImage(); } } void TerminalDisplay::propagateSize() { if (_image != nullptr) { updateImageSize(); } } void TerminalDisplay::updateImageSize() { Character* oldImage = _image; const int oldLines = _lines; const int oldColumns = _columns; makeImage(); if (oldImage != nullptr) { // copy the old image to reduce flicker int lines = qMin(oldLines, _lines); int columns = qMin(oldColumns, _columns); for (int line = 0; line < lines; line++) { memcpy((void*)&_image[_columns * line], (void*)&oldImage[oldColumns * line], columns * sizeof(Character)); } delete[] oldImage; } if (!_screenWindow.isNull()) { _screenWindow->setWindowLines(_lines); } _resizing = (oldLines != _lines) || (oldColumns != _columns); if (_resizing) { showResizeNotification(); emit changedContentSizeSignal(_contentRect.height(), _contentRect.width()); // expose resizeEvent } _resizing = false; } void TerminalDisplay::makeImage() { _wallpaper->load(); calcGeometry(); // confirm that array will be of non-zero size, since the painting code // assumes a non-zero array length Q_ASSERT(_lines > 0 && _columns > 0); Q_ASSERT(_usedLines <= _lines && _usedColumns <= _columns); _imageSize = _lines * _columns; _image = new Character[_imageSize]; clearImage(); } void TerminalDisplay::clearImage() { for (int i = 0; i < _imageSize; ++i) { _image[i] = Screen::DefaultChar; } } void TerminalDisplay::calcGeometry() { const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0; _scrollBar->resize( _scrollBar->sizeHint().width(), // width contentsRect().height() - headerHeight // height ); _contentRect = contentsRect().adjusted(_margin, _margin, -_margin, -_margin); switch (_scrollbarLocation) { case Enum::ScrollBarHidden : break; case Enum::ScrollBarLeft : _contentRect.setLeft(_contentRect.left() + _scrollBar->width()); _scrollBar->move(contentsRect().left(), contentsRect().top() + headerHeight); break; case Enum::ScrollBarRight: _contentRect.setRight(_contentRect.right() - _scrollBar->width()); _scrollBar->move(contentsRect().right() - _scrollBar->width() - 1, contentsRect().top() + headerHeight); break; } _contentRect.setTop(_contentRect.top() + headerHeight); // ensure that display is always at least one column wide _columns = qMax(1, _contentRect.width() / _fontWidth); _usedColumns = qMin(_usedColumns, _columns); // ensure that display is always at least one line high _lines = qMax(1, _contentRect.height() / _fontHeight); _usedLines = qMin(_usedLines, _lines); if(_centerContents) { QSize unusedPixels = _contentRect.size() - QSize(_columns * _fontWidth, _lines * _fontHeight); _contentRect.adjust(unusedPixels.width() / 2, unusedPixels.height() / 2, 0, 0); } } // calculate the needed size, this must be synced with calcGeometry() void TerminalDisplay::setSize(int columns, int lines) { const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->sizeHint().width(); const int horizontalMargin = _margin * 2; const int verticalMargin = _margin * 2; QSize newSize = QSize(horizontalMargin + scrollBarWidth + (columns * _fontWidth) , verticalMargin + (lines * _fontHeight)); if (newSize != size()) { _size = newSize; updateGeometry(); } } QSize TerminalDisplay::sizeHint() const { return _size; } //showEvent and hideEvent are reimplemented here so that it appears to other classes that the //display has been resized when the display is hidden or shown. // //TODO: Perhaps it would be better to have separate signals for show and hide instead of using //the same signal as the one for a content size change void TerminalDisplay::showEvent(QShowEvent*) { propagateSize(); emit changedContentSizeSignal(_contentRect.height(), _contentRect.width()); } void TerminalDisplay::hideEvent(QHideEvent*) { emit changedContentSizeSignal(_contentRect.height(), _contentRect.width()); } void TerminalDisplay::setMargin(int margin) { if (margin < 0) { margin = 0; } _margin = margin; updateImageSize(); } void TerminalDisplay::setCenterContents(bool enable) { _centerContents = enable; calcGeometry(); update(); } /* ------------------------------------------------------------------------- */ /* */ /* Scrollbar */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setScrollBarPosition(Enum::ScrollBarPositionEnum position) { if (_scrollbarLocation == position) { return; } _scrollBar->setHidden(position == Enum::ScrollBarHidden); _scrollbarLocation = position; propagateSize(); update(); } void TerminalDisplay::scrollBarPositionChanged(int) { if (_screenWindow.isNull()) { return; } _screenWindow->scrollTo(_scrollBar->value()); // if the thumb has been moved to the bottom of the _scrollBar then set // the display to automatically track new output, // that is, scroll down automatically // to how new _lines as they are added const bool atEndOfOutput = (_scrollBar->value() == _scrollBar->maximum()); _screenWindow->setTrackOutput(atEndOfOutput); updateImage(); } void TerminalDisplay::setScroll(int cursor, int slines) { // update _scrollBar if the range or value has changed, // otherwise return // // setting the range or value of a _scrollBar will always trigger // a repaint, so it should be avoided if it is not necessary if (_scrollBar->minimum() == 0 && _scrollBar->maximum() == (slines - _lines) && _scrollBar->value() == cursor) { return; } disconnect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged); _scrollBar->setRange(0, slines - _lines); _scrollBar->setSingleStep(1); _scrollBar->setPageStep(_lines); _scrollBar->setValue(cursor); connect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged); } void TerminalDisplay::setScrollFullPage(bool fullPage) { _scrollFullPage = fullPage; } bool TerminalDisplay::scrollFullPage() const { return _scrollFullPage; } /* ------------------------------------------------------------------------- */ /* */ /* Mouse */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::mousePressEvent(QMouseEvent* ev) { if (_possibleTripleClick && (ev->button() == Qt::LeftButton)) { mouseTripleClickEvent(ev); return; } if (!contentsRect().contains(ev->pos())) { return; } if (_screenWindow.isNull()) { return; } // Ignore clicks on the message widget if (_readOnlyMessageWidget != nullptr) { if (_readOnlyMessageWidget->isVisible() && _readOnlyMessageWidget->frameGeometry().contains(ev->pos())) { return; } } if (_outputSuspendedMessageWidget != nullptr) { if (_outputSuspendedMessageWidget->isVisible() && _outputSuspendedMessageWidget->frameGeometry().contains(ev->pos())) { return; } } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); QPoint pos = QPoint(charColumn, charLine); if (ev->button() == Qt::LeftButton) { // request the software keyboard, if any if (qApp->autoSipEnabled()) { auto behavior = QStyle::RequestSoftwareInputPanel( style()->styleHint(QStyle::SH_RequestSoftwareInputPanel)); if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) { QEvent event(QEvent::RequestSoftwareInputPanel); QApplication::sendEvent(this, &event); } } if (!ev->modifiers()) { _lineSelectionMode = false; _wordSelectionMode = false; } // The user clicked inside selected text bool selected = _screenWindow->isSelected(pos.x(), pos.y()); // Drag only when the Control key is held if ((!_ctrlRequiredForDrag || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && selected) { _dragInfo.state = diPending; _dragInfo.start = ev->pos(); } else { // No reason to ever start a drag event _dragInfo.state = diNone; _preserveLineBreaks = !(((ev->modifiers() & Qt::ControlModifier) != 0u) && !(ev->modifiers() & Qt::AltModifier)); _columnSelectionMode = ((ev->modifiers() & Qt::AltModifier) != 0u) && ((ev->modifiers() & Qt::ControlModifier) != 0u); // There are a couple of use cases when selecting text : // Normal buffer or Alternate buffer when not using Mouse Tracking: // select text or extendSelection or columnSelection or columnSelection + extendSelection // // Alternate buffer when using Mouse Tracking and with Shift pressed: // select text or columnSelection if (!_usesMouseTracking && ((ev->modifiers() == Qt::ShiftModifier) || (((ev->modifiers() & Qt::ShiftModifier) != 0u) && _columnSelectionMode))) { extendSelection(ev->pos()); } else if ((!_usesMouseTracking && !((ev->modifiers() & Qt::ShiftModifier))) || (_usesMouseTracking && ((ev->modifiers() & Qt::ShiftModifier) != 0u))) { _screenWindow->clearSelection(); pos.ry() += _scrollBar->value(); _iPntSel = _pntSel = pos; _actSel = 1; // left mouse button pressed but nothing selected yet. } else if (_usesMouseTracking && !_readOnly) { emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u))) { Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) { QObject action; action.setObjectName(QStringLiteral("open-action")); spot->activate(&action); } } } } else if (ev->button() == Qt::MidButton) { processMidButtonClick(ev); } else if (ev->button() == Qt::RightButton) { if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) { emit configureRequest(ev->pos()); } else { if(!_readOnly) { emit mouseSignal(2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } } QList TerminalDisplay::filterActions(const QPoint& position) { int charLine, charColumn; getCharacterPosition(position, charLine, charColumn, false); Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); return spot != nullptr ? spot->actions() : QList(); } void TerminalDisplay::mouseMoveEvent(QMouseEvent* ev) { int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); processFilters(); // handle filters // change link hot-spot appearance on mouse-over Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) { QRegion previousHotspotArea = _mouseOverHotspotArea; _mouseOverHotspotArea = QRegion(); QRect r; if (spot->startLine() == spot->endLine()) { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } else { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) { r.setCoords(0 * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } r.setCoords(0 * _fontWidth + _contentRect.left(), spot->endLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && (cursor().shape() != Qt::PointingHandCursor)) { setCursor(Qt::PointingHandCursor); } update(_mouseOverHotspotArea | previousHotspotArea); } else if (!_mouseOverHotspotArea.isEmpty()) { if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) || (cursor().shape() == Qt::PointingHandCursor)) { setCursor(_usesMouseTracking ? Qt::ArrowCursor : Qt::IBeamCursor); } update(_mouseOverHotspotArea); // set hotspot area to an invalid rectangle _mouseOverHotspotArea = QRegion(); } // for auto-hiding the cursor, we need mouseTracking if (ev->buttons() == Qt::NoButton) { return; } // if the program running in the terminal is interested in Mouse Tracking // evnets then emit a mouse movement signal, unless the shift key is // being held down, which overrides this. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { int button = 3; if ((ev->buttons() & Qt::LeftButton) != 0u) { button = 0; } if ((ev->buttons() & Qt::MidButton) != 0u) { button = 1; } if ((ev->buttons() & Qt::RightButton) != 0u) { button = 2; } emit mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 1); return; } if (_dragInfo.state == diPending) { // we had a mouse down, but haven't confirmed a drag yet // if the mouse has moved sufficiently, we will confirm const int distance = QApplication::startDragDistance(); if (ev->x() > _dragInfo.start.x() + distance || ev->x() < _dragInfo.start.x() - distance || ev->y() > _dragInfo.start.y() + distance || ev->y() < _dragInfo.start.y() - distance) { // we've left the drag square, we can start a real drag operation now _screenWindow->clearSelection(); doDrag(); } return; } else if (_dragInfo.state == diDragging) { // this isn't technically needed because mouseMoveEvent is suppressed during // Qt drag operations, replaced by dragMoveEvent return; } if (_actSel == 0) { return; } // don't extend selection while pasting if ((ev->buttons() & Qt::MidButton) != 0u) { return; } extendSelection(ev->pos()); } void TerminalDisplay::leaveEvent(QEvent *) { // remove underline from an active link when cursor leaves the widget area if(!_mouseOverHotspotArea.isEmpty()) { update(_mouseOverHotspotArea); _mouseOverHotspotArea = QRegion(); } } void TerminalDisplay::extendSelection(const QPoint& position) { if (_screenWindow.isNull()) { return; } //if ( !contentsRect().contains(ev->pos()) ) return; const QPoint tL = contentsRect().topLeft(); const int tLx = tL.x(); const int tLy = tL.y(); const int scroll = _scrollBar->value(); // we're in the process of moving the mouse with the left button pressed // the mouse cursor will kept caught within the bounds of the text in // this widget. int linesBeyondWidget = 0; QRect textBounds(tLx + _contentRect.left(), tLy + _contentRect.top(), _usedColumns * _fontWidth - 1, _usedLines * _fontHeight - 1); QPoint pos = position; // Adjust position within text area bounds. const QPoint oldpos = pos; pos.setX(qBound(textBounds.left(), pos.x(), textBounds.right())); pos.setY(qBound(textBounds.top(), pos.y(), textBounds.bottom())); if (oldpos.y() > textBounds.bottom()) { linesBeyondWidget = (oldpos.y() - textBounds.bottom()) / _fontHeight; _scrollBar->setValue(_scrollBar->value() + linesBeyondWidget + 1); // scrollforward } if (oldpos.y() < textBounds.top()) { linesBeyondWidget = (textBounds.top() - oldpos.y()) / _fontHeight; _scrollBar->setValue(_scrollBar->value() - linesBeyondWidget - 1); // history } int charColumn = 0; int charLine = 0; getCharacterPosition(pos, charLine, charColumn, true); QPoint here = QPoint(charColumn, charLine); QPoint ohere; QPoint _iPntSelCorr = _iPntSel; _iPntSelCorr.ry() -= _scrollBar->value(); QPoint _pntSelCorr = _pntSel; _pntSelCorr.ry() -= _scrollBar->value(); bool swapping = false; if (_wordSelectionMode) { // Extend to word boundaries const bool left_not_right = (here.y() < _iPntSelCorr.y() || (here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x())); const bool old_left_not_right = (_pntSelCorr.y() < _iPntSelCorr.y() || (_pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x())); swapping = left_not_right != old_left_not_right; // Find left (left_not_right ? from here : from start of word) QPoint left = left_not_right ? here : _iPntSelCorr; // Find left (left_not_right ? from end of word : from here) QPoint right = left_not_right ? _iPntSelCorr : here; if (left.y() < 0 || left.y() >= _lines || left.x() < 0 || left.x() >= _columns) { left = _pntSelCorr; } else { left = findWordStart(left); } if (right.y() < 0 || right.y() >= _lines || right.x() < 0 || right.x() >= _columns) { right = _pntSelCorr; } else { right = findWordEnd(right); } // Pick which is start (ohere) and which is extension (here) if (left_not_right) { here = left; ohere = right; } else { here = right; ohere = left; } ohere.rx()++; } if (_lineSelectionMode) { // Extend to complete line const bool above_not_below = (here.y() < _iPntSelCorr.y()); if (above_not_below) { ohere = findLineEnd(_iPntSelCorr); here = findLineStart(here); } else { ohere = findLineStart(_iPntSelCorr); here = findLineEnd(here); } swapping = !(_tripleSelBegin == ohere); _tripleSelBegin = ohere; ohere.rx()++; } int offset = 0; if (!_wordSelectionMode && !_lineSelectionMode) { QChar selClass; const bool left_not_right = (here.y() < _iPntSelCorr.y() || (here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x())); const bool old_left_not_right = (_pntSelCorr.y() < _iPntSelCorr.y() || (_pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x())); swapping = left_not_right != old_left_not_right; // Find left (left_not_right ? from here : from start) const QPoint left = left_not_right ? here : _iPntSelCorr; // Find left (left_not_right ? from start : from here) QPoint right = left_not_right ? _iPntSelCorr : here; if (right.x() > 0 && !_columnSelectionMode) { if (right.x() - 1 < _columns && right.y() < _lines) { selClass = charClass(_image[loc(right.x() - 1, right.y())]); } } // Pick which is start (ohere) and which is extension (here) if (left_not_right) { here = left; ohere = right; offset = 0; } else { here = right; ohere = left; offset = -1; } } if ((here == _pntSelCorr) && (scroll == _scrollBar->value())) { return; // not moved } if (here == ohere) { return; // It's not left, it's not right. } if (_actSel < 2 || swapping) { if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) { _screenWindow->setSelectionStart(ohere.x() , ohere.y() , true); } else { _screenWindow->setSelectionStart(ohere.x() - 1 - offset , ohere.y() , false); } } _actSel = 2; // within selection _pntSel = here; _pntSel.ry() += _scrollBar->value(); if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) { _screenWindow->setSelectionEnd(here.x() , here.y()); } else { _screenWindow->setSelectionEnd(here.x() + offset , here.y()); } } void TerminalDisplay::mouseReleaseEvent(QMouseEvent* ev) { if (_screenWindow.isNull()) { return; } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); if (ev->button() == Qt::LeftButton) { if (_dragInfo.state == diPending) { // We had a drag event pending but never confirmed. Kill selection _screenWindow->clearSelection(); } else { if (_actSel > 1) { copyToX11Selection(); } _actSel = 0; //FIXME: emits a release event even if the mouse is // outside the range. The procedure used in `mouseMoveEvent' // applies here, too. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2); } } _dragInfo.state = diNone; } if (_usesMouseTracking && (ev->button() == Qt::RightButton || ev->button() == Qt::MidButton) && !(ev->modifiers() & Qt::ShiftModifier)) { emit mouseSignal(ev->button() == Qt::MidButton ? 1 : 2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2); } } void TerminalDisplay::getCharacterPosition(const QPoint& widgetPoint, int& line, int& column, bool edge) const { // the column value returned can be equal to _usedColumns (when edge == true), // which is the position just after the last character displayed in a line. // // this is required so that the user can select characters in the right-most // column (or left-most for right-to-left input) const int columnMax = edge ? _usedColumns : _usedColumns - 1; const int xOffset = edge ? _fontWidth / 2 : 0; column = qBound(0, (widgetPoint.x() + xOffset - contentsRect().left() - _contentRect.left()) / _fontWidth, columnMax); line = qBound(0, (widgetPoint.y() - contentsRect().top() - _contentRect.top()) / _fontHeight, _usedLines - 1); } void TerminalDisplay::updateLineProperties() { if (_screenWindow.isNull()) { return; } _lineProperties = _screenWindow->getLineProperties(); } void TerminalDisplay::processMidButtonClick(QMouseEvent* ev) { if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) { const bool appendEnter = (ev->modifiers() & Qt::ControlModifier) != 0u; if (_middleClickPasteMode == Enum::PasteFromX11Selection) { pasteFromX11Selection(appendEnter); } else if (_middleClickPasteMode == Enum::PasteFromClipboard) { pasteFromClipboard(appendEnter); } else { Q_ASSERT(false); } } else { if(!_readOnly) { int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); emit mouseSignal(1, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } void TerminalDisplay::mouseDoubleClickEvent(QMouseEvent* ev) { // Yes, successive middle click can trigger this event if (ev->button() == Qt::MidButton) { processMidButtonClick(ev); return; } if (ev->button() != Qt::LeftButton) { return; } if (_screenWindow.isNull()) { return; } int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); QPoint pos(qMin(charColumn, _columns - 1), qMin(charLine, _lines - 1)); // pass on double click as two clicks. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { if(!_readOnly) { // Send just _ONE_ click event, since the first click of the double click // was already sent by the click handler emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0); // left button } return; } _screenWindow->clearSelection(); _iPntSel = pos; _iPntSel.ry() += _scrollBar->value(); _wordSelectionMode = true; _actSel = 2; // within selection // find word boundaries... { // find the start of the word const QPoint bgnSel = findWordStart(pos); const QPoint endSel = findWordEnd(pos); _actSel = 2; // within selection _screenWindow->setSelectionStart(bgnSel.x() , bgnSel.y() , false); _screenWindow->setSelectionEnd(endSel.x() , endSel.y()); copyToX11Selection(); } _possibleTripleClick = true; QTimer::singleShot(QApplication::doubleClickInterval(), this, [this]() { _possibleTripleClick = false; }); } void TerminalDisplay::wheelEvent(QWheelEvent* ev) { // Only vertical scrolling is supported if (ev->orientation() != Qt::Vertical) { return; } const int modifiers = ev->modifiers(); // ctrl+ for zooming, like in konqueror and firefox if (((modifiers & Qt::ControlModifier) != 0u) && _mouseWheelZoom) { _scrollWheelState.addWheelEvent(ev); int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE); for (;steps > 0; --steps) { // wheel-up for increasing font size increaseFontSize(); } for (;steps < 0; ++steps) { // wheel-down for decreasing font size decreaseFontSize(); } } else if (!_usesMouseTracking && (_scrollBar->maximum() > 0)) { // If the program running in the terminal is not interested in Mouse // Tracking events, send the event to the scrollbar if the slider // has room to move _scrollWheelState.addWheelEvent(ev); _scrollBar->event(ev); Q_ASSERT(_sessionController != nullptr); _sessionController->setSearchStartToWindowCurrentLine(); _scrollWheelState.clearAll(); } else if (!_readOnly) { _scrollWheelState.addWheelEvent(ev); Q_ASSERT(!_sessionController->session().isNull()); if(!_usesMouseTracking && !_sessionController->session()->isPrimaryScreen() && _alternateScrolling) { // Send simulated up / down key presses to the terminal program // for the benefit of programs such as 'less' (which use the alternate screen) // assume that each Up / Down key event will cause the terminal application // to scroll by one line. // // to get a reasonable scrolling speed, scroll by one line for every 5 degrees // of mouse wheel rotation. Mouse wheels typically move in steps of 15 degrees, // giving a scroll of 3 lines const int lines = _scrollWheelState.consumeSteps(static_cast(_fontHeight * qApp->devicePixelRatio()), ScrollState::degreesToAngle(5)); const int keyCode = lines > 0 ? Qt::Key_Up : Qt::Key_Down; QKeyEvent keyEvent(QEvent::KeyPress, keyCode, Qt::NoModifier); for (int i = 0; i < abs(lines); i++) { _screenWindow->screen()->setCurrentTerminalDisplay(this); emit keyPressedSignal(&keyEvent); } } else if (_usesMouseTracking) { // terminal program wants notification of mouse activity int charLine; int charColumn; getCharacterPosition(ev->pos() , charLine , charColumn, !_usesMouseTracking); const int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE); const int button = (steps > 0) ? 4 : 5; for (int i = 0; i < abs(steps); ++i) { emit mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } } void TerminalDisplay::viewScrolledByUser() { Q_ASSERT(_sessionController != nullptr); _sessionController->setSearchStartToWindowCurrentLine(); } /* Moving left/up from the line containing pnt, return the starting offset point which the given line is continuously wrapped (top left corner = 0,0; previous line not visible = 0,-1). */ QPoint TerminalDisplay::findLineStart(const QPoint &pnt) { const int visibleScreenLines = _lineProperties.size(); const int topVisibleLine = _screenWindow->currentLine(); Screen *screen = _screenWindow->screen(); int line = pnt.y(); int lineInHistory= line + topVisibleLine; QVector lineProperties = _lineProperties; while (lineInHistory > 0) { for (; line > 0; line--, lineInHistory--) { // Does previous line wrap around? if ((lineProperties[line - 1] & LINE_WRAPPED) == 0) { return {0, lineInHistory - topVisibleLine}; } } if (lineInHistory < 1) { break; } // _lineProperties is only for the visible screen, so grab new data int newRegionStart = qMax(0, lineInHistory - visibleScreenLines); lineProperties = screen->getLineProperties(newRegionStart, lineInHistory - 1); line = lineInHistory - newRegionStart; } return {0, lineInHistory - topVisibleLine}; } /* Moving right/down from the line containing pnt, return the ending offset point which the given line is continuously wrapped. */ QPoint TerminalDisplay::findLineEnd(const QPoint &pnt) { const int visibleScreenLines = _lineProperties.size(); const int topVisibleLine = _screenWindow->currentLine(); const int maxY = _screenWindow->lineCount() - 1; Screen *screen = _screenWindow->screen(); int line = pnt.y(); int lineInHistory= line + topVisibleLine; QVector lineProperties = _lineProperties; while (lineInHistory < maxY) { for (; line < lineProperties.count() && lineInHistory < maxY; line++, lineInHistory++) { // Does current line wrap around? if ((lineProperties[line] & LINE_WRAPPED) == 0) { return {_columns - 1, lineInHistory - topVisibleLine}; } } line = 0; lineProperties = screen->getLineProperties(lineInHistory, qMin(lineInHistory + visibleScreenLines, maxY)); } return {_columns - 1, lineInHistory - topVisibleLine}; } QPoint TerminalDisplay::findWordStart(const QPoint &pnt) { const int regSize = qMax(_screenWindow->windowLines(), 10); const int firstVisibleLine = _screenWindow->currentLine(); Screen *screen = _screenWindow->screen(); Character *image = _image; Character *tmp_image = nullptr; int imgLine = pnt.y(); int x = pnt.x(); int y = imgLine + firstVisibleLine; int imgLoc = loc(x, imgLine); QVector lineProperties = _lineProperties; const QChar selClass = charClass(image[imgLoc]); const int imageSize = regSize * _columns; while (true) { for (;;imgLoc--, x--) { if (imgLoc < 1) { // no more chars in this region break; } if (x > 0) { // has previous char on this line if (charClass(image[imgLoc - 1]) == selClass) { continue; } goto out; } else if (imgLine > 0) { // not the first line in the session if ((lineProperties[imgLine - 1] & LINE_WRAPPED) != 0) { // have continuation on prev line if (charClass(image[imgLoc - 1]) == selClass) { x = _columns; imgLine--; y--; continue; } } goto out; } else if (y > 0) { // want more data, but need to fetch new region break; } else { goto out; } } if (y <= 0) { // No more data goto out; } int newRegStart = qMax(0, y - regSize + 1); lineProperties = screen->getLineProperties(newRegStart, y - 1); imgLine = y - newRegStart; delete[] tmp_image; tmp_image = new Character[imageSize]; image = tmp_image; screen->getImage(tmp_image, imageSize, newRegStart, y - 1); imgLoc = loc(x, imgLine); if (imgLoc < 1) { // Reached the start of the session break; } } out: delete[] tmp_image; return {x, y - firstVisibleLine}; } QPoint TerminalDisplay::findWordEnd(const QPoint &pnt) { const int regSize = qMax(_screenWindow->windowLines(), 10); const int curLine = _screenWindow->currentLine(); int i = pnt.y(); int x = pnt.x(); int y = i + curLine; int j = loc(x, i); QVector lineProperties = _lineProperties; Screen *screen = _screenWindow->screen(); Character *image = _image; Character *tmp_image = nullptr; const QChar selClass = charClass(image[j]); const int imageSize = regSize * _columns; const int maxY = _screenWindow->lineCount() - 1; const int maxX = _columns - 1; while (true) { const int lineCount = lineProperties.count(); for (;;j++, x++) { if (x < maxX) { if (charClass(image[j + 1]) == selClass && // A colon right before whitespace is never part of a word ! (image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) { continue; } goto out; } else if (i < lineCount - 1) { if (((lineProperties[i] & LINE_WRAPPED) != 0) && charClass(image[j + 1]) == selClass && // A colon right before whitespace is never part of a word ! (image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) { x = -1; i++; y++; continue; } goto out; } else if (y < maxY) { if (i < lineCount && ((lineProperties[i] & LINE_WRAPPED) == 0)) { goto out; } break; } else { goto out; } } int newRegEnd = qMin(y + regSize - 1, maxY); lineProperties = screen->getLineProperties(y, newRegEnd); i = 0; if (tmp_image == nullptr) { tmp_image = new Character[imageSize]; image = tmp_image; } screen->getImage(tmp_image, imageSize, y, newRegEnd); x--; j = loc(x, i); } out: y -= curLine; // In word selection mode don't select @ (64) if at end of word. if (((image[j].rendition & RE_EXTENDED_CHAR) == 0) && (QChar(image[j].character) == QLatin1Char('@')) && (y > pnt.y() || x > pnt.x())) { if (x > 0) { x--; } else { y--; } } delete[] tmp_image; return {x, y}; } Screen::DecodingOptions TerminalDisplay::currentDecodingOptions() { Screen::DecodingOptions decodingOptions; if (_preserveLineBreaks) { decodingOptions |= Screen::PreserveLineBreaks; } if (_trimLeadingSpaces) { decodingOptions |= Screen::TrimLeadingWhitespace; } if (_trimTrailingSpaces) { decodingOptions |= Screen::TrimTrailingWhitespace; } return decodingOptions; } void TerminalDisplay::mouseTripleClickEvent(QMouseEvent* ev) { if (_screenWindow.isNull()) { return; } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, true); selectLine(QPoint(charColumn, charLine), _tripleClickMode == Enum::SelectWholeLine); } void TerminalDisplay::selectLine(QPoint pos, bool entireLine) { _iPntSel = pos; _screenWindow->clearSelection(); _lineSelectionMode = true; _wordSelectionMode = false; _actSel = 2; // within selection if (!entireLine) { // Select from cursor to end of line _tripleSelBegin = findWordStart(_iPntSel); _screenWindow->setSelectionStart(_tripleSelBegin.x(), _tripleSelBegin.y() , false); } else { _tripleSelBegin = findLineStart(_iPntSel); _screenWindow->setSelectionStart(0 , _tripleSelBegin.y() , false); } _iPntSel = findLineEnd(_iPntSel); _screenWindow->setSelectionEnd(_iPntSel.x() , _iPntSel.y()); copyToX11Selection(); _iPntSel.ry() += _scrollBar->value(); } void TerminalDisplay::selectCurrentLine() { if (_screenWindow.isNull()) { return; } selectLine(cursorPosition(), true); } void TerminalDisplay::selectAll() { if (_screenWindow.isNull()) { return; } _preserveLineBreaks = true; _screenWindow->setSelectionByLineRange(0, _screenWindow->lineCount()); copyToX11Selection(); } bool TerminalDisplay::focusNextPrevChild(bool next) { // for 'Tab', always disable focus switching among widgets // for 'Shift+Tab', leave the decision to higher level if (next) { return false; } else { return QWidget::focusNextPrevChild(next); } } QChar TerminalDisplay::charClass(const Character& ch) const { if ((ch.rendition & RE_EXTENDED_CHAR) != 0) { ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength); if ((chars != nullptr) && extendedCharLength > 0) { const QString s = QString::fromUcs4(chars, extendedCharLength); if (_wordCharacters.contains(s, Qt::CaseInsensitive)) { return QLatin1Char('a'); } bool letterOrNumber = false; for (int i = 0; !letterOrNumber && i < s.size(); ++i) { letterOrNumber = s.at(i).isLetterOrNumber(); } return letterOrNumber ? QLatin1Char('a') : s.at(0); } return 0; } else { const QChar qch(ch.character); if (qch.isSpace()) { return QLatin1Char(' '); } if (qch.isLetterOrNumber() || _wordCharacters.contains(qch, Qt::CaseInsensitive)) { return QLatin1Char('a'); } return qch; } } void TerminalDisplay::setWordCharacters(const QString& wc) { _wordCharacters = wc; } void TerminalDisplay::setUsesMouseTracking(bool on) { _usesMouseTracking = on; setCursor(_usesMouseTracking ? Qt::ArrowCursor : Qt::IBeamCursor); } bool TerminalDisplay::usesMouseTracking() const { return _usesMouseTracking; } void TerminalDisplay::setAlternateScrolling(bool enable) { _alternateScrolling = enable; } bool TerminalDisplay::alternateScrolling() const { return _alternateScrolling; } void TerminalDisplay::setBracketedPasteMode(bool on) { _bracketedPasteMode = on; } bool TerminalDisplay::bracketedPasteMode() const { return _bracketedPasteMode; } /* ------------------------------------------------------------------------- */ /* */ /* Clipboard */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::doPaste(QString text, bool appendReturn) { if (_screenWindow.isNull()) { return; } if (_readOnly) { return; } if (appendReturn) { text.append(QLatin1String("\r")); } if (text.length() > 8000) { if (KMessageBox::warningContinueCancel(window(), i18np("Are you sure you want to paste %1 character?", "Are you sure you want to paste %1 characters?", text.length()), i18n("Confirm Paste"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("ShowPasteHugeTextWarning")) == KMessageBox::Cancel) { return; } } QStringList unsafeCharacters; for (const QChar &c : text) { if (!c.isPrint() && c != QLatin1Char('\t') && c != QLatin1Char('\n')) { QString description; switch(c.unicode()) { case '\x03': description = i18n("^C Interrupt: May abort the current process"); break; case '\x04': description = i18n("^D End of transmission: May exit the current process"); break; case '\x07': description = i18n("^G Bell: Will try to emit an audible warning"); break; case '\x08': description = i18n("^H Backspace"); break; case '\x13': description = i18n("^S Scroll lock: Locks terminal output"); break; case '\x1a': description = i18n("^Z Suspend: Stops current process"); break; case '\x1b': description = i18n("ESC: Used for special commands to the current process"); break; default: description = ki18n("Other unprintable character (\\x%1)").subs(c.unicode(), 0, 16).toString(); break; } unsafeCharacters.append(description); } } unsafeCharacters.removeDuplicates(); if (!unsafeCharacters.isEmpty()) { int result = KMessageBox::warningYesNoCancelList(window(), i18n("The text you're trying to paste contains hidden unprintable characters, " "do you want to filter them out?"), unsafeCharacters, i18nc("@title", "Filter"), KGuiItem(i18nc("@action:button", "&Remove unprintable"), QStringLiteral("filter-symbolic")), KGuiItem(i18nc("@action:button", "Confirm &paste"), QStringLiteral("edit-paste")), KGuiItem(i18nc("@action:button", "&Cancel paste"), QStringLiteral("dialog-cancel")), QStringLiteral("ShowPasteUnprintableWarning") ); switch(result){ case KMessageBox::Cancel: return; case KMessageBox::Yes: { QString sanitized; for (const QChar &c : text) { if (c.isPrint() || c == QLatin1Char('\t') || c == QLatin1Char('\n')) { sanitized.append(c); } } text = sanitized; } case KMessageBox::No: break; default: break; } } if (!text.isEmpty()) { text.replace(QLatin1Char('\n'), QLatin1Char('\r')); if (bracketedPasteMode()) { text.remove(QLatin1String("\033")); text.prepend(QLatin1String("\033[200~")); text.append(QLatin1String("\033[201~")); } // perform paste by simulating keypress events QKeyEvent e(QEvent::KeyPress, 0, Qt::NoModifier, text); emit keyPressedSignal(&e); } } void TerminalDisplay::setAutoCopySelectedText(bool enabled) { _autoCopySelectedText = enabled; } void TerminalDisplay::setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode) { _middleClickPasteMode = mode; } void TerminalDisplay::setCopyTextAsHTML(bool enabled) { _copyTextAsHTML = enabled; } void TerminalDisplay::copyToX11Selection() { if (_screenWindow.isNull()) { return; } const QString &text = _screenWindow->selectedText(currentDecodingOptions()); if (text.isEmpty()) { return; } auto mimeData = new QMimeData; mimeData->setText(text); if (_copyTextAsHTML) { mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml)); } if (QApplication::clipboard()->supportsSelection()) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection); } if (_autoCopySelectedText) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } } void TerminalDisplay::copyToClipboard() { if (_screenWindow.isNull()) { return; } const QString &text = _screenWindow->selectedText(currentDecodingOptions()); if (text.isEmpty()) { return; } auto mimeData = new QMimeData; mimeData->setText(text); if (_copyTextAsHTML) { mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml)); } QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void TerminalDisplay::pasteFromClipboard(bool appendEnter) { QString text = QApplication::clipboard()->text(QClipboard::Clipboard); doPaste(text, appendEnter); } void TerminalDisplay::pasteFromX11Selection(bool appendEnter) { if (QApplication::clipboard()->supportsSelection()) { QString text = QApplication::clipboard()->text(QClipboard::Selection); doPaste(text, appendEnter); } } /* ------------------------------------------------------------------------- */ /* */ /* Input Method */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::inputMethodEvent(QInputMethodEvent* event) { if (!event->commitString().isEmpty()) { QKeyEvent keyEvent(QEvent::KeyPress, 0, Qt::NoModifier, event->commitString()); emit keyPressedSignal(&keyEvent); } if (!_readOnly && isCursorOnDisplay()) { _inputMethodData.preeditString = event->preeditString(); update(preeditRect() | _inputMethodData.previousPreeditRect); } event->accept(); } QVariant TerminalDisplay::inputMethodQuery(Qt::InputMethodQuery query) const { const QPoint cursorPos = cursorPosition(); switch (query) { case Qt::ImMicroFocus: return imageToWidget(QRect(cursorPos.x(), cursorPos.y(), 1, 1)); case Qt::ImFont: return font(); case Qt::ImCursorPosition: // return the cursor position within the current line return cursorPos.x(); case Qt::ImSurroundingText: { // return the text from the current line QString lineText; QTextStream stream(&lineText); PlainTextDecoder decoder; decoder.begin(&stream); if (isCursorOnDisplay()) { decoder.decodeLine(&_image[loc(0, cursorPos.y())], _usedColumns, LINE_DEFAULT); } decoder.end(); return lineText; } case Qt::ImCurrentSelection: return QString(); default: break; } return QVariant(); } QRect TerminalDisplay::preeditRect() const { const int preeditLength = Character::stringWidth(_inputMethodData.preeditString); if (preeditLength == 0) { return {}; } const QRect stringRect(_contentRect.left() + _fontWidth * cursorPosition().x(), _contentRect.top() + _fontHeight * cursorPosition().y(), _fontWidth * preeditLength, _fontHeight); return stringRect.intersected(_contentRect); } void TerminalDisplay::drawInputMethodPreeditString(QPainter& painter , const QRect& rect) { if (_inputMethodData.preeditString.isEmpty() || !isCursorOnDisplay()) { return; } const QPoint cursorPos = cursorPosition(); bool invertColors = false; const QColor background = _colorTable[DEFAULT_BACK_COLOR]; const QColor foreground = _colorTable[DEFAULT_FORE_COLOR]; const Character* style = &_image[loc(cursorPos.x(), cursorPos.y())]; drawBackground(painter, rect, background, true); drawCursor(painter, rect, foreground, background, invertColors); drawCharacters(painter, rect, _inputMethodData.preeditString, style, invertColors); _inputMethodData.previousPreeditRect = rect; } /* ------------------------------------------------------------------------- */ /* */ /* Keyboard */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setFlowControlWarningEnabled(bool enable) { _flowControlWarningEnabled = enable; // if the dialog is currently visible and the flow control warning has // been disabled then hide the dialog if (!enable) { outputSuspended(false); } } void TerminalDisplay::outputSuspended(bool suspended) { //create the label when this function is first called if (_outputSuspendedMessageWidget == nullptr) { //This label includes a link to an English language website //describing the 'flow control' (Xon/Xoff) feature found in almost //all terminal emulators. //If there isn't a suitable article available in the target language the link //can simply be removed. _outputSuspendedMessageWidget = createMessageWidget(i18n("Output has been " "suspended" " by pressing Ctrl+S." " Press Ctrl+Q to resume.")); connect(_outputSuspendedMessageWidget, &KMessageWidget::linkActivated, this, [](const QString &url) { QDesktopServices::openUrl(QUrl(url)); }); _outputSuspendedMessageWidget->setMessageType(KMessageWidget::Warning); } suspended ? _outputSuspendedMessageWidget->animatedShow() : _outputSuspendedMessageWidget->animatedHide(); } void TerminalDisplay::dismissOutputSuspendedMessage() { outputSuspended(false); } KMessageWidget* TerminalDisplay::createMessageWidget(const QString &text) { auto widget = new KMessageWidget(text); widget->setWordWrap(true); widget->setFocusProxy(this); widget->setCursor(Qt::ArrowCursor); _verticalLayout->insertWidget(1, widget); return widget; } void TerminalDisplay::updateReadOnlyState(bool readonly) { if (_readOnly == readonly) { return; } if (readonly) { // Lazy create the readonly messagewidget if (_readOnlyMessageWidget == nullptr) { _readOnlyMessageWidget = createMessageWidget(i18n("This terminal is read-only.")); _readOnlyMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("object-locked"))); } } if (_readOnlyMessageWidget != nullptr) { readonly ? _readOnlyMessageWidget->animatedShow() : _readOnlyMessageWidget->animatedHide(); } _readOnly = readonly; } void TerminalDisplay::scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode, int amount) { _screenWindow->scrollBy(mode, amount, _scrollFullPage); _screenWindow->setTrackOutput(_screenWindow->atEndOfOutput()); updateLineProperties(); updateImage(); viewScrolledByUser(); } void TerminalDisplay::keyPressEvent(QKeyEvent* event) { if ((_urlHintsModifiers != 0u) && event->modifiers() == _urlHintsModifiers) { int nHotSpots = _filterChain->hotSpots().count(); int hintSelected = event->key() - 0x31; if (hintSelected >= 0 && hintSelected < 10 && hintSelected < nHotSpots) { if (_reverseUrlHints) { hintSelected = nHotSpots - hintSelected - 1; } _filterChain->hotSpots().at(hintSelected)->activate(); _showUrlHint = false; update(); return; } if (!_showUrlHint) { processFilters(); _showUrlHint = true; update(); } } _screenWindow->screen()->setCurrentTerminalDisplay(this); if (!_readOnly) { _actSel = 0; // Key stroke implies a screen update, so TerminalDisplay won't // know where the current selection is. if (_allowBlinkingCursor) { _blinkCursorTimer->start(); if (_cursorBlinking) { // if cursor is blinking(hidden), blink it again to show it blinkCursorEvent(); } Q_ASSERT(!_cursorBlinking); } } if (_searchBar->isVisible() && event->key() == Qt::Key_Escape) { _sessionController->searchClosed(); } emit keyPressedSignal(event); #ifndef QT_NO_ACCESSIBILITY if (!_readOnly) { QAccessibleTextCursorEvent textCursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX()); QAccessible::updateAccessibility(&textCursorEvent); } #endif event->accept(); } void TerminalDisplay::keyReleaseEvent(QKeyEvent *event) { if (_showUrlHint) { _showUrlHint = false; update(); } if (_readOnly) { event->accept(); return; } QWidget::keyReleaseEvent(event); } bool TerminalDisplay::handleShortcutOverrideEvent(QKeyEvent* keyEvent) { const int modifiers = keyEvent->modifiers(); // When a possible shortcut combination is pressed, // emit the overrideShortcutCheck() signal to allow the host // to decide whether the terminal should override it or not. if (modifiers != Qt::NoModifier) { int modifierCount = 0; unsigned int currentModifier = Qt::ShiftModifier; while (currentModifier <= Qt::KeypadModifier) { if ((modifiers & currentModifier) != 0u) { modifierCount++; } currentModifier <<= 1; } if (modifierCount < 2) { bool override = false; emit overrideShortcutCheck(keyEvent, override); if (override) { keyEvent->accept(); return true; } } } // Override any of the following shortcuts because // they are needed by the terminal int keyCode = keyEvent->key() | modifiers; switch (keyCode) { // list is taken from the QLineEdit::event() code case Qt::Key_Tab: case Qt::Key_Delete: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Backspace: case Qt::Key_Left: case Qt::Key_Right: case Qt::Key_Slash: case Qt::Key_Period: case Qt::Key_Space: keyEvent->accept(); return true; } return false; } bool TerminalDisplay::event(QEvent* event) { bool eventHandled = false; switch (event->type()) { case QEvent::ShortcutOverride: eventHandled = handleShortcutOverrideEvent(static_cast(event)); break; case QEvent::PaletteChange: case QEvent::ApplicationPaletteChange: onColorsChanged(); break; case QEvent::FocusOut: case QEvent::FocusIn: if(_screenWindow != nullptr) { // force a redraw on focusIn, fixes the // black screen bug when the view is focused // but doesn't redraws. _screenWindow->notifyOutputChanged(); } update(); break; default: break; } return eventHandled ? true : QWidget::event(event); } void TerminalDisplay::contextMenuEvent(QContextMenuEvent* event) { // the logic for the mouse case is within MousePressEvent() if (event->reason() != QContextMenuEvent::Mouse) { emit configureRequest(mapFromGlobal(QCursor::pos())); } } /* --------------------------------------------------------------------- */ /* */ /* Bell */ /* */ /* --------------------------------------------------------------------- */ void TerminalDisplay::setBellMode(int mode) { _bellMode = mode; } int TerminalDisplay::bellMode() const { return _bellMode; } void TerminalDisplay::bell(const QString& message) { if (_bellMasked) { return; } switch (_bellMode) { case Enum::SystemBeepBell: KNotification::beep(); break; case Enum::NotifyBell: // STABLE API: // Please note that these event names, "BellVisible" and "BellInvisible", // should not change and should be kept stable, because other applications // that use this code via KPart rely on these names for notifications. KNotification::event(hasFocus() ? QStringLiteral("BellVisible") : QStringLiteral("BellInvisible"), message, QPixmap(), this); break; case Enum::VisualBell: visualBell(); break; default: break; } // limit the rate at which bells can occur. // ...mainly for sound effects where rapid bells in sequence // produce a horrible noise. _bellMasked = true; QTimer::singleShot(500, this, [this]() { _bellMasked = false; }); } void TerminalDisplay::visualBell() { swapFGBGColors(); QTimer::singleShot(200, this, &Konsole::TerminalDisplay::swapFGBGColors); } void TerminalDisplay::swapFGBGColors() { // swap the default foreground & background color ColorEntry color = _colorTable[DEFAULT_BACK_COLOR]; _colorTable[DEFAULT_BACK_COLOR] = _colorTable[DEFAULT_FORE_COLOR]; _colorTable[DEFAULT_FORE_COLOR] = color; onColorsChanged(); } /* --------------------------------------------------------------------- */ /* */ /* Drag & Drop */ /* */ /* --------------------------------------------------------------------- */ void TerminalDisplay::dragEnterEvent(QDragEnterEvent* event) { // text/plain alone is enough for KDE-apps // text/uri-list is for supporting some non-KDE apps, such as thunar // and pcmanfm // That also applies in dropEvent() const auto mimeData = event->mimeData(); if ((!_readOnly) && (mimeData != nullptr) && (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list")))) { event->acceptProposedAction(); } } namespace { QString extractDroppedText(const QList& urls) { QString dropText; for (int i = 0 ; i < urls.count() ; i++) { KIO::StatJob* job = KIO::mostLocalUrl(urls[i], KIO::HideProgressInfo); if (!job->exec()) { continue; } const QUrl url = job->mostLocalUrl(); // in future it may be useful to be able to insert file names with drag-and-drop // without quoting them (this only affects paths with spaces in) dropText += KShell::quoteArg(url.isLocalFile() ? url.path() : url.url()); // Each filename(including the last) should be followed by one space. dropText += QLatin1Char(' '); } return dropText; } void setupCdToUrlAction(const QString& dropText, const QUrl& url, QList& additionalActions, TerminalDisplay *display) { KIO::StatJob* job = KIO::mostLocalUrl(url, KIO::HideProgressInfo); if (!job->exec()) { return; } const QUrl localUrl = job->mostLocalUrl(); if (!localUrl.isLocalFile()) { return; } const QFileInfo fileInfo(localUrl.path()); if (!fileInfo.isDir()) { return; } QAction* cdAction = new QAction(i18n("Change &Directory To"), display); const QByteArray triggerText = QString(QLatin1String(" cd ") + dropText + QLatin1Char('\n')).toLocal8Bit(); display->connect(cdAction, &QAction::triggered, display, [display, triggerText]{ emit display->sendStringToEmu(triggerText);} ); additionalActions.append(cdAction); } } void TerminalDisplay::dropEvent(QDropEvent* event) { if (_readOnly) { event->accept(); return; } const auto mimeData = event->mimeData(); if (mimeData == nullptr) { return; } auto urls = mimeData->urls(); QString dropText; if (!urls.isEmpty()) { dropText = extractDroppedText(urls); // If our target is local we will open a popup - otherwise the fallback kicks // in and the URLs will simply be pasted as text. if (!_dropUrlsAsText && (_sessionController != nullptr) && _sessionController->url().isLocalFile()) { // A standard popup with Copy, Move and Link as options - // plus an additional Paste option. QAction* pasteAction = new QAction(i18n("&Paste Location"), this); connect(pasteAction, &QAction::triggered, this, [this, dropText]{ emit sendStringToEmu(dropText.toLocal8Bit());} ); QList additionalActions; additionalActions.append(pasteAction); if (urls.count() == 1) { setupCdToUrlAction(dropText, urls.at(0), additionalActions, this); } QUrl target = QUrl::fromLocalFile(_sessionController->currentDir()); KIO::DropJob* job = KIO::drop(event, target); KJobWidgets::setWindow(job, this); job->setApplicationActions(additionalActions); return; } } else { dropText = mimeData->text(); } if (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list"))) { emit sendStringToEmu(dropText.toLocal8Bit()); } } void TerminalDisplay::doDrag() { const QMimeData *clipboardMimeData = QApplication::clipboard()->mimeData(QClipboard::Selection); if (clipboardMimeData == nullptr) { return; } auto mimeData = new QMimeData(); _dragInfo.state = diDragging; _dragInfo.dragObject = new QDrag(this); mimeData->setText(clipboardMimeData->text()); mimeData->setHtml(clipboardMimeData->html()); _dragInfo.dragObject->setMimeData(mimeData); _dragInfo.dragObject->exec(Qt::CopyAction); } void TerminalDisplay::setSessionController(SessionController* controller) { _sessionController = controller; _headerBar->finishHeaderSetup(controller); } SessionController* TerminalDisplay::sessionController() { return _sessionController; } IncrementalSearchBar *TerminalDisplay::searchBar() const { return _searchBar; } AutoScrollHandler::AutoScrollHandler(QWidget* parent) : QObject(parent) , _timerId(0) { parent->installEventFilter(this); } void AutoScrollHandler::timerEvent(QTimerEvent* event) { if (event->timerId() != _timerId) { return; } QMouseEvent mouseEvent(QEvent::MouseMove, widget()->mapFromGlobal(QCursor::pos()), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(widget(), &mouseEvent); } bool AutoScrollHandler::eventFilter(QObject* watched, QEvent* event) { Q_ASSERT(watched == parent()); Q_UNUSED(watched); switch (event->type()) { case QEvent::MouseMove: { auto* mouseEvent = static_cast(event); bool mouseInWidget = widget()->rect().contains(mouseEvent->pos()); if (mouseInWidget) { if (_timerId != 0) { killTimer(_timerId); } _timerId = 0; } else { if ((_timerId == 0) && ((mouseEvent->buttons() & Qt::LeftButton) != 0u)) { _timerId = startTimer(100); } } break; } case QEvent::MouseButtonRelease: { auto* mouseEvent = static_cast(event); if ((_timerId != 0) && ((mouseEvent->buttons() & ~Qt::LeftButton) != 0u)) { killTimer(_timerId); _timerId = 0; } break; } default: break; }; return false; } void TerminalDisplay::applyProfile(const Profile::Ptr &profile) { // load color scheme ColorEntry table[TABLE_COLORS]; _colorScheme = ViewManager::colorSchemeForProfile(profile); _colorScheme->getColorTable(table, randomSeed()); setColorTable(table); setOpacity(_colorScheme->opacity()); setWallpaper(_colorScheme->wallpaper()); // load font _antialiasText = profile->antiAliasFonts(); _boldIntense = profile->boldIntense(); _useFontLineCharacters = profile->useFontLineCharacters(); setVTFont(profile->font()); // set scroll-bar position setScrollBarPosition(Enum::ScrollBarPositionEnum(profile->property(Profile::ScrollBarPosition))); setScrollFullPage(profile->property(Profile::ScrollFullPage)); // show hint about terminal size after resizing _showTerminalSizeHint = profile->showTerminalSizeHint(); _dimWhenInactive = profile->dimWhenInactive(); // terminal features setBlinkingCursorEnabled(profile->blinkingCursorEnabled()); setBlinkingTextEnabled(profile->blinkingTextEnabled()); _tripleClickMode = Enum::TripleClickModeEnum(profile->property(Profile::TripleClickMode)); setAutoCopySelectedText(profile->autoCopySelectedText()); _ctrlRequiredForDrag = profile->property(Profile::CtrlRequiredForDrag); _dropUrlsAsText = profile->property(Profile::DropUrlsAsText); _bidiEnabled = profile->bidiRenderingEnabled(); setLineSpacing(uint(profile->lineSpacing())); _trimLeadingSpaces = profile->property(Profile::TrimLeadingSpacesInSelectedText); _trimTrailingSpaces = profile->property(Profile::TrimTrailingSpacesInSelectedText); _openLinksByDirectClick = profile->property(Profile::OpenLinksByDirectClickEnabled); _urlHintsModifiers = Qt::KeyboardModifiers(profile->property(Profile::UrlHintsModifiers)); _reverseUrlHints = profile->property(Profile::ReverseUrlHints); setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum(profile->property(Profile::MiddleClickPasteMode))); setCopyTextAsHTML(profile->property(Profile::CopyTextAsHTML)); // margin/center setMargin(profile->property(Profile::TerminalMargin)); setCenterContents(profile->property(Profile::TerminalCenter)); // cursor shape setKeyboardCursorShape(Enum::CursorShapeEnum(profile->property(Profile::CursorShape))); // cursor color // an invalid QColor is used to inform the view widget to // draw the cursor using the default color( matching the text) setKeyboardCursorColor(profile->useCustomCursorColor() ? profile->customCursorColor() : QColor()); // word characters setWordCharacters(profile->wordCharacters()); // bell mode setBellMode(profile->property(Profile::BellMode)); // mouse wheel zoom _mouseWheelZoom = profile->mouseWheelZoomEnabled(); setAlternateScrolling(profile->property(Profile::AlternateScrolling)); } diff --git a/src/settings/ProfileSettings.cpp b/src/settings/ProfileSettings.cpp index a07411a1..69f6fa82 100644 --- a/src/settings/ProfileSettings.cpp +++ b/src/settings/ProfileSettings.cpp @@ -1,545 +1,545 @@ /* Copyright 2007-2008 by Robert Knight 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" #include // Qt #include #include #include #include // KDE #include #include #include // 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* aParent) : QWidget(aParent) , _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(), 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(), 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) { 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 { _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& 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 { 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 profiles = ProfileManager::instance()->allProfiles(); ProfileManager::instance()->sortProfiles(profiles); foreach(const Profile::Ptr& profile, 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(); 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 if (!isDefault) { // 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() { foreach(const Profile::Ptr & profile, selectedProfiles()) { 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 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() { QList profiles(selectedProfiles()); foreach (Session* session, SessionManager::instance()->sessions()) { foreach (TerminalDisplay* terminal, session->views()) { // Searching for opened profiles if (terminal->sessionController()->profileDialogPointer() != nullptr) { foreach (const Profile::Ptr & profile, profiles) { if (profile->name() == terminal->sessionController()->profileDialogPointer()->lookupProfile()->name() && terminal->sessionController()->profileDialogPointer()->isVisible()) { // close opened edit dialog terminal->sessionController()->profileDialogPointer()->close(); } } } } } EditProfileDialog dialog(this); // the dialog will delete the profile group when it is destroyed ProfileGroup* group = new ProfileGroup; foreach (const Profile::Ptr & profile, profiles) { group->addProfile(profile); } group->updateValues(); dialog.setProfile(Profile::Ptr(group)); dialog.exec(); } QList ProfileSettings::selectedProfiles() const { QList list; QItemSelectionModel* selection = profilesList->selectionModel(); if (selection == nullptr) { return list; } foreach(const QModelIndex & index, selection->selectedIndexes()) { if (index.column() == ProfileColumn) { list << index.data(ProfilePtrRole).value(); } } 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(); } 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) { 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(&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* aParent) : QStyledItemDelegate(aParent), _modifiedEditors(QSet()), _itemsBeingEdited(QSet()) { } void ShortcutItemDelegate::editorModified() { auto* editor = qobject_cast(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(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.width(shortcutString + QStringLiteral(", ...")) + 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); }