diff --git a/src/EditProfileDialog.cpp b/src/EditProfileDialog.cpp index cfca11e1..5fff7674 100644 --- a/src/EditProfileDialog.cpp +++ b/src/EditProfileDialog.cpp @@ -1,1349 +1,1364 @@ /* 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 "EditProfileDialog.h" // Standard #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include // Konsole #include "ColorSchemeManager.h" #include "ui_EditProfileDialog.h" #include "KeyBindingEditor.h" #include "KeyboardTranslator.h" #include "KeyboardTranslatorManager.h" #include "ProfileManager.h" #include "ShellCommand.h" #include "WindowSystemInfo.h" using namespace Konsole; EditProfileDialog::EditProfileDialog(QWidget* aParent) : QDialog(aParent) , _delayedPreviewTimer(new QTimer(this)) , _colorDialog(0) { setWindowTitle(i18n("Edit Profile")); mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::Apply); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QPushButton *okButton = mButtonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(mButtonBox, &QDialogButtonBox::accepted, this, &Konsole::EditProfileDialog::accept); connect(mButtonBox, &QDialogButtonBox::rejected, this, &Konsole::EditProfileDialog::reject); // disable the apply button , since no modification has been made mButtonBox->button(QDialogButtonBox::Apply)->setEnabled(false); connect(mButtonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &Konsole::EditProfileDialog::save); connect(_delayedPreviewTimer, &QTimer::timeout, this, &Konsole::EditProfileDialog::delayedPreviewActivate); _ui = new Ui::EditProfileDialog(); _ui->setupUi(mainWidget); mainLayout->addWidget(mButtonBox); // 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 _pageNeedsUpdate.resize(_ui->tabWidget->count()); connect(_ui->tabWidget, &QTabWidget::currentChanged, this, &Konsole::EditProfileDialog::preparePage); createTempProfile(); } EditProfileDialog::~EditProfileDialog() { delete _ui; } 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(); mButtonBox->button(QDialogButtonBox::Apply)->setEnabled(false); } void EditProfileDialog::reject() { unpreviewAll(); QDialog::reject(); } void EditProfileDialog::accept() { Q_ASSERT(_profile); Q_ASSERT(_tempProfile); 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.

")); return; } save(); unpreviewAll(); QDialog::accept(); } 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 += ','; // 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(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 _pageNeedsUpdate.fill(true); preparePage(_ui->tabWidget->currentIndex()); if (_tempProfile) { createTempProfile(); } } const Profile::Ptr EditProfileDialog::lookupProfile() const { return _profile; } void EditProfileDialog::preparePage(int page) { const Profile::Ptr profile = lookupProfile(); Q_ASSERT(_pageNeedsUpdate.count() > page); Q_ASSERT(profile); QWidget* pageWidget = _ui->tabWidget->widget(page); if (_pageNeedsUpdate[page]) { if (pageWidget == _ui->generalTab) setupGeneralPage(profile); else if (pageWidget == _ui->tabsTab) setupTabsPage(profile); else if (pageWidget == _ui->appearanceTab) setupAppearancePage(profile); else if (pageWidget == _ui->scrollingTab) setupScrollingPage(profile); else if (pageWidget == _ui->keyboardTab) setupKeyboardPage(profile); else if (pageWidget == _ui->mouseTab) setupMousePage(profile); else if (pageWidget == _ui->advancedTab) setupAdvancedPage(profile); else Q_ASSERT(false); _pageNeedsUpdate[page] = false; } } void EditProfileDialog::selectProfileName() { _ui->profileNameEdit->setFocus(); _ui->profileNameEdit->selectAll(); } void EditProfileDialog::setupGeneralPage(const Profile::Ptr profile) { // basic profile options { _ui->emptyNameWarningWidget->setWordWrap(false); _ui->emptyNameWarningWidget->setCloseButtonVisible(false); _ui->emptyNameWarningWidget->setMessageType(KMessageWidget::Warning); ProfileGroup::Ptr group = profile->asGroup(); if (!group || group->profiles().count() < 2) { _ui->profileNameEdit->setText(profile->name()); _ui->profileNameEdit->setClearButtonEnabled(true); _ui->emptyNameWarningWidget->setVisible(profile->name().isEmpty()); _ui->emptyNameWarningWidget->setText(i18n("Profile name is empty.")); } else { _ui->profileNameEdit->setText(groupProfileNames(group, -1)); _ui->profileNameEdit->setEnabled(false); _ui->profileNameLabel->setEnabled(false); _ui->emptyNameWarningWidget->setVisible(false); } } ShellCommand command(profile->command() , profile->arguments()); _ui->commandEdit->setText(command.fullCommand()); // If a "completion" is requested, consider changing this to KLineEdit // and using KCompletion. _ui->initialDirEdit->setText(profile->defaultWorkingDirectory()); _ui->initialDirEdit->setClearButtonEnabled(true); _ui->dirSelectButton->setIcon(QIcon::fromTheme(QStringLiteral("folder-open"))); _ui->iconSelectButton->setIcon(QIcon::fromTheme(profile->icon())); _ui->startInSameDirButton->setChecked(profile->startInCurrentSessionDir()); // terminal options _ui->terminalColumnsEntry->setValue(profile->terminalColumns()); _ui->terminalRowsEntry->setValue(profile->terminalRows()); // window options _ui->showTerminalSizeHintButton->setChecked(profile->showTerminalSizeHint()); // signals and slots connect(_ui->dirSelectButton, &QToolButton::clicked, this, &Konsole::EditProfileDialog::selectInitialDir); connect(_ui->iconSelectButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::selectIcon); connect(_ui->startInSameDirButton, &QCheckBox::toggled, this , &Konsole::EditProfileDialog::startInSameDir); connect(_ui->profileNameEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::profileNameChanged); connect(_ui->initialDirEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::initialDirChanged); connect(_ui->commandEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::commandChanged); connect(_ui->environmentEditButton , &QPushButton::clicked, this, &Konsole::EditProfileDialog::showEnvironmentEditor); connect(_ui->terminalColumnsEntry, static_cast(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::terminalColumnsEntryChanged); connect(_ui->terminalRowsEntry, static_cast(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::terminalRowsEntryChanged); connect(_ui->showTerminalSizeHintButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::showTerminalSizeHint); } void EditProfileDialog::showEnvironmentEditor() { bool ok; const Profile::Ptr profile = lookupProfile(); QStringList currentEnvironment = profile->environment(); QString text = QInputDialog::getMultiLineText(this, i18n("Edit Environment"), i18n("One environment variable per line"), currentEnvironment.join(QStringLiteral("\n")), &ok); if (ok && !text.isEmpty()) { QStringList newEnvironment = text.split('\n'); updateTempProfileProperty(Profile::Environment, newEnvironment); } } void EditProfileDialog::setupTabsPage(const Profile::Ptr profile) { // tab title format _ui->renameTabWidget->setTabTitleText(profile->localTabTitleFormat()); _ui->renameTabWidget->setRemoteTabTitleText(profile->remoteTabTitleFormat()); connect(_ui->renameTabWidget, &Konsole::RenameTabWidget::tabTitleFormatChanged, this, &Konsole::EditProfileDialog::tabTitleFormatChanged); connect(_ui->renameTabWidget, &Konsole::RenameTabWidget::remoteTabTitleFormatChanged, this, &Konsole::EditProfileDialog::remoteTabTitleFormatChanged); // tab monitoring const int silenceSeconds = profile->silenceSeconds(); _ui->silenceSecondsSpinner->setValue(silenceSeconds); _ui->silenceSecondsSpinner->setSuffix(ki18ncp("Unit of time", " second", " seconds")); connect(_ui->silenceSecondsSpinner, static_cast(&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::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()) { _ui->iconSelectButton->setIcon(QIcon::fromTheme(icon)); updateTempProfileProperty(Profile::Icon, icon); } } void EditProfileDialog::profileNameChanged(const QString& text) { _ui->emptyNameWarningWidget->setVisible(text.isEmpty()); updateTempProfileProperty(Profile::Name, text); updateTempProfileProperty(Profile::UntranslatedName, text); 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(_ui->initialDirEdit->text())); if (!url.isEmpty()) _ui->initialDirEdit->setText(url.path()); } void EditProfileDialog::setupAppearancePage(const Profile::Ptr profile) { ColorSchemeViewDelegate* delegate = new ColorSchemeViewDelegate(this); _ui->colorSchemeList->setItemDelegate(delegate); _ui->transparencyWarningWidget->setVisible(false); _ui->transparencyWarningWidget->setWordWrap(true); _ui->transparencyWarningWidget->setCloseButtonVisible(false); _ui->transparencyWarningWidget->setMessageType(KMessageWidget::Warning); _ui->editColorSchemeButton->setEnabled(false); _ui->removeColorSchemeButton->setEnabled(false); // setup color list updateColorSchemeList(true); _ui->colorSchemeList->setMouseTracking(true); _ui->colorSchemeList->installEventFilter(this); _ui->colorSchemeList->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(_ui->colorSchemeList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Konsole::EditProfileDialog::colorSchemeSelected); connect(_ui->colorSchemeList, &QListView::entered, this, &Konsole::EditProfileDialog::previewColorScheme); updateColorSchemeButtons(); connect(_ui->editColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::editColorScheme); connect(_ui->removeColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::removeColorScheme); connect(_ui->newColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::newColorScheme); // setup font preview const bool antialias = profile->antiAliasFonts(); QFont profileFont = profile->font(); profileFont.setStyleStrategy(antialias ? QFont::PreferAntialias : QFont::NoAntialias); _ui->fontPreviewLabel->installEventFilter(this); _ui->fontPreviewLabel->setFont(profileFont); setFontInputValue(profileFont); // Always set to unchecked _ui->showAllFontsButton->setChecked(false); connect(_ui->showAllFontsButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::showAllFontsButtonWarning); connect(_ui->fontSizeInput, static_cast(&QDoubleSpinBox::valueChanged), this, &Konsole::EditProfileDialog::setFontSize); connect(_ui->selectFontButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::showFontDialog); // setup font smoothing _ui->antialiasTextButton->setChecked(antialias); connect(_ui->antialiasTextButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::setAntialiasText); _ui->boldIntenseButton->setChecked(profile->boldIntense()); connect(_ui->boldIntenseButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::setBoldIntense); _ui->useFontLineCharactersButton->setChecked(profile->useFontLineCharacters()); connect(_ui->useFontLineCharactersButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::useFontLineCharacters); _ui->enableMouseWheelZoomButton->setChecked(profile->mouseWheelZoomEnabled()); connect(_ui->enableMouseWheelZoomButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::toggleMouseWheelZoom); } void EditProfileDialog::showAllFontsButtonWarning(bool enable) { if (enable) { KMessageBox::information(this, "By its very nature, a terminal program requires font characters that are equal width (monospace). Any non monospaced font may cause display issues. This should not be necessary except in rare cases.", "Warning"); } } void EditProfileDialog::setAntialiasText(bool enable) { QFont profileFont = _ui->fontPreviewLabel->font(); profileFont.setStyleStrategy(enable ? QFont::PreferAntialias : QFont::NoAntialias); // update preview to reflect text smoothing state fontSelected(profileFont); updateTempProfileProperty(Profile::AntiAliasFonts, enable); } 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::toggleMouseWheelZoom(bool enable) { updateTempProfileProperty(Profile::MouseWheelZoomEnabled, enable); } void EditProfileDialog::updateColorSchemeList(bool selectCurrentScheme) { if (!_ui->colorSchemeList->model()) _ui->colorSchemeList->setModel(new QStandardItemModel(this)); const QString& name = lookupProfile()->colorScheme(); const ColorScheme* currentScheme = ColorSchemeManager::instance()->findColorScheme(name); QStandardItemModel* model = qobject_cast(_ui->colorSchemeList->model()); Q_ASSERT(model); model->clear(); QStandardItem* selectedItem = 0; 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 (currentScheme == scheme) selectedItem = item; model->appendRow(item); } model->sort(0); if (selectCurrentScheme && selectedItem) { _ui->colorSchemeList->updateGeometry(); _ui->colorSchemeList->selectionModel()->setCurrentIndex(selectedItem->index() , QItemSelectionModel::Select); // update transparency warning label updateTransparencyWarning(); } } void EditProfileDialog::updateKeyBindingsList(bool selectCurrentTranslator) { if (!_ui->keyBindingList->model()) _ui->keyBindingList->setModel(new QStandardItemModel(this)); const QString& name = lookupProfile()->keyBindings(); KeyboardTranslatorManager* keyManager = KeyboardTranslatorManager::instance(); const KeyboardTranslator* currentTranslator = keyManager->findTranslator(name); QStandardItemModel* model = qobject_cast(_ui->keyBindingList->model()); Q_ASSERT(model); model->clear(); QStandardItem* selectedItem = 0; QStringList translatorNames = keyManager->allTranslators(); foreach(const QString& translatorName, translatorNames) { const KeyboardTranslator* translator = keyManager->findTranslator(translatorName); if (!translator) continue; QStandardItem* item = new QStandardItem(translator->description()); item->setEditable(false); item->setData(QVariant::fromValue(translator), Qt::UserRole + 1); item->setData(QVariant::fromValue(_profile->font()), Qt::UserRole + 2); item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-keyboard"))); if (translator == currentTranslator) selectedItem = item; model->appendRow(item); } model->sort(0); if (selectCurrentTranslator && selectedItem) { _ui->keyBindingList->selectionModel()->setCurrentIndex(selectedItem->index() , QItemSelectionModel::Select); } } bool EditProfileDialog::eventFilter(QObject* watched , QEvent* aEvent) { if (watched == _ui->colorSchemeList && aEvent->type() == QEvent::Leave) { if (_tempProfile->isPropertySet(Profile::ColorScheme)) preview(Profile::ColorScheme, _tempProfile->colorScheme()); else unpreview(Profile::ColorScheme); } if (watched == _ui->fontPreviewLabel && aEvent->type() == QEvent::FontChange) { const QFont& labelFont = _ui->fontPreviewLabel->font(); _ui->fontPreviewLabel->setText(i18n("%1", labelFont.family())); } return QDialog::eventFilter(watched, aEvent); } void EditProfileDialog::unpreviewAll() { _delayedPreviewTimer->stop(); _delayedPreviewProperties.clear(); QHash map; QHashIterator iter(_previewedProperties); while (iter.hasNext()) { iter.next(); map.insert((Profile::Property)iter.key(), iter.value()); } // undo any preview changes if (!map.isEmpty()) ProfileManager::instance()->changeProfile(_profile, map, false); } void EditProfileDialog::unpreview(int aProperty) { _delayedPreviewProperties.remove(aProperty); if (!_previewedProperties.contains(aProperty)) return; QHash map; map.insert((Profile::Property)aProperty, _previewedProperties[aProperty]); ProfileManager::instance()->changeProfile(_profile, map, false); _previewedProperties.remove(aProperty); } void EditProfileDialog::delayedPreview(int aProperty , const QVariant& value) { _delayedPreviewProperties.insert(aProperty, 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 aProperty , const QVariant& value) { QHash map; map.insert((Profile::Property)aProperty, value); _delayedPreviewProperties.remove(aProperty); 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((Profile::Property)aProperty).isNull()) return; if (!_previewedProperties.contains(aProperty)) { _previewedProperties.insert(aProperty , original->property((Profile::Property)aProperty)); } // 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::removeColorScheme() { QModelIndexList selected = _ui->colorSchemeList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { const QString& name = selected.first().data(Qt::UserRole + 1).value()->name(); if (ColorSchemeManager::instance()->deleteColorScheme(name)) _ui->colorSchemeList->model()->removeRow(selected.first().row()); } } void EditProfileDialog::showColorSchemeEditor(bool isNewScheme) { // Finding selected ColorScheme QModelIndexList selected = _ui->colorSchemeList->selectionModel()->selectedIndexes(); QAbstractItemModel* model = _ui->colorSchemeList->model(); const ColorScheme* colors = 0; 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) { 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) { _colorDialog->close(); delete _colorDialog; } } void EditProfileDialog::newColorScheme() { showColorSchemeEditor(true); } void EditProfileDialog::editColorScheme() { showColorSchemeEditor(false); } void EditProfileDialog::saveColorScheme(const ColorScheme& scheme, bool isNewScheme) { ColorScheme* 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); updateColorSchemeList(true); preview(Profile::ColorScheme, newScheme->name()); } void EditProfileDialog::colorSchemeSelected() { QModelIndexList selected = _ui->colorSchemeList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { QAbstractItemModel* model = _ui->colorSchemeList->model(); const ColorScheme* colors = model->data(selected.first(), Qt::UserRole + 1).value(); if (colors) { updateTempProfileProperty(Profile::ColorScheme, colors->name()); previewColorScheme(selected.first()); updateTransparencyWarning(); } } updateColorSchemeButtons(); } void EditProfileDialog::updateColorSchemeButtons() { enableIfNonEmptySelection(_ui->editColorSchemeButton, _ui->colorSchemeList->selectionModel()); enableIfNonEmptySelection(_ui->removeColorSchemeButton, _ui->colorSchemeList->selectionModel()); } void EditProfileDialog::updateKeyBindingsButtons() { enableIfNonEmptySelection(_ui->editKeyBindingsButton, _ui->keyBindingList->selectionModel()); enableIfNonEmptySelection(_ui->removeKeyBindingsButton, _ui->keyBindingList->selectionModel()); } 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 , _ui->colorSchemeList->selectionModel()->selectedIndexes()) { bool needTransparency = index.data(Qt::UserRole + 1).value()->opacity() < 1.0; if (!needTransparency) { _ui->transparencyWarningWidget->setHidden(true); } else if (!KWindowSystem::compositingActive()) { _ui->transparencyWarningWidget->setText(i18n("This color scheme uses a transparent background" " which does not appear to be supported on your" " desktop")); _ui->transparencyWarningWidget->setHidden(false); } else if (!WindowSystemInfo::HAVE_TRANSPARENCY) { _ui->transparencyWarningWidget->setText(i18n("Konsole was started before desktop effects were enabled." " You need to restart Konsole to see transparent background.")); _ui->transparencyWarningWidget->setHidden(false); } } } void EditProfileDialog::createTempProfile() { _tempProfile = Profile::Ptr(new Profile); _tempProfile->setHidden(true); } void EditProfileDialog::updateTempProfileProperty(Profile::Property aProperty, const QVariant & value) { _tempProfile->setProperty(aProperty, value); updateButtonApply(); } void EditProfileDialog::updateButtonApply() { bool userModified = false; QHashIterator iter(_tempProfile->setProperties()); while (iter.hasNext()) { iter.next(); Profile::Property aProperty = iter.key(); QVariant value = iter.value(); // for previewed property if (_previewedProperties.contains(static_cast(aProperty))) { if (value != _previewedProperties.value(static_cast(aProperty))) { userModified = true; break; } // for not-previewed property } else if ((value != _profile->property(aProperty))) { userModified = true; break; } } mButtonBox->button(QDialogButtonBox::Apply)->setEnabled(userModified); } void EditProfileDialog::setupKeyboardPage(const Profile::Ptr /* profile */) { // setup translator list updateKeyBindingsList(true); connect(_ui->keyBindingList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Konsole::EditProfileDialog::keyBindingSelected); connect(_ui->newKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::newKeyBinding); updateKeyBindingsButtons(); connect(_ui->editKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::editKeyBinding); connect(_ui->removeKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::removeKeyBinding); } void EditProfileDialog::keyBindingSelected() { QModelIndexList selected = _ui->keyBindingList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { QAbstractItemModel* model = _ui->keyBindingList->model(); const KeyboardTranslator* translator = model->data(selected.first(), Qt::UserRole + 1) .value(); if (translator) { updateTempProfileProperty(Profile::KeyBindings, translator->name()); } } updateKeyBindingsButtons(); } void EditProfileDialog::removeKeyBinding() { QModelIndexList selected = _ui->keyBindingList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { const QString& name = selected.first().data(Qt::UserRole + 1).value()->name(); if (KeyboardTranslatorManager::instance()->deleteTranslator(name)) _ui->keyBindingList->model()->removeRow(selected.first().row()); } } + void EditProfileDialog::showKeyBindingEditor(bool isNewTranslator) { QModelIndexList selected = _ui->keyBindingList->selectionModel()->selectedIndexes(); QAbstractItemModel* model = _ui->keyBindingList->model(); const KeyboardTranslator* translator = 0; if (!selected.isEmpty()) translator = model->data(selected.first(), Qt::UserRole + 1).value(); else translator = KeyboardTranslatorManager::instance()->defaultTranslator(); Q_ASSERT(translator); QPointer dialog = new QDialog(this); QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); if (isNewTranslator) dialog->setWindowTitle(i18n("New Key Binding List")); else dialog->setWindowTitle(i18n("Edit Key Binding List")); KeyBindingEditor* editor = new KeyBindingEditor; if (translator) editor->setup(translator); if (isNewTranslator) editor->setDescription(i18n("New Key Binding List")); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(editor); layout->addWidget(buttonBox); dialog->setLayout(layout); if (dialog->exec() == QDialog::Accepted) { KeyboardTranslator* newTranslator = new KeyboardTranslator(*editor->translator()); if (isNewTranslator) newTranslator->setName(newTranslator->description()); KeyboardTranslatorManager::instance()->addTranslator(newTranslator); updateKeyBindingsList(); const QString& currentTranslator = lookupProfile() ->property(Profile::KeyBindings); if (newTranslator->name() == currentTranslator) { updateTempProfileProperty(Profile::KeyBindings, newTranslator->name()); } } delete dialog; } void EditProfileDialog::newKeyBinding() { showKeyBindingEditor(true); } void EditProfileDialog::editKeyBinding() { showKeyBindingEditor(false); } void EditProfileDialog::setupCheckBoxes(BooleanOption* options , const Profile::Ptr profile) { while (options->button != 0) { options->button->setChecked(profile->property(options->property)); connect(options->button, SIGNAL(toggled(bool)), this, options->slot); ++options; } } void EditProfileDialog::setupRadio(RadioOption* possibilities , int actual) { while (possibilities->button != 0) { if (possibilities->value == actual) possibilities->button->setChecked(true); else possibilities->button->setChecked(false); connect(possibilities->button, SIGNAL(clicked()), this, possibilities->slot); ++possibilities; } } void EditProfileDialog::setupScrollingPage(const Profile::Ptr profile) { // setup scrollbar radio int scrollBarPosition = profile->property(Profile::ScrollBarPosition); RadioOption positions[] = { {_ui->scrollBarHiddenButton, Enum::ScrollBarHidden, SLOT(hideScrollBar())}, {_ui->scrollBarLeftButton, Enum::ScrollBarLeft, SLOT(showScrollBarLeft())}, {_ui->scrollBarRightButton, Enum::ScrollBarRight, SLOT(showScrollBarRight())}, {0, 0, 0} }; setupRadio(positions , scrollBarPosition); // setup scrollback type radio int scrollBackType = profile->property(Profile::HistoryMode); _ui->historySizeWidget->setMode(Enum::HistoryModeEnum(scrollBackType)); connect(_ui->historySizeWidget, &Konsole::HistorySizeWidget::historyModeChanged, this, &Konsole::EditProfileDialog::historyModeChanged); // setup scrollback line count spinner const int historySize = profile->historySize(); _ui->historySizeWidget->setLineCount(historySize); // setup scrollpageamount type radio int scrollFullPage = profile->property(Profile::ScrollFullPage); RadioOption pageamounts[] = { {_ui->scrollHalfPage, Enum::ScrollPageHalf, SLOT(scrollHalfPage())}, {_ui->scrollFullPage, Enum::ScrollPageFull, SLOT(scrollFullPage())}, {0, 0, 0} }; setupRadio(pageamounts, scrollFullPage); // signals and slots connect(_ui->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::hideScrollBar() { updateTempProfileProperty(Profile::ScrollBarPosition, Enum::ScrollBarHidden); } void EditProfileDialog::showScrollBarLeft() { updateTempProfileProperty(Profile::ScrollBarPosition, Enum::ScrollBarLeft); } void EditProfileDialog::showScrollBarRight() { updateTempProfileProperty(Profile::ScrollBarPosition, Enum::ScrollBarRight); } void EditProfileDialog::scrollFullPage() { updateTempProfileProperty(Profile::ScrollFullPage, Enum::ScrollPageFull); } void EditProfileDialog::scrollHalfPage() { updateTempProfileProperty(Profile::ScrollFullPage, Enum::ScrollPageHalf); } void EditProfileDialog::setupMousePage(const Profile::Ptr profile) { - BooleanOption options[] = { { + BooleanOption options[] = { + { _ui->underlineLinksButton , Profile::UnderlineLinksEnabled, SLOT(toggleUnderlineLinks(bool)) }, + { + _ui->underlineFilesButton , Profile::UnderlineFilesEnabled, + SLOT(toggleUnderlineFiles(bool)) + }, { _ui->ctrlRequiredForDragButton, Profile::CtrlRequiredForDrag, SLOT(toggleCtrlRequiredForDrag(bool)) }, { _ui->copyTextToClipboardButton , Profile::AutoCopySelectedText, SLOT(toggleCopyTextToClipboard(bool)) }, { _ui->trimTrailingSpacesButton , Profile::TrimTrailingSpacesInSelectedText, SLOT(toggleTrimTrailingSpacesInSelectedText(bool)) }, { _ui->openLinksByDirectClickButton , Profile::OpenLinksByDirectClickEnabled, SLOT(toggleOpenLinksByDirectClick(bool)) }, { _ui->dropUrlsAsText , Profile::DropUrlsAsText, SLOT(toggleDropUrlsAsText(bool)) }, { 0 , Profile::Property(0) , 0 } }; setupCheckBoxes(options , profile); // setup middle click paste mode const int middleClickPasteMode = profile->property(Profile::MiddleClickPasteMode); RadioOption pasteModes[] = { {_ui->pasteFromX11SelectionButton, Enum::PasteFromX11Selection, SLOT(pasteFromX11Selection())}, {_ui->pasteFromClipboardButton, Enum::PasteFromClipboard, SLOT(pasteFromClipboard())}, {0, 0, 0} }; setupRadio(pasteModes , middleClickPasteMode); // interaction options _ui->wordCharacterEdit->setText(profile->wordCharacters()); connect(_ui->wordCharacterEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::wordCharactersChanged); int tripleClickMode = profile->property(Profile::TripleClickMode); _ui->tripleClickModeCombo->setCurrentIndex(tripleClickMode); connect(_ui->tripleClickModeCombo, static_cast(&KComboBox::activated), this, &Konsole::EditProfileDialog::TripleClickModeChanged); - _ui->openLinksByDirectClickButton->setEnabled(_ui->underlineLinksButton->isChecked()); + _ui->openLinksByDirectClickButton->setEnabled(_ui->underlineLinksButton->isChecked() || _ui->underlineFilesButton->isChecked()); _ui->enableMouseWheelZoomButton->setChecked(profile->mouseWheelZoomEnabled()); connect(_ui->enableMouseWheelZoomButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::toggleMouseWheelZoom); } void EditProfileDialog::setupAdvancedPage(const Profile::Ptr profile) { BooleanOption options[] = { { _ui->enableBlinkingTextButton , Profile::BlinkingTextEnabled , SLOT(toggleBlinkingText(bool)) }, { _ui->enableFlowControlButton , Profile::FlowControlEnabled , SLOT(toggleFlowControl(bool)) }, { _ui->enableBlinkingCursorButton , Profile::BlinkingCursorEnabled , SLOT(toggleBlinkingCursor(bool)) }, { _ui->enableBidiRenderingButton , Profile::BidiRenderingEnabled , SLOT(togglebidiRendering(bool)) }, { 0 , Profile::Property(0) , 0 } }; setupCheckBoxes(options , profile); // Setup the URL hints modifier checkboxes { int modifiers = profile->property(Profile::UrlHintsModifiers); _ui->urlHintsModifierShift->setChecked(modifiers & Qt::ShiftModifier); _ui->urlHintsModifierCtrl->setChecked(modifiers & Qt::ControlModifier); _ui->urlHintsModifierAlt->setChecked(modifiers & Qt::AltModifier); _ui->urlHintsModifierMeta->setChecked(modifiers & Qt::MetaModifier); connect(_ui->urlHintsModifierShift, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); connect(_ui->urlHintsModifierCtrl, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); connect(_ui->urlHintsModifierAlt, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); connect(_ui->urlHintsModifierMeta, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); } const int lineSpacing = profile->lineSpacing(); _ui->lineSpacingSpinner->setValue(lineSpacing); connect(_ui->lineSpacingSpinner, static_cast(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::lineSpacingChanged); // cursor options if (profile->useCustomCursorColor()) _ui->customCursorColorButton->setChecked(true); else _ui->autoCursorColorButton->setChecked(true); _ui->customColorSelectButton->setColor(profile->customCursorColor()); connect(_ui->customCursorColorButton, &QRadioButton::clicked, this, &Konsole::EditProfileDialog::customCursorColor); connect(_ui->autoCursorColorButton, &QRadioButton::clicked, this, &Konsole::EditProfileDialog::autoCursorColor); connect(_ui->customColorSelectButton, &KColorButton::changed, this, &Konsole::EditProfileDialog::customCursorColorChanged); int shape = profile->property(Profile::CursorShape); _ui->cursorShapeCombo->setCurrentIndex(shape); connect(_ui->cursorShapeCombo, static_cast(&KComboBox::activated), this, &Konsole::EditProfileDialog::setCursorShape); // encoding options KCodecAction* codecAction = new KCodecAction(this); _ui->selectEncodingButton->setMenu(codecAction->menu()); connect(codecAction, static_cast(&KCodecAction::triggered), this, &Konsole::EditProfileDialog::setDefaultCodec); _ui->characterEncodingLabel->setText(profile->defaultEncoding()); } void EditProfileDialog::setDefaultCodec(QTextCodec* codec) { QString name = QString(codec->name()); updateTempProfileProperty(Profile::DefaultEncoding, name); _ui->characterEncodingLabel->setText(codec->name()); } void EditProfileDialog::customCursorColorChanged(const QColor& color) { updateTempProfileProperty(Profile::CustomCursorColor, color); // ensure that custom cursor colors are enabled _ui->customCursorColorButton->click(); } void EditProfileDialog::wordCharactersChanged(const QString& text) { updateTempProfileProperty(Profile::WordCharacters, text); } void EditProfileDialog::autoCursorColor() { updateTempProfileProperty(Profile::UseCustomCursorColor, false); } void EditProfileDialog::customCursorColor() { updateTempProfileProperty(Profile::UseCustomCursorColor, true); } void EditProfileDialog::setCursorShape(int index) { updateTempProfileProperty(Profile::CursorShape, index); } void EditProfileDialog::togglebidiRendering(bool enable) { updateTempProfileProperty(Profile::BidiRenderingEnabled, enable); } void EditProfileDialog::lineSpacingChanged(int spacing) { updateTempProfileProperty(Profile::LineSpacing, spacing); } void EditProfileDialog::toggleBlinkingCursor(bool enable) { updateTempProfileProperty(Profile::BlinkingCursorEnabled, enable); } void EditProfileDialog::toggleUnderlineLinks(bool enable) { updateTempProfileProperty(Profile::UnderlineLinksEnabled, enable); - _ui->openLinksByDirectClickButton->setEnabled(enable); + + bool enableClick = _ui->underlineFilesButton->isChecked() || enable; + _ui->openLinksByDirectClickButton->setEnabled(enableClick); +} +void EditProfileDialog::toggleUnderlineFiles(bool enable) +{ + updateTempProfileProperty(Profile::UnderlineFilesEnabled, enable); + + bool enableClick = _ui->underlineLinksButton->isChecked() || enable; + _ui->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::toggleCopyTextToClipboard(bool enable) { updateTempProfileProperty(Profile::AutoCopySelectedText, 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 (_ui->urlHintsModifierShift->isChecked()) modifiers |= Qt::ShiftModifier; if (_ui->urlHintsModifierCtrl->isChecked()) modifiers |= Qt::ControlModifier; if (_ui->urlHintsModifierAlt->isChecked()) modifiers |= Qt::AltModifier; if (_ui->urlHintsModifierMeta->isChecked()) modifiers |= Qt::MetaModifier; updateTempProfileProperty(Profile::UrlHintsModifiers, int(modifiers)); } void EditProfileDialog::toggleBlinkingText(bool enable) { updateTempProfileProperty(Profile::BlinkingTextEnabled, enable); } void EditProfileDialog::toggleFlowControl(bool enable) { updateTempProfileProperty(Profile::FlowControlEnabled, enable); } void EditProfileDialog::fontSelected(const QFont& aFont) { QFont previewFont = aFont; setFontInputValue(aFont); _ui->fontPreviewLabel->setFont(previewFont); preview(Profile::Font, aFont); updateTempProfileProperty(Profile::Font, aFont); } void EditProfileDialog::showFontDialog() { QFont currentFont = _ui->fontPreviewLabel->font(); bool showAllFonts = _ui->showAllFontsButton->isChecked(); bool result; if (showAllFonts) { currentFont = QFontDialog::getFont(&result, currentFont, this, i18n("Select All Font")); } else { currentFont = QFontDialog::getFont(&result, currentFont, this, i18n("Select Fixed Width Font"), QFontDialog::MonospacedFonts); } if (!result) return; fontSelected(currentFont); } void EditProfileDialog::setFontSize(double pointSize) { QFont newFont = _ui->fontPreviewLabel->font(); newFont.setPointSizeF(pointSize); _ui->fontPreviewLabel->setFont(newFont); preview(Profile::Font, newFont); updateTempProfileProperty(Profile::Font, newFont); } void EditProfileDialog::setFontInputValue(const QFont& aFont) { _ui->fontSizeInput->setValue(aFont.pointSizeF()); } ColorSchemeViewDelegate::ColorSchemeViewDelegate(QObject* aParent) : QAbstractItemDelegate(aParent) { } void ColorSchemeViewDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { const ColorScheme* scheme = index.data(Qt::UserRole + 1).value(); QFont profileFont = index.data(Qt::UserRole + 2).value(); Q_ASSERT(scheme); if (!scheme) return; painter->setRenderHint(QPainter::Antialiasing); // Draw background QStyle *style = option.widget ? option.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, option.widget); // Draw name QPalette::ColorRole textColor = (option.state & QStyle::State_Selected) ? 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); 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; qreal colorWidth = (qreal)width / TABLE_COLORS; int margin = 5; qreal heightForWidth = (colorWidth * 2) + option.fontMetrics.height() + margin; // temporary return QSize(width, static_cast(heightForWidth)); } diff --git a/src/EditProfileDialog.h b/src/EditProfileDialog.h index bb2896f5..37713ff6 100644 --- a/src/EditProfileDialog.h +++ b/src/EditProfileDialog.h @@ -1,280 +1,281 @@ /* 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. */ #ifndef EDITPROFILEDIALOG_H #define EDITPROFILEDIALOG_H // Qt #include #include #include // KDE #include // Konsole #include "Profile.h" #include "Enumeration.h" #include "ColorScheme.h" #include "ColorSchemeEditor.h" class QAbstractButton; class QItemSelectionModel; class QTextCodec; class QDialogButtonBox; namespace Ui { class EditProfileDialog; } namespace Konsole { /** * A dialog which allows the user to edit a profile. * After the dialog is created, it can be initialized with the settings * for a profile using setProfile(). When the user makes changes to the * dialog and accepts the changes, the dialog will update the * profile in the SessionManager by calling the SessionManager's * changeProfile() method. * * Some changes made in the dialog are preview-only changes which cause * the SessionManager's changeProfile() method to be called with * the persistent argument set to false. These changes are then * un-done when the dialog is closed. */ class KONSOLEPRIVATE_EXPORT EditProfileDialog : public QDialog { Q_OBJECT public: /** Constructs a new dialog with the specified parent. */ explicit EditProfileDialog(QWidget* parent = 0); virtual ~EditProfileDialog(); /** * Initializes the dialog with the settings for the specified session * type. * * When the dialog closes, the profile will be updated in the SessionManager * with the altered settings. * * @param profile The profile to be edited */ void setProfile(Profile::Ptr profile); /** * Selects the text in the profile name edit area. * When the dialog is being used to create a new profile, * this can be used to draw the user's attention to the profile name * and make it easy for them to change it. */ void selectProfileName(); const Profile::Ptr lookupProfile() const; public slots: // reimplemented virtual void accept(); // reimplemented virtual void reject(); protected: virtual bool eventFilter(QObject* watched , QEvent* event); private slots: // sets up the specified tab page if necessary void preparePage(int); // saves changes to profile void save(); // general page void selectInitialDir(); void selectIcon(); void profileNameChanged(const QString& text); void initialDirChanged(const QString& text); void startInSameDir(bool); void commandChanged(const QString& text); void tabTitleFormatChanged(const QString& text); void remoteTabTitleFormatChanged(const QString& text); void terminalColumnsEntryChanged(int); void terminalRowsEntryChanged(int); void showTerminalSizeHint(bool); void showEnvironmentEditor(); void silenceSecondsChanged(int); // appearance page void setFontSize(double pointSize); void setFontInputValue(const QFont&); void showAllFontsButtonWarning(bool enable); void setAntialiasText(bool enable); void setBoldIntense(bool enable); void useFontLineCharacters(bool enable); void showFontDialog(); void newColorScheme(); void editColorScheme(); void saveColorScheme(const ColorScheme& scheme, bool isNewScheme); void removeColorScheme(); void colorSchemeSelected(); void previewColorScheme(const QModelIndex& index); void fontSelected(const QFont&); void toggleMouseWheelZoom(bool enable); // scrolling page void historyModeChanged(Enum::HistoryModeEnum mode); void historySizeChanged(int); void hideScrollBar(); void showScrollBarLeft(); void showScrollBarRight(); void scrollFullPage(); void scrollHalfPage(); // keyboard page void editKeyBinding(); void newKeyBinding(); void keyBindingSelected(); void removeKeyBinding(); // mouse page + void toggleUnderlineFiles(bool enable); void toggleUnderlineLinks(bool); void toggleOpenLinksByDirectClick(bool); void toggleCtrlRequiredForDrag(bool); void toggleDropUrlsAsText(bool); void toggleCopyTextToClipboard(bool); void toggleTrimTrailingSpacesInSelectedText(bool); void pasteFromX11Selection(); void pasteFromClipboard(); void TripleClickModeChanged(int); void wordCharactersChanged(const QString&); // advanced page void toggleBlinkingText(bool); void toggleFlowControl(bool); void togglebidiRendering(bool); void lineSpacingChanged(int); void toggleBlinkingCursor(bool); void updateUrlHintsModifier(bool); void setCursorShape(int); void autoCursorColor(); void customCursorColor(); void customCursorColorChanged(const QColor&); void setDefaultCodec(QTextCodec*); // apply the first previewed changes stored up by delayedPreview() void delayedPreviewActivate(); private: // initialize various pages of the dialog void setupGeneralPage(const Profile::Ptr profile); void setupTabsPage(const Profile::Ptr profile); void setupAppearancePage(const Profile::Ptr profile); void setupKeyboardPage(const Profile::Ptr profile); void setupScrollingPage(const Profile::Ptr profile); void setupAdvancedPage(const Profile::Ptr profile); void setupMousePage(const Profile::Ptr info); void updateColorSchemeList(bool selectCurrentScheme = false); void updateColorSchemeButtons(); void updateKeyBindingsList(bool selectCurrentTranslator = false); void updateKeyBindingsButtons(); void showColorSchemeEditor(bool isNewScheme); void closeColorSchemeEditor(); void showKeyBindingEditor(bool newTranslator); void preview(int property , const QVariant& value); void delayedPreview(int property , const QVariant& value); void unpreview(int property); void unpreviewAll(); void enableIfNonEmptySelection(QWidget* widget, QItemSelectionModel* selectionModel); void updateCaption(const Profile::Ptr profile); void updateTransparencyWarning(); // Update _tempProfile in a way of respecting the apply button. // When used with some previewed property, this method should // always come after the preview operation. void updateTempProfileProperty(Profile::Property, const QVariant& value); // helper method for creating an empty & hidden profile and assigning // it to _tempProfile. void createTempProfile(); // Enable or disable apply button, used only within // updateTempProfileProperty(). void updateButtonApply(); static QString groupProfileNames(const ProfileGroup::Ptr group, int maxLength = -1); struct RadioOption { QAbstractButton* button; int value; const char* slot; }; void setupRadio(RadioOption* possibilities, int actual); struct BooleanOption { QAbstractButton* button; Profile::Property property; const char* slot; }; void setupCheckBoxes(BooleanOption* options , const Profile::Ptr profile); Ui::EditProfileDialog* _ui; Profile::Ptr _tempProfile; Profile::Ptr _profile; // keeps track of pages which need to be updated to match the current // profile. all elements in this vector are set to true when the // profile is changed and individual elements are set to false // after an update by a call to ensurePageLoaded() QVector _pageNeedsUpdate; QHash _previewedProperties; QHash _delayedPreviewProperties; QTimer* _delayedPreviewTimer; ColorSchemeEditor* _colorDialog; QDialogButtonBox *mButtonBox; }; /** * A delegate which can display and edit color schemes in a view. */ class ColorSchemeViewDelegate : public QAbstractItemDelegate { Q_OBJECT public: explicit ColorSchemeViewDelegate(QObject* parent = 0); // reimplemented virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; }; } #endif // EDITPROFILEDIALOG_H diff --git a/src/EditProfileDialog.ui b/src/EditProfileDialog.ui index 8358bd38..228704f4 100644 --- a/src/EditProfileDialog.ui +++ b/src/EditProfileDialog.ui @@ -1,1469 +1,1479 @@ EditProfileDialog 0 0 594 551 0 0 0 0 true General General true Profile name: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter A descriptive name for the profile Command: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter The command to execute when new terminal sessions are created using this profile Qt::LeftToRight Initial directory: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter The initial working directory for new terminal sessions using this profile Qt::LeftToRight Choose the initial directory ... Start in same directory as current tab Icon: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 64 64 0 0 Select the icon displayed on tabs using this profile 48 48 Qt::Horizontal 20 20 Environment: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Edit the list of environment variables and associated values Edit... Terminal Size true Columns 1 500 Rows 1 Qt::Horizontal QSizePolicy::Fixed 40 20 This will not alter any open windows. Qt::Horizontal 81 20 Qt::Horizontal QSizePolicy::Fixed 40 20 Configure Konsole->General->Use current window size on next startup must be disabled for these entries to work. true Window true Show terminal size in columns and lines in the center of window after resizing Show hint for terminal size after resizing Qt::Vertical 20 20 Tabs Tab Titles true Tab Monitoring true Threshold for continuous silence: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter The threshold for continuous silence to be detected by Konsole 1 3600 Qt::Horizontal 20 20 Qt::Vertical 20 10 Appearance 0 1 Color Scheme && Background true QAbstractItemView::ScrollPerPixel Create a new color scheme based upon the selected scheme New... Edit the selected color scheme Edit... Delete the selected color scheme Remove Qt::Vertical 20 20 Font true Preview: 1 0 KSqueezedTextLabel Qt::ElideRight Text size: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 4.000000000000000 999.000000000000000 1.000000000000000 1 Select the font used in this profile Select Font... Show all fonts instead of the monospaced fonts Show All Fonts Qt::Horizontal 40 20 Smooth fonts Draw intense colors in bold font Use the selected font for line characters instead of the builtin code Use line characters contained in font Scrolling Scrollback true Scroll Bar true 0 0 Show the scroll bar on the left side of the terminal window Show on left side 0 0 Show the scroll bar on the right side of the terminal window Show on right side 0 0 Hide the scroll bar Hide Scroll Page Up/Down Amount true Scroll the page the half height of window Half Page Height Scroll the page the full height of window Full Page Height Qt::Vertical 20 20 Keyboard Key Bindings true Key bindings control how combinations of keystrokes in the terminal window are converted into the stream of characters which are sent to the current terminal program. true 32 32 Create a new key bindings list based upon the selected bindings New... Edit the selected key bindings list Edit... Delete the selected key bindings list Remove Qt::Vertical 20 20 Mouse Select Text true Characters considered part of a word when double clicking: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter true Characters which are considered part of a word when double-clicking to select whole words in the terminal Triple-click select: Which part of current line should be selected with triple click . The whole current line From mouse position to the end of line Copy && Paste true Automatically copy selected text into clipboard Copy on select Trim trailing spaces in selected text, useful in some instances Trim trailing spaces Qt::Horizontal 561 5 Mouse middle button: Paste from selection Paste from clipboard Miscellaneous true - Text recognized as a file, link or an email address will be underlined when hovered by the mouse pointer. + Text recognized as a link or an email address will be underlined when hovered by the mouse pointer. - Underline files and links + Underline links + + + + + + + Text recognized as a file will be underlined when hovered by the mouse pointer. + + + Underline files Qt::Horizontal QSizePolicy::Fixed 10 20 false Text recognized as a file, link or an email address can be opened by direct mouse click. Open files and links by direct click Selected text will require control key plus click to drag. Require Ctrl key for drag and drop Always paste dropped URLs as text without offering move, copy and link actions. Disable drag and drop menu for URLs and files Pressing Ctrl+scrollwheel will increase/decrease the text size. Allow Ctrl+scrollwheel to zoom text size Qt::Vertical 20 328 Advanced Terminal Features true Show URL hints when these keys are pressed: Qt::Horizontal QSizePolicy::Fixed 20 20 Shift Control Alt Meta 0 0 Allow terminal programs to create blinking sections of text Allow blinking text 0 0 Allow the output to be suspended by pressing Ctrl+S Enable flow control using Ctrl+S, Ctrl+Q 0 0 Enable Bi-Directional display on terminals (valid for Arabic, Farsi or Hebrew only) Enable Bi-Directional text rendering Line Spacing: The number of pixels between two lines 0 5 Qt::Horizontal 40 20 Cursor true 0 0 Make the cursor blink regularly Blinking cursor Cursor shape: Change the shape of the cursor Block I-Beam Underline Qt::Horizontal 40 20 0 0 Set the cursor to match the color of the character underneath it. Set cursor color to match current character 0 0 Use a custom, fixed color for the cursor Custom cursor color: 0 0 Select the color used to draw the cursor Qt::Horizontal 40 20 Encoding true Default character encoding: 0 0 DEFAULTENCODING Select Qt::Vertical 20 20 KPluralHandlingSpinBox QSpinBox
KPluralHandlingSpinBox
KColorButton QPushButton
kcolorbutton.h
KComboBox QComboBox
kcombobox.h
KSqueezedTextLabel QLabel
ksqueezedtextlabel.h
KMessageWidget QFrame
kmessagewidget.h
1
Konsole::RenameTabWidget QWidget
RenameTabWidget.h
Konsole::HistorySizeWidget QWidget
HistorySizeWidget.h
diff --git a/src/Profile.cpp b/src/Profile.cpp index a7866dab..096c9099 100644 --- a/src/Profile.cpp +++ b/src/Profile.cpp @@ -1,379 +1,381 @@ /* This source file is part of Konsole, a terminal emulator. Copyright 2006-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 "Profile.h" // Qt #include #include // KDE #include #include // Konsole #include "Enumeration.h" using namespace Konsole; // mappings between property enum values and names // // multiple names are defined for some property values, // in these cases, the "proper" string name comes first, // as that is used when reading/writing profiles from/to disk // // the other names are usually shorter versions for convenience // when parsing konsoleprofile commands static const char GENERAL_GROUP[] = "General"; static const char KEYBOARD_GROUP[] = "Keyboard"; static const char APPEARANCE_GROUP[] = "Appearance"; static const char SCROLLING_GROUP[] = "Scrolling"; static const char TERMINAL_GROUP[] = "Terminal Features"; static const char CURSOR_GROUP[] = "Cursor Options"; static const char INTERACTION_GROUP[] = "Interaction Options"; static const char ENCODING_GROUP[] = "Encoding Options"; const Profile::PropertyInfo Profile::DefaultPropertyNames[] = { // General { Path , "Path" , 0 , QVariant::String } , { Name , "Name" , GENERAL_GROUP , QVariant::String } , { UntranslatedName, "UntranslatedName" , 0 , QVariant::String } , { Icon , "Icon" , GENERAL_GROUP , QVariant::String } , { Command , "Command" , 0 , QVariant::String } , { Arguments , "Arguments" , 0 , QVariant::StringList } , { MenuIndex, "MenuIndex" , 0, QVariant::String } , { Environment , "Environment" , GENERAL_GROUP , QVariant::StringList } , { Directory , "Directory" , GENERAL_GROUP , QVariant::String } , { LocalTabTitleFormat , "LocalTabTitleFormat" , GENERAL_GROUP , QVariant::String } , { LocalTabTitleFormat , "tabtitle" , 0 , QVariant::String } , { RemoteTabTitleFormat , "RemoteTabTitleFormat" , GENERAL_GROUP , QVariant::String } , { ShowTerminalSizeHint , "ShowTerminalSizeHint" , GENERAL_GROUP , QVariant::Bool } , { StartInCurrentSessionDir , "StartInCurrentSessionDir" , GENERAL_GROUP , QVariant::Bool } , { SilenceSeconds, "SilenceSeconds" , GENERAL_GROUP , QVariant::Int } , { TerminalColumns, "TerminalColumns" , GENERAL_GROUP , QVariant::Int } , { TerminalRows, "TerminalRows" , GENERAL_GROUP , QVariant::Int } , { TerminalMargin, "TerminalMargin" , GENERAL_GROUP , QVariant::Int } , { TerminalCenter, "TerminalCenter" , GENERAL_GROUP , QVariant::Bool } // Appearance , { Font , "Font" , APPEARANCE_GROUP , QVariant::Font } , { ColorScheme , "ColorScheme" , APPEARANCE_GROUP , QVariant::String } , { ColorScheme , "colors" , 0 , QVariant::String } , { AntiAliasFonts, "AntiAliasFonts" , APPEARANCE_GROUP , QVariant::Bool } , { BoldIntense, "BoldIntense", APPEARANCE_GROUP, QVariant::Bool } , { UseFontLineCharacters, "UseFontLineChararacters", APPEARANCE_GROUP, QVariant::Bool } , { LineSpacing , "LineSpacing" , APPEARANCE_GROUP , QVariant::Int } // Keyboard , { KeyBindings , "KeyBindings" , KEYBOARD_GROUP , QVariant::String } // Scrolling , { HistoryMode , "HistoryMode" , SCROLLING_GROUP , QVariant::Int } , { HistorySize , "HistorySize" , SCROLLING_GROUP , QVariant::Int } , { ScrollBarPosition , "ScrollBarPosition" , SCROLLING_GROUP , QVariant::Int } , { ScrollFullPage , "ScrollFullPage" , SCROLLING_GROUP , QVariant::Bool } // Terminal Features , { UrlHintsModifiers , "UrlHintsModifiers" , TERMINAL_GROUP , QVariant::Int } , { BlinkingTextEnabled , "BlinkingTextEnabled" , TERMINAL_GROUP , QVariant::Bool } , { FlowControlEnabled , "FlowControlEnabled" , TERMINAL_GROUP , QVariant::Bool } , { BidiRenderingEnabled , "BidiRenderingEnabled" , TERMINAL_GROUP , QVariant::Bool } , { BlinkingCursorEnabled , "BlinkingCursorEnabled" , TERMINAL_GROUP , QVariant::Bool } , { BellMode , "BellMode" , TERMINAL_GROUP , QVariant::Int } // Cursor , { UseCustomCursorColor , "UseCustomCursorColor" , CURSOR_GROUP , QVariant::Bool} , { CursorShape , "CursorShape" , CURSOR_GROUP , QVariant::Int} , { CustomCursorColor , "CustomCursorColor" , CURSOR_GROUP , QVariant::Color } // Interaction , { WordCharacters , "WordCharacters" , INTERACTION_GROUP , QVariant::String } , { TripleClickMode , "TripleClickMode" , INTERACTION_GROUP , QVariant::Int } , { UnderlineLinksEnabled , "UnderlineLinksEnabled" , INTERACTION_GROUP , QVariant::Bool } + , { UnderlineFilesEnabled , "UnderlineFilesEnabled" , INTERACTION_GROUP , QVariant::Bool } , { OpenLinksByDirectClickEnabled , "OpenLinksByDirectClickEnabled" , INTERACTION_GROUP , QVariant::Bool } , { CtrlRequiredForDrag, "CtrlRequiredForDrag" , INTERACTION_GROUP , QVariant::Bool } , { DropUrlsAsText , "DropUrlsAsText" , INTERACTION_GROUP , QVariant::Bool } , { AutoCopySelectedText , "AutoCopySelectedText" , INTERACTION_GROUP , QVariant::Bool } , { TrimTrailingSpacesInSelectedText , "TrimTrailingSpacesInSelectedText" , INTERACTION_GROUP , QVariant::Bool } , { PasteFromSelectionEnabled , "PasteFromSelectionEnabled" , INTERACTION_GROUP , QVariant::Bool } , { PasteFromClipboardEnabled , "PasteFromClipboardEnabled" , INTERACTION_GROUP , QVariant::Bool } , { MiddleClickPasteMode, "MiddleClickPasteMode" , INTERACTION_GROUP , QVariant::Int } , { MouseWheelZoomEnabled, "MouseWheelZoomEnabled", INTERACTION_GROUP, QVariant::Bool } // Encoding , { DefaultEncoding , "DefaultEncoding" , ENCODING_GROUP , QVariant::String } , { (Property)0 , 0 , 0, QVariant::Invalid } }; QHash Profile::PropertyInfoByName; QHash Profile::PropertyInfoByProperty; void Profile::fillTableWithDefaultNames() { static bool filledDefaults = false; if (filledDefaults) return; const PropertyInfo* iter = DefaultPropertyNames; while (iter->name != 0) { registerProperty(*iter); iter++; } filledDefaults = true; } FallbackProfile::FallbackProfile() : Profile() { // Fallback settings setProperty(Name, i18nc("Name of the default/builtin profile", "Default")); setProperty(UntranslatedName, "Default"); // magic path for the fallback profile which is not a valid // non-directory file name setProperty(Path, "FALLBACK/"); setProperty(Command, qgetenv("SHELL")); // See Pty.cpp on why Arguments is populated setProperty(Arguments, QStringList() << qgetenv("SHELL")); setProperty(Icon, "utilities-terminal"); setProperty(Environment, QStringList() << QStringLiteral("TERM=xterm")); setProperty(LocalTabTitleFormat, "%d : %n"); setProperty(RemoteTabTitleFormat, "(%u) %H"); setProperty(ShowTerminalSizeHint, true); setProperty(StartInCurrentSessionDir, true); setProperty(MenuIndex, "0"); setProperty(SilenceSeconds, 10); setProperty(TerminalColumns, 80); setProperty(TerminalRows, 24); setProperty(TerminalMargin, 1); setProperty(TerminalCenter, false); setProperty(MouseWheelZoomEnabled, true); setProperty(KeyBindings, "default"); setProperty(ColorScheme, "Linux"); //use DarkPastels when is start support blue ncurses UI properly setProperty(Font, QFontDatabase::systemFont(QFontDatabase::FixedFont)); setProperty(HistoryMode, Enum::FixedSizeHistory); setProperty(HistorySize, 1000); setProperty(ScrollBarPosition, Enum::ScrollBarRight); setProperty(ScrollFullPage, false); setProperty(FlowControlEnabled, true); setProperty(UrlHintsModifiers, 0); setProperty(BlinkingTextEnabled, true); - setProperty(UnderlineLinksEnabled, false); + setProperty(UnderlineLinksEnabled, true); + setProperty(UnderlineFilesEnabled, false); setProperty(OpenLinksByDirectClickEnabled, false); setProperty(CtrlRequiredForDrag, true); setProperty(AutoCopySelectedText, false); setProperty(TrimTrailingSpacesInSelectedText, false); setProperty(DropUrlsAsText, false); setProperty(PasteFromSelectionEnabled, true); setProperty(PasteFromClipboardEnabled, false); setProperty(MiddleClickPasteMode, Enum::PasteFromX11Selection); setProperty(TripleClickMode, Enum::SelectWholeLine); setProperty(BlinkingCursorEnabled, false); setProperty(BidiRenderingEnabled, true); setProperty(LineSpacing, 0); setProperty(CursorShape, Enum::BlockCursor); setProperty(UseCustomCursorColor, false); setProperty(CustomCursorColor, QColor(Qt::black)); setProperty(BellMode, Enum::NotifyBell); setProperty(DefaultEncoding, QString(QTextCodec::codecForLocale()->name())); setProperty(AntiAliasFonts, true); setProperty(BoldIntense, true); setProperty(UseFontLineCharacters, false); setProperty(WordCharacters, ":@-./_~?&=%+#"); // Fallback should not be shown in menus setHidden(true); } Profile::Profile(Profile::Ptr parent) : _parent(parent) , _hidden(false) { } void Profile::clone(Profile::Ptr profile, bool differentOnly) { const PropertyInfo* properties = DefaultPropertyNames; while (properties->name != 0) { Property current = properties->property; QVariant otherValue = profile->property(current); switch (current) { case Name: case Path: break; default: if (!differentOnly || property(current) != otherValue) { setProperty(current, otherValue); } } properties++; } } Profile::~Profile() { } bool Profile::isHidden() const { return _hidden; } void Profile::setHidden(bool hidden) { _hidden = hidden; } void Profile::setParent(Profile::Ptr parent) { _parent = parent; } const Profile::Ptr Profile::parent() const { return _parent; } bool Profile::isEmpty() const { return _propertyValues.isEmpty(); } QHash Profile::setProperties() const { return _propertyValues; } void Profile::setProperty(Property property , const QVariant& value) { _propertyValues.insert(property, value); } bool Profile::isPropertySet(Property property) const { return _propertyValues.contains(property); } Profile::Property Profile::lookupByName(const QString& name) { // insert default names into table the first time this is called fillTableWithDefaultNames(); return PropertyInfoByName[name.toLower()].property; } void Profile::registerProperty(const PropertyInfo& info) { PropertyInfoByName.insert(QString(info.name).toLower(), info); // only allow one property -> name map // (multiple name -> property mappings are allowed though) if (!PropertyInfoByProperty.contains(info.property)) PropertyInfoByProperty.insert(info.property, info); } int Profile::menuIndexAsInt() const { bool ok; int index = menuIndex().toInt(&ok, 10); if (ok) return index; else return 0; } const QStringList Profile::propertiesInfoList() const { QStringList info; const PropertyInfo* iter = DefaultPropertyNames; while (iter->name != 0) { info << QString(iter->name) + " : " + QString(QVariant(iter->type).typeName()); iter++; } return info; } QHash ProfileCommandParser::parse(const QString& input) { QHash changes; // regular expression to parse profile change requests. // // format: property=value;property=value ... // // where 'property' is a word consisting only of characters from A-Z // where 'value' is any sequence of characters other than a semi-colon // static const QRegularExpression regExp("([a-zA-Z]+)=([^;]+)"); QRegularExpressionMatchIterator iterator(regExp.globalMatch(input)); while (iterator.hasNext()) { QRegularExpressionMatch match(iterator.next()); Profile::Property property = Profile::lookupByName(match.captured(1)); const QString value = match.captured(2); changes.insert(property, value); } return changes; } void ProfileGroup::updateValues() { const PropertyInfo* properties = Profile::DefaultPropertyNames; while (properties->name != 0) { // the profile group does not store a value for some properties // (eg. name, path) if even they are equal between profiles - // // the exception is when the group has only one profile in which // case it behaves like a standard Profile if (_profiles.count() > 1 && !canInheritProperty(properties->property)) { properties++; continue; } QVariant value; for (int i = 0; i < _profiles.count(); i++) { QVariant profileValue = _profiles[i]->property(properties->property); if (value.isNull()) value = profileValue; else if (value != profileValue) { value = QVariant(); break; } } Profile::setProperty(properties->property, value); properties++; } } void ProfileGroup::setProperty(Property property, const QVariant& value) { if (_profiles.count() > 1 && !canInheritProperty(property)) return; Profile::setProperty(property, value); foreach(Profile::Ptr profile, _profiles) { profile->setProperty(property, value); } } diff --git a/src/Profile.h b/src/Profile.h index 3e3aebf4..db6c9fd3 100644 --- a/src/Profile.h +++ b/src/Profile.h @@ -1,717 +1,726 @@ /* This source file is part of Konsole, a terminal emulator. 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. */ #ifndef PROFILE_H #define PROFILE_H // Qt #include #include #include #include #include // Konsole #include "konsoleprivate_export.h" namespace Konsole { class ProfileGroup; /** * Represents a terminal set-up which can be used to * set the initial state of new terminal sessions or applied * to existing sessions. Profiles consist of a number of named * properties, which can be retrieved using property() and * set using setProperty(). isPropertySet() can be used to check * whether a particular property has been set in a profile. * * Profiles support a simple form of inheritance. When a new Profile * is constructed, a pointer to a parent profile can be passed to * the constructor. When querying a particular property of a profile * using property(), the profile will return its own value for that * property if one has been set or otherwise it will return the * parent's value for that property. * * Profiles can be loaded from disk using ProfileReader instances * and saved to disk using ProfileWriter instances. */ class KONSOLEPRIVATE_EXPORT Profile : public QSharedData { friend class KDE4ProfileReader; friend class KDE4ProfileWriter; friend class ProfileGroup; public: typedef QExplicitlySharedDataPointer Ptr; typedef QExplicitlySharedDataPointer GroupPtr; /** * This enum describes the available properties * which a Profile may consist of. * * Properties can be set using setProperty() and read * using property() */ enum Property { /** (QString) Path to the profile's configuration file on-disk. */ Path, /** (QString) The descriptive name of this profile. */ Name, /** (QString) The untranslated name of this profile. * Warning: this is an internal property. Do not touch it. */ UntranslatedName, /** (QString) The name of the icon associated with this profile. * This is used in menus and tabs to represent the profile. */ Icon, /** (QString) The command to execute ( excluding arguments ) when * creating a new terminal session using this profile. */ Command, /** (QStringList) The arguments which are passed to the program * specified by the Command property when creating a new terminal * session using this profile. */ Arguments, /** (QStringList) Additional environment variables (in the form of * NAME=VALUE pairs) which are passed to the program specified by * the Command property when creating a new terminal session using * this profile. */ Environment, /** (QString) The initial working directory for sessions created * using this profile. */ Directory, /** (QString) The format used for tab titles when running normal * commands. */ LocalTabTitleFormat, /** (QString) The format used for tab titles when the session is * running a remote command (eg. SSH) */ RemoteTabTitleFormat, /** (bool) Specifies whether show hint for terminal size after * resizing the application window. */ ShowTerminalSizeHint, /** (QFont) The font to use in terminal displays using this profile. */ Font, /** (QString) The name of the color scheme to use in terminal * displays using this profile. * Color schemes are managed by the ColorSchemeManager class. */ ColorScheme, /** (QString) The name of the key bindings. * Key bindings are managed by the KeyboardTranslatorManager class. */ KeyBindings, /** (HistoryModeEnum) Specifies the storage type used for keeping * the output produced by terminal sessions using this profile. * * See Enum::HistoryModeEnum */ HistoryMode, /** (int) Specifies the number of lines of output to remember in * terminal sessions using this profile. Once the limit is reached, * the oldest lines are lost if the HistoryMode property is * FixedSizeHistory */ HistorySize, /** (ScrollBarPositionEnum) Specifies the position of the scroll bar * in terminal displays using this profile. * * See Enum::ScrollBarPositionEnum */ ScrollBarPosition, /** (bool) Specifies whether the PageUp/Down will scroll the full * height or half height. */ ScrollFullPage, /** (bool) Specifies whether the terminal will enable Bidirectional * text display */ BidiRenderingEnabled, /** (bool) Specifies whether text in terminal displays is allowed * to blink. */ BlinkingTextEnabled, /** (bool) Specifies whether the flow control keys (typically Ctrl+S, * Ctrl+Q) have any effect. Also known as Xon/Xoff */ FlowControlEnabled, /** (int) Specifies the pixels between the terminal lines. */ LineSpacing, /** (bool) Specifies whether the cursor blinks ( in a manner similar * to text editing applications ) */ BlinkingCursorEnabled, /** (bool) If true, terminal displays use a fixed color to draw the * cursor, specified by the CustomCursorColor property. Otherwise * the cursor changes color to match the character underneath it. */ UseCustomCursorColor, /** (CursorShapeEnum) The shape used by terminal displays to * represent the cursor. * * See Enum::CursorShapeEnum */ CursorShape, /** (QColor) The color used by terminal displays to draw the cursor. * Only applicable if the UseCustomCursorColor property is true. */ CustomCursorColor, /** (QString) A string consisting of the characters used to delimit * words when selecting text in the terminal display. */ WordCharacters, /** (TripleClickModeEnum) Specifies which part of current line should * be selected with triple click action. * * See Enum::TripleClickModeEnum */ TripleClickMode, /** (bool) If true, text that matches a link or an email address is * underlined when hovered by the mouse pointer. */ UnderlineLinksEnabled, + /** (bool) If true, text that matches a file is + * underlined when hovered by the mouse pointer. + */ + UnderlineFilesEnabled, /** (bool) If true, links can be opened by direct mouse click.*/ OpenLinksByDirectClickEnabled, /** (bool) If true, control key must be pressed to click and drag selected text. */ CtrlRequiredForDrag, /** (bool) If true, automatically copy selected text into the clipboard */ AutoCopySelectedText, /** (bool) If true, trailing spaces are trimmed in selected text */ TrimTrailingSpacesInSelectedText, /** (bool) If true, then dropped URLs will be pasted as text without asking */ DropUrlsAsText, /** (bool) If true, middle mouse button pastes from X Selection */ PasteFromSelectionEnabled, /** (bool) If true, middle mouse button pastes from Clipboard */ PasteFromClipboardEnabled, /** (MiddleClickPasteModeEnum) Specifies the source from which mouse * middle click pastes data. * * See Enum::MiddleClickPasteModeEnum */ MiddleClickPasteMode, /** (String) Default text codec */ DefaultEncoding, /** (bool) Whether fonts should be aliased or not */ AntiAliasFonts, /** (bool) Whether character with intense colors should be rendered * in bold font or just in bright color. */ BoldIntense, /** (bool) Whether to use font's line characters instead of the * builtin code. */ UseFontLineCharacters, /** (bool) Whether new sessions should be started in the same * directory as the currently active session. */ StartInCurrentSessionDir, /** (int) Specifies the threshold of detected silence in seconds. */ SilenceSeconds, /** (BellModeEnum) Specifies the behavior of bell. * * See Enum::BellModeEnum */ BellMode, /** (int) Specifies the preferred columns. */ TerminalColumns, /** (int) Specifies the preferred rows. */ TerminalRows, /** Index of profile in the File Menu * WARNING: this is currently an internal field, which is * expected to be zero on disk. Do not modify it manually. * * In future, the format might be #.#.# to account for levels */ MenuIndex, /** (int) Margin width in pixels */ TerminalMargin, /** (bool) Center terminal when there is a margin */ TerminalCenter, /** (bool) If true, mouse wheel scroll with Ctrl key pressed * increases/decreases the terminal font size. */ MouseWheelZoomEnabled, /** (int) Keyboard modifiers to show URL hints */ UrlHintsModifiers }; /** * Constructs a new profile * * @param parent The parent profile. When querying the value of a * property using property(), if the property has not been set in this * profile then the parent's value for the property will be returned. */ explicit Profile(Ptr parent = Ptr()); virtual ~Profile(); /** * Copies all properties except Name and Path from the specified @p * profile into this profile * * @param profile The profile to copy properties from * @param differentOnly If true, only properties in @p profile which have * a different value from this profile's current value (either set via * setProperty() or inherited from the parent profile) will be set. */ void clone(Ptr profile, bool differentOnly = true); /** * Changes the parent profile. When calling the property() method, * if the specified property has not been set for this profile, * the parent's value for the property will be returned instead. */ void setParent(Ptr parent); /** Returns the parent profile. */ const Ptr parent() const; /** Returns this profile as a group or null if this profile is not a * group. */ const GroupPtr asGroup() const; GroupPtr asGroup(); /** * Returns the current value of the specified @p property, cast to type T. * Internally properties are stored using the QVariant type and cast to T * using QVariant::value(); * * If the specified @p property has not been set in this profile, * and a non-null parent was specified in the Profile's constructor, * the parent's value for @p property will be returned. */ template T property(Property property) const; /** Sets the value of the specified @p property to @p value. */ virtual void setProperty(Property property, const QVariant& value); /** Returns true if the specified property has been set in this Profile * instance. */ virtual bool isPropertySet(Property property) const; /** Returns a map of the properties set in this Profile instance. */ virtual QHash setProperties() const; /** Returns true if no properties have been set in this Profile instance. */ bool isEmpty() const; /** * Returns true if this is a 'hidden' profile which should not be * displayed in menus or saved to disk. * * This is used for the fallback profile, in case there are no profiles on * disk which can be loaded, or for overlay profiles created to handle * command-line arguments which change profile properties. */ bool isHidden() const; /** Specifies whether this is a hidden profile. See isHidden() */ void setHidden(bool hidden); // // Convenience methods for property() and setProperty() go here // /** Convenience method for property(Profile::Path) */ QString path() const { return property(Profile::Path); } /** Convenience method for property(Profile::Name) */ QString name() const { return property(Profile::Name); } /** Convenience method for property(Profile::UntranslatedName) */ QString untranslatedName() const { return property(Profile::UntranslatedName); } /** Convenience method for property(Profile::Directory) */ QString defaultWorkingDirectory() const { return property(Profile::Directory); } /** Convenience method for property(Profile::Icon) */ QString icon() const { return property(Profile::Icon); } /** Convenience method for property(Profile::Command) */ QString command() const { return property(Profile::Command); } /** Convenience method for property(Profile::Arguments) */ QStringList arguments() const { return property(Profile::Arguments); } /** Convenience method for property(Profile::LocalTabTitleFormat) */ QString localTabTitleFormat() const { return property(Profile::LocalTabTitleFormat); } /** Convenience method for property(Profile::RemoteTabTitleFormat) */ QString remoteTabTitleFormat() const { return property(Profile::RemoteTabTitleFormat); } /** Convenience method for property(Profile::ShowTerminalSizeHint) */ bool showTerminalSizeHint() const { return property(Profile::ShowTerminalSizeHint); } /** Convenience method for property(Profile::Font) */ QFont font() const { return property(Profile::Font); } /** Convenience method for property(Profile::ColorScheme) */ QString colorScheme() const { return property(Profile::ColorScheme); } /** Convenience method for property(Profile::Environment) */ QStringList environment() const { return property(Profile::Environment); } /** Convenience method for property(Profile::KeyBindings) */ QString keyBindings() const { return property(Profile::KeyBindings); } /** Convenience method for property(Profile::HistorySize) */ int historySize() const { return property(Profile::HistorySize); } /** Convenience method for property(Profile::BidiRenderingEnabled) */ bool bidiRenderingEnabled() const { return property(Profile::BidiRenderingEnabled); } /** Convenience method for property(Profile::LineSpacing) */ int lineSpacing() const { return property(Profile::LineSpacing); } /** Convenience method for property(Profile::BlinkingTextEnabled) */ bool blinkingTextEnabled() const { return property(Profile::BlinkingTextEnabled); } /** Convenience method for property(Profile::MouseWheelZoomEnabled) */ bool mouseWheelZoomEnabled() const { return property(Profile::MouseWheelZoomEnabled); } /** Convenience method for property(Profile::BlinkingCursorEnabled) */ bool blinkingCursorEnabled() const { return property(Profile::BlinkingCursorEnabled); } /** Convenience method for property(Profile::FlowControlEnabled) */ bool flowControlEnabled() const { return property(Profile::FlowControlEnabled); } /** Convenience method for property(Profile::UseCustomCursorColor) */ bool useCustomCursorColor() const { return property(Profile::UseCustomCursorColor); } /** Convenience method for property(Profile::CustomCursorColor) */ QColor customCursorColor() const { return property(Profile::CustomCursorColor); } /** Convenience method for property(Profile::WordCharacters) */ QString wordCharacters() const { return property(Profile::WordCharacters); } /** Convenience method for property(Profile::UnderlineLinksEnabled) */ bool underlineLinksEnabled() const { return property(Profile::UnderlineLinksEnabled); } + /** Convenience method for property(Profile::UnderlineFilesEnabled) */ + bool underlineFilesEnabled() const { + return property(Profile::UnderlineFilesEnabled); + } + bool autoCopySelectedText() const { return property(Profile::AutoCopySelectedText); } /** Convenience method for property(Profile::DefaultEncoding) */ QString defaultEncoding() const { return property(Profile::DefaultEncoding); } /** Convenience method for property(Profile::AntiAliasFonts) */ bool antiAliasFonts() const { return property(Profile::AntiAliasFonts); } /** Convenience method for property(Profile::BoldIntense) */ bool boldIntense() const { return property(Profile::BoldIntense); } /** Convenience method for property(Profile::UseFontLineCharacters)*/ bool useFontLineCharacters() const { return property(Profile::UseFontLineCharacters); } /** Convenience method for property(Profile::StartInCurrentSessionDir) */ bool startInCurrentSessionDir() const { return property(Profile::StartInCurrentSessionDir); } /** Convenience method for property(Profile::SilenceSeconds) */ int silenceSeconds() const { return property(Profile::SilenceSeconds); } /** Convenience method for property(Profile::TerminalColumns) */ int terminalColumns() const { return property(Profile::TerminalColumns); } /** Convenience method for property(Profile::TerminalRows) */ int terminalRows() const { return property(Profile::TerminalRows); } /** Convenience method for property(Profile::TerminalMargin) */ int terminalMargin() const { return property(Profile::TerminalMargin); } /** Convenience method for property(Profile::TerminalCenter) */ bool terminalCenter() const { return property(Profile::TerminalCenter); } /** Convenience method for property(Profile::MenuIndex) */ QString menuIndex() const { return property(Profile::MenuIndex); } int menuIndexAsInt() const; /** Return a list of all properties names and their type * (for use with -p option). */ const QStringList propertiesInfoList() const; /** * Returns the element from the Property enum associated with the * specified @p name. * * @param name The name of the property to look for, this is case * insensitive. */ static Property lookupByName(const QString& name); private: struct PropertyInfo; // Defines a new property, this property is then available // to all Profile instances. static void registerProperty(const PropertyInfo& info); // fills the table with default names for profile properties // the first time it is called. // subsequent calls return immediately static void fillTableWithDefaultNames(); // returns true if the property can be inherited static bool canInheritProperty(Property property); QHash _propertyValues; Ptr _parent; bool _hidden; static QHash PropertyInfoByName; static QHash PropertyInfoByProperty; // Describes a property. Each property has a name and group // which is used when saving/loading the profile. struct PropertyInfo { Property property; const char* name; const char* group; QVariant::Type type; }; static const PropertyInfo DefaultPropertyNames[]; }; inline bool Profile::canInheritProperty(Property aProperty) { return aProperty != Name && aProperty != Path; } template inline T Profile::property(Property aProperty) const { return property(aProperty).value(); } template <> inline QVariant Profile::property(Property aProperty) const { if (_propertyValues.contains(aProperty)) { return _propertyValues[aProperty]; } else if (_parent && canInheritProperty(aProperty)) { return _parent->property(aProperty); } else { return QVariant(); } } /** * A profile which contains a number of default settings for various * properties. This can be used as a parent for other profiles or a * fallback in case a profile cannot be loaded from disk. */ class FallbackProfile : public Profile { public: FallbackProfile(); }; /** * A composite profile which allows a group of profiles to be treated as one. * When setting a property, the new value is applied to all profiles in the * group. When reading a property, if all profiles in the group have the same * value then that value is returned, otherwise the result is null. * * Profiles can be added to the group using addProfile(). When all profiles * have been added updateValues() must be called * to sync the group's property values with those of the group's profiles. * * The Profile::Name and Profile::Path properties are unique to individual * profiles, setting these properties on a ProfileGroup has no effect. */ class KONSOLEPRIVATE_EXPORT ProfileGroup : public Profile { public: typedef QExplicitlySharedDataPointer Ptr; /** Construct a new profile group, which is hidden by default. */ explicit ProfileGroup(Profile::Ptr parent = Profile::Ptr()); /** Add a profile to the group. Calling setProperty() will update this * profile. When creating a group, add the profiles to the group then * call updateValues() to make the group's property values reflect the * profiles currently in the group. */ void addProfile(Profile::Ptr profile) { _profiles.append(profile); } /** Remove a profile from the group. Calling setProperty() will no longer * affect this profile. */ void removeProfile(Profile::Ptr profile) { _profiles.removeAll(profile); } /** Returns the profiles in this group .*/ QList profiles() const { return _profiles; } /** * Updates the property values in this ProfileGroup to match those from * the group's profiles() * * For each available property, if each profile in the group has the same * value then the ProfileGroup will use that value for the property. * Otherwise the value for the property will be set to a null QVariant * * Some properties such as the name and the path of the profile * will always be set to null if the group has more than one profile. */ void updateValues(); /** Sets the value of @p property in each of the group's profiles to * @p value. */ void setProperty(Property property, const QVariant& value); private: QList _profiles; }; inline ProfileGroup::ProfileGroup(Profile::Ptr profileParent) : Profile(profileParent) { setHidden(true); } inline const Profile::GroupPtr Profile::asGroup() const { const Profile::GroupPtr ptr(dynamic_cast( const_cast(this))); return ptr; } inline Profile::GroupPtr Profile::asGroup() { return Profile::GroupPtr(dynamic_cast(this)); } /** * Parses an input string consisting of property names * and assigned values and returns a table of properties * and values. * * The input string will typically look like this: * * @code * PropertyName=Value;PropertyName=Value ... * @endcode * * For example: * * @code * Icon=konsole;Directory=/home/bob * @endcode */ class KONSOLEPRIVATE_EXPORT ProfileCommandParser { public: /** * Parses an input string consisting of property names * and assigned values and returns a table of * properties and values. */ QHash parse(const QString& input); }; } Q_DECLARE_METATYPE(Konsole::Profile::Ptr) #endif // PROFILE_H diff --git a/src/SessionController.cpp b/src/SessionController.cpp index 929a6d26..0e286e45 100644 --- a/src/SessionController.cpp +++ b/src/SessionController.cpp @@ -1,1962 +1,1994 @@ /* Copyright 2006-2008 by Robert Knight Copyright 2009 by Thomas Dreibholz 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 "SessionController.h" +#include "ProfileManager.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Konsole #include "EditProfileDialog.h" #include "CopyInputDialog.h" #include "Emulation.h" #include "Filter.h" #include "History.h" #include "HistorySizeDialog.h" #include "IncrementalSearchBar.h" #include "RenameTabDialog.h" #include "ScreenWindow.h" #include "Session.h" #include "ProfileList.h" #include "TerminalDisplay.h" #include "SessionManager.h" #include "Enumeration.h" #include "PrintOptions.h" // for SaveHistoryTask #include #include #include "TerminalCharacterDecoder.h" // For Unix signal names #include using namespace Konsole; // TODO - Replace the icon choices below when suitable icons for silence and // activity are available Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _activityIcon, (QIcon::fromTheme("dialog-information"))); Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _silenceIcon, (QIcon::fromTheme("dialog-information"))); Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _broadcastIcon, (QIcon::fromTheme("emblem-important"))); QSet SessionController::_allControllers; int SessionController::_lastControllerId; SessionController::SessionController(Session* session , TerminalDisplay* view, QObject* parent) : ViewProperties(parent) , KXMLGUIClient() , _session(session) , _view(view) , _copyToGroup(0) , _profileList(0) , _previousState(-1) , _searchFilter(0) + , _urlFilter(0) + , _fileFilter(0) , _copyInputToAllTabsAction(0) , _findAction(0) , _findNextAction(0) , _findPreviousAction(0) , _searchStartLine(0) , _prevSearchResultLine(0) , _searchBar(0) , _codecAction(0) , _switchProfileMenu(0) , _webSearchMenu(0) , _listenForScreenWindowUpdates(false) , _preventClose(false) , _keepIconUntilInteraction(false) , _showMenuAction(0) , _isSearchBarEnabled(false) { Q_ASSERT(session); Q_ASSERT(view); // handle user interface related to session (menus etc.) if (isKonsolePart()) { setComponentName(QStringLiteral("konsole"), i18n("Konsole")); setXMLFile("partui.rc"); setupCommonActions(); } else { setXMLFile("sessionui.rc"); setupCommonActions(); setupExtraActions(); } actionCollection()->addAssociatedWidget(view); foreach(QAction * action, actionCollection()->actions()) { action->setShortcutContext(Qt::WidgetWithChildrenShortcut); } setIdentifier(++_lastControllerId); sessionTitleChanged(); view->installEventFilter(this); view->setSessionController(this); - // install filter on the view to highlight URLs - _view->filterChain()->addFilter(new UrlFilter); + // install filter on the view to highlight URLs and files + updateFilterList(SessionManager::instance()->sessionProfile(_session)); - // install filter on the view to highlight Files - _view->filterChain()->addFilter(new FileFilter(_session)); + // listen for changes in session, we might need to change the enabled filters + connect(ProfileManager::instance(), &Konsole::ProfileManager::profileChanged, this, &Konsole::SessionController::updateFilterList); // listen for session resize requests connect(_session.data(), &Konsole::Session::resizeRequest, this, &Konsole::SessionController::sessionResizeRequest); // listen for popup menu requests connect(_view.data(), &Konsole::TerminalDisplay::configureRequest, this, &Konsole::SessionController::showDisplayContextMenu); // move view to newest output when keystrokes occur connect(_view.data(), &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::SessionController::trackOutput); // listen to activity / silence notifications from session connect(_session.data(), &Konsole::Session::stateChanged, this, &Konsole::SessionController::sessionStateChanged); // listen to title and icon changes connect(_session.data(), &Konsole::Session::titleChanged, this, &Konsole::SessionController::sessionTitleChanged); connect(_session.data() , &Konsole::Session::currentDirectoryChanged , this , &Konsole::SessionController::currentDirectoryChanged); // listen for color changes connect(_session.data(), &Konsole::Session::changeBackgroundColorRequest, _view.data(), &Konsole::TerminalDisplay::setBackgroundColor); connect(_session.data(), &Konsole::Session::changeForegroundColorRequest, _view.data(), &Konsole::TerminalDisplay::setForegroundColor); // update the title when the session starts connect(_session.data(), &Konsole::Session::started, this, &Konsole::SessionController::snapshot); // listen for output changes to set activity flag connect(_session->emulation(), &Konsole::Emulation::outputChanged, this, &Konsole::SessionController::fireActivity); // listen for detection of ZModem transfer connect(_session.data(), &Konsole::Session::zmodemDetected, this, &Konsole::SessionController::zmodemDownload); // listen for flow control status changes connect(_session.data(), &Konsole::Session::flowControlEnabledChanged, _view.data(), &Konsole::TerminalDisplay::setFlowControlWarningEnabled); _view->setFlowControlWarningEnabled(_session->flowControlEnabled()); // take a snapshot of the session state every so often when // user activity occurs // // the timer is owned by the session so that it will be destroyed along // with the session _interactionTimer = new QTimer(_session); _interactionTimer->setSingleShot(true); _interactionTimer->setInterval(500); connect(_interactionTimer, &QTimer::timeout, this, &Konsole::SessionController::snapshot); connect(_view.data(), &Konsole::TerminalDisplay::keyPressedSignal, this, &Konsole::SessionController::interactionHandler); // take a snapshot of the session state periodically in the background QTimer* backgroundTimer = new QTimer(_session); backgroundTimer->setSingleShot(false); backgroundTimer->setInterval(2000); connect(backgroundTimer, &QTimer::timeout, this, &Konsole::SessionController::snapshot); backgroundTimer->start(); // xterm '11;?' request connect(_session.data(), &Konsole::Session::getBackgroundColor, this, &Konsole::SessionController::sendBackgroundColor); _allControllers.insert(this); // A list of programs that accept Ctrl+C to clear command line used // before outputting bookmark. _bookmarkValidProgramsToClear << "bash" << "fish" << "sh"; _bookmarkValidProgramsToClear << "tcsh" << "zsh"; } SessionController::~SessionController() { if (_view) _view->setScreenWindow(0); _allControllers.remove(this); if (!_editProfileDialog.isNull()) { delete _editProfileDialog.data(); } } void SessionController::trackOutput(QKeyEvent* event) { Q_ASSERT(_view->screenWindow()); // Only jump to the bottom if the user actually typed something in, // not if the user e. g. just pressed a modifier. if (event->text().isEmpty() && event->modifiers()) { return; } _view->screenWindow()->setTrackOutput(true); } void SessionController::interactionHandler() { // This flag is used to make sure those special icons indicating interest // events (activity/silence/bell?) remain in the tab until user interaction // happens. Otherwise, those special icons will quickly be replaced by // normal icon when ::snapshot() is triggered _keepIconUntilInteraction = false; _interactionTimer->start(); } void SessionController::snapshot() { Q_ASSERT(_session != 0); QString title = _session->getDynamicTitle(); title = title.simplified(); // Visualize that the session is broadcasting to others if (_copyToGroup && _copyToGroup->sessions().count() > 1) { title.append('*'); } // use the fallback title if needed if (title.isEmpty()) { title = _session->title(Session::NameRole); } // apply new title _session->setTitle(Session::DisplayedTitleRole, title); // do not forget icon updateSessionIcon(); } QString SessionController::currentDir() const { return _session->currentWorkingDirectory(); } QUrl SessionController::url() const { return _session->getUrl(); } void SessionController::rename() { renameSession(); } void SessionController::openUrl(const QUrl& url) { // Clear shell's command line if (!_session->isForegroundProcessActive() && _bookmarkValidProgramsToClear.contains(_session->foregroundProcessName())) { _session->sendTextToTerminal(QChar(0x03), '\n'); // Ctrl+C } // handle local paths if (url.isLocalFile()) { QString path = url.toLocalFile(); _session->sendTextToTerminal("cd " + KShell::quoteArg(path), '\r'); } else if (url.scheme().isEmpty()) { // QUrl couldn't parse what the user entered into the URL field // so just dump it to the shell QString command = url.toDisplayString(); if (!command.isEmpty()) _session->sendTextToTerminal(command, '\r'); } else if (url.scheme() == "ssh") { QString sshCommand = "ssh "; if (url.port() > -1) { sshCommand += QString("-p %1 ").arg(url.port()); } if (!url.userName().isEmpty()) { sshCommand += (url.userName() + '@'); } if (!url.host().isEmpty()) { sshCommand += url.host(); } _session->sendTextToTerminal(sshCommand, '\r'); } else if (url.scheme() == "telnet") { QString telnetCommand = "telnet "; if (!url.userName().isEmpty()) { telnetCommand += QString("-l %1 ").arg(url.userName()); } if (!url.host().isEmpty()) { telnetCommand += (url.host() + ' '); } if (url.port() > -1) { telnetCommand += QString::number(url.port()); } _session->sendTextToTerminal(telnetCommand, '\r'); } else { //TODO Implement handling for other Url types KMessageBox::sorry(_view->window(), i18n("Konsole does not know how to open the bookmark: ") + url.toDisplayString()); qWarning() << "Unable to open bookmark at url" << url << ", I do not know" << " how to handle the protocol " << url.scheme(); } } void SessionController::setupPrimaryScreenSpecificActions(bool use) { KActionCollection* collection = actionCollection(); QAction* clearAction = collection->action("clear-history"); QAction* resetAction = collection->action("clear-history-and-reset"); QAction* selectAllAction = collection->action("select-all"); QAction* selectLineAction = collection->action("select-line"); // these actions are meaningful only when primary screen is used. clearAction->setEnabled(use); resetAction->setEnabled(use); selectAllAction->setEnabled(use); selectLineAction->setEnabled(use); } void SessionController::selectionChanged(const QString& selectedText) { _selectedText = selectedText; updateCopyAction(selectedText); } void SessionController::updateCopyAction(const QString& selectedText) { QAction* copyAction = actionCollection()->action("edit_copy"); // copy action is meaningful only when some text is selected. copyAction->setEnabled(!selectedText.isEmpty()); } void SessionController::updateWebSearchMenu() { // reset _webSearchMenu->setVisible(false); _webSearchMenu->menu()->clear(); if (_selectedText.isEmpty()) return; QString searchText = _selectedText; searchText = searchText.replace('\n', ' ').replace('\r', ' ').simplified(); if (searchText.isEmpty()) return; KUriFilterData filterData(searchText); filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly); if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) { const QStringList searchProviders = filterData.preferredSearchProviders(); if (!searchProviders.isEmpty()) { _webSearchMenu->setText(i18n("Search for '%1' with", KStringHandler::rsqueeze(searchText, 16))); QAction* action = 0; foreach(const QString& searchProvider, searchProviders) { action = new QAction(searchProvider, _webSearchMenu); action->setIcon(QIcon::fromTheme(filterData.iconNameForPreferredSearchProvider(searchProvider))); action->setData(filterData.queryForPreferredSearchProvider(searchProvider)); connect(action, &QAction::triggered, this, &Konsole::SessionController::handleWebShortcutAction); _webSearchMenu->addAction(action); } _webSearchMenu->addSeparator(); action = new QAction(i18n("Configure Web Shortcuts..."), _webSearchMenu); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); connect(action, &QAction::triggered, this, &Konsole::SessionController::configureWebShortcuts); _webSearchMenu->addAction(action); _webSearchMenu->setVisible(true); } } } void SessionController::handleWebShortcutAction() { QAction * action = qobject_cast(sender()); if (!action) return; KUriFilterData filterData(action->data().toString()); if (KUriFilter::self()->filterUri(filterData, QStringList() << "kurisearchfilter")) { const QUrl& url = filterData.uri(); new KRun(url, QApplication::activeWindow()); } } void SessionController::configureWebShortcuts() { KToolInvocation::kdeinitExec("kcmshell5", QStringList() << "webshortcuts"); } void SessionController::sendSignal(QAction* action) { const int signal = action->data().value(); _session->sendSignal(signal); } void SessionController::sendBackgroundColor() { const QColor c = _view->getBackgroundColor(); _session->reportBackgroundColor(c); } bool SessionController::eventFilter(QObject* watched , QEvent* event) { if (watched == _view) { if (event->type() == QEvent::FocusIn) { // notify the world that the view associated with this session has been focused // used by the view manager to update the title of the MainWindow widget containing the view emit focused(this); // when the view is focused, set bell events from the associated session to be delivered // by the focused view // first, disconnect any other views which are listening for bell signals from the session disconnect(_session.data(), &Konsole::Session::bellRequest, 0, 0); // second, connect the newly focused view to listen for the session's bell signal connect(_session.data(), &Konsole::Session::bellRequest, _view.data(), &Konsole::TerminalDisplay::bell); if (_copyInputToAllTabsAction && _copyInputToAllTabsAction->isChecked()) { // A session with "Copy To All Tabs" has come into focus: // Ensure that newly created sessions are included in _copyToGroup! copyInputToAllTabs(); } } } return false; } void SessionController::removeSearchFilter() { if (!_searchFilter) return; _view->filterChain()->removeFilter(_searchFilter); delete _searchFilter; _searchFilter = 0; } void SessionController::setSearchBar(IncrementalSearchBar* searchBar) { // disconnect the existing search bar if (_searchBar) { disconnect(this, 0, _searchBar, 0); disconnect(_searchBar, 0, this, 0); } // connect new search bar _searchBar = searchBar; if (_searchBar) { connect(_searchBar.data(), &Konsole::IncrementalSearchBar::unhandledMovementKeyPressed, this, &Konsole::SessionController::movementKeyFromSearchBarReceived); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::closeClicked, this, &Konsole::SessionController::searchClosed); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchFromClicked, this, &Konsole::SessionController::searchFrom); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::findNextClicked, this, &Konsole::SessionController::findNextInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::findPreviousClicked, this, &Konsole::SessionController::findPreviousInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::highlightMatchesToggled , this , &Konsole::SessionController::highlightMatches); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::matchCaseToggled, this, &Konsole::SessionController::changeSearchMatch); // if the search bar was previously active // then re-enter search mode enableSearchBar(_isSearchBarEnabled); } } IncrementalSearchBar* SessionController::searchBar() const { return _searchBar; } void SessionController::setShowMenuAction(QAction* action) { _showMenuAction = action; } void SessionController::setupCommonActions() { QAction* action = 0; KActionCollection* collection = actionCollection(); // Close Session action = collection->addAction("close-session", this, SLOT(closeSession())); if (isKonsolePart()) action->setText(i18n("&Close Session")); else action->setText(i18n("&Close Tab")); action->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); collection->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_W); // Open Browser action = collection->addAction("open-browser", this, SLOT(openBrowser())); action->setText(i18n("Open File Manager")); action->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager"))); // Copy and Paste action = KStandardAction::copy(this, SLOT(copy()), collection); collection->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_C); // disabled at first, since nothing has been selected now action->setEnabled(false); action = KStandardAction::paste(this, SLOT(paste()), collection); QList pasteShortcut; pasteShortcut.append(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_V)); pasteShortcut.append(QKeySequence(Qt::SHIFT + Qt::Key_Insert)); collection->setDefaultShortcuts(action, pasteShortcut); action = collection->addAction("paste-selection", this, SLOT(pasteFromX11Selection())); action->setText(i18n("Paste Selection")); collection->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_Insert); _webSearchMenu = new KActionMenu(i18n("Web Search"), this); _webSearchMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts"))); _webSearchMenu->setVisible(false); collection->addAction("web-search", _webSearchMenu); action = collection->addAction("select-all", this, SLOT(selectAll())); action->setText(i18n("&Select All")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all"))); action = collection->addAction("select-line", this, SLOT(selectLine())); action->setText(i18n("Select &Line")); action = KStandardAction::saveAs(this, SLOT(saveHistory()), collection); action->setText(i18n("Save Output &As...")); action = KStandardAction::print(this, SLOT(print_screen()), collection); action->setText(i18n("&Print Screen...")); collection->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_P); action = collection->addAction("adjust-history", this, SLOT(showHistoryOptions())); action->setText(i18n("Adjust Scrollback...")); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); action = collection->addAction("clear-history", this, SLOT(clearHistory())); action->setText(i18n("Clear Scrollback")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); action = collection->addAction("clear-history-and-reset", this, SLOT(clearHistoryAndReset())); action->setText(i18n("Clear Scrollback and Reset")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); collection->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_K); // Profile Options action = collection->addAction("edit-current-profile", this, SLOT(editCurrentProfile())); action->setText(i18n("Edit Current Profile...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); _switchProfileMenu = new KActionMenu(i18n("Switch Profile"), this); collection->addAction("switch-profile", _switchProfileMenu); connect(_switchProfileMenu->menu(), &QMenu::aboutToShow, this, &Konsole::SessionController::prepareSwitchProfileMenu); // History _findAction = KStandardAction::find(this, SLOT(searchBarEvent()), collection); collection->setDefaultShortcut(_findAction, QKeySequence()); _findNextAction = KStandardAction::findNext(this, SLOT(findNextInHistory()), collection); collection->setDefaultShortcut(_findNextAction, QKeySequence()); _findNextAction->setEnabled(false); _findPreviousAction = KStandardAction::findPrev(this, SLOT(findPreviousInHistory()), collection); collection->setDefaultShortcut(_findPreviousAction, QKeySequence()); _findPreviousAction->setEnabled(false); // Character Encoding _codecAction = new KCodecAction(i18n("Set &Encoding"), this); _codecAction->setIcon(QIcon::fromTheme(QStringLiteral("character-set"))); collection->addAction("set-encoding", _codecAction); connect(_codecAction->menu(), &QMenu::aboutToShow, this, &Konsole::SessionController::updateCodecAction); connect(_codecAction, static_cast(&KCodecAction::triggered), this, &Konsole::SessionController::changeCodec); } void SessionController::setupExtraActions() { QAction* action = 0; KToggleAction* toggleAction = 0; KActionCollection* collection = actionCollection(); // Rename Session action = collection->addAction("rename-session", this, SLOT(renameSession())); action->setText(i18n("&Rename Tab...")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); collection->setDefaultShortcut(action, Qt::CTRL + Qt::ALT + Qt::Key_S); // Copy input to ==> all tabs KToggleAction* copyInputToAllTabsAction = collection->add("copy-input-to-all-tabs"); copyInputToAllTabsAction->setText(i18n("&All Tabs in Current Window")); copyInputToAllTabsAction->setData(CopyInputToAllTabsMode); // this action is also used in other place, so remember it _copyInputToAllTabsAction = copyInputToAllTabsAction; // Copy input to ==> selected tabs KToggleAction* copyInputToSelectedTabsAction = collection->add("copy-input-to-selected-tabs"); copyInputToSelectedTabsAction->setText(i18n("&Select Tabs...")); collection->setDefaultShortcut(copyInputToSelectedTabsAction, Qt::CTRL + Qt::SHIFT + Qt::Key_Period); copyInputToSelectedTabsAction->setData(CopyInputToSelectedTabsMode); // Copy input to ==> none KToggleAction* copyInputToNoneAction = collection->add("copy-input-to-none"); copyInputToNoneAction->setText(i18nc("@action:inmenu Do not select any tabs", "&None")); collection->setDefaultShortcut(copyInputToNoneAction, Qt::CTRL + Qt::SHIFT + Qt::Key_Slash); copyInputToNoneAction->setData(CopyInputToNoneMode); copyInputToNoneAction->setChecked(true); // the default state // The "Copy Input To" submenu // The above three choices are represented as combo boxes KSelectAction* copyInputActions = collection->add("copy-input-to"); copyInputActions->setText(i18n("Copy Input To")); copyInputActions->addAction(copyInputToAllTabsAction); copyInputActions->addAction(copyInputToSelectedTabsAction); copyInputActions->addAction(copyInputToNoneAction); connect(copyInputActions, static_cast(&KSelectAction::triggered), this, &Konsole::SessionController::copyInputActionsTriggered); action = collection->addAction("zmodem-upload", this, SLOT(zmodemUpload())); action->setText(i18n("&ZModem Upload...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); collection->setDefaultShortcut(action, Qt::CTRL + Qt::ALT + Qt::Key_U); // Monitor toggleAction = new KToggleAction(i18n("Monitor for &Activity"), this); collection->setDefaultShortcut(toggleAction, Qt::CTRL + Qt::SHIFT + Qt::Key_A); action = collection->addAction("monitor-activity", toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorActivity); toggleAction = new KToggleAction(i18n("Monitor for &Silence"), this); collection->setDefaultShortcut(toggleAction, Qt::CTRL + Qt::SHIFT + Qt::Key_I); action = collection->addAction("monitor-silence", toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorSilence); // Text Size action = collection->addAction("enlarge-font", this, SLOT(increaseFontSize())); action->setText(i18n("Enlarge Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-more"))); QList enlargeFontShortcut; enlargeFontShortcut.append(QKeySequence(Qt::CTRL + Qt::Key_Plus)); enlargeFontShortcut.append(QKeySequence(Qt::CTRL + Qt::Key_Equal)); collection->setDefaultShortcuts(action, enlargeFontShortcut); action = collection->addAction("shrink-font", this, SLOT(decreaseFontSize())); action->setText(i18n("Shrink Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-less"))); collection->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Minus); // Send signal KSelectAction* sendSignalActions = collection->add("send-signal"); sendSignalActions->setText(i18n("Send Signal")); connect(sendSignalActions, static_cast(&KSelectAction::triggered), this, &Konsole::SessionController::sendSignal); action = collection->addAction("sigstop-signal"); action->setText(i18n("&Suspend Task") + " (STOP)"); action->setData(SIGSTOP); sendSignalActions->addAction(action); action = collection->addAction("sigcont-signal"); action->setText(i18n("&Continue Task") + " (CONT)"); action->setData(SIGCONT); sendSignalActions->addAction(action); action = collection->addAction("sighup-signal"); action->setText(i18n("&Hangup") + " (HUP)"); action->setData(SIGHUP); sendSignalActions->addAction(action); action = collection->addAction("sigint-signal"); action->setText(i18n("&Interrupt Task") + " (INT)"); action->setData(SIGINT); sendSignalActions->addAction(action); action = collection->addAction("sigterm-signal"); action->setText(i18n("&Terminate Task") + " (TERM)"); action->setData(SIGTERM); sendSignalActions->addAction(action); action = collection->addAction("sigkill-signal"); action->setText(i18n("&Kill Task") + " (KILL)"); action->setData(SIGKILL); sendSignalActions->addAction(action); action = collection->addAction("sigusr1-signal"); action->setText(i18n("User Signal &1") + " (USR1)"); action->setData(SIGUSR1); sendSignalActions->addAction(action); action = collection->addAction("sigusr2-signal"); action->setText(i18n("User Signal &2") + " (USR2)"); action->setData(SIGUSR2); sendSignalActions->addAction(action); collection->setDefaultShortcut(_findAction, Qt::CTRL + Qt::SHIFT + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::Key_F3); collection->setDefaultShortcut(_findPreviousAction, Qt::SHIFT + Qt::Key_F3); } void SessionController::switchProfile(Profile::Ptr profile) { SessionManager::instance()->setSessionProfile(_session, profile); + updateFilterList(profile); } void SessionController::prepareSwitchProfileMenu() { if (_switchProfileMenu->menu()->isEmpty()) { _profileList = new ProfileList(false, this); connect(_profileList, &Konsole::ProfileList::profileSelected, this, &Konsole::SessionController::switchProfile); } _switchProfileMenu->menu()->clear(); _switchProfileMenu->menu()->addActions(_profileList->actions()); } void SessionController::updateCodecAction() { _codecAction->setCurrentCodec(QString(_session->codec())); } void SessionController::changeCodec(QTextCodec* codec) { _session->setCodec(codec); } EditProfileDialog* SessionController::profileDialogPointer() { return _editProfileDialog.data(); } void SessionController::editCurrentProfile() { // Searching for Edit profile dialog opened with the same profile foreach (SessionController* session, _allControllers.values()) { if (session->profileDialogPointer() && session->profileDialogPointer()->isVisible() && session->profileDialogPointer()->lookupProfile() == SessionManager::instance()->sessionProfile(_session)) { session->profileDialogPointer()->close(); } } // NOTE bug311270: For to prevent the crash, the profile must be reset. if (!_editProfileDialog.isNull()) { // exists but not visible delete _editProfileDialog.data(); } _editProfileDialog = new EditProfileDialog(QApplication::activeWindow()); _editProfileDialog.data()->setProfile(SessionManager::instance()->sessionProfile(_session)); _editProfileDialog.data()->show(); } void SessionController::renameSession() { QScopedPointer dialog(new RenameTabDialog(QApplication::activeWindow())); dialog->setTabTitleText(_session->tabTitleFormat(Session::LocalTabTitle)); dialog->setRemoteTabTitleText(_session->tabTitleFormat(Session::RemoteTabTitle)); if (_session->isRemote()) { dialog->focusRemoteTabTitleText(); } else { dialog->focusTabTitleText(); } QPointer guard(_session); int result = dialog->exec(); if (!guard) return; if (result) { QString tabTitle = dialog->tabTitleText(); QString remoteTabTitle = dialog->remoteTabTitleText(); _session->setTabTitleFormat(Session::LocalTabTitle, tabTitle); _session->setTabTitleFormat(Session::RemoteTabTitle, remoteTabTitle); // trigger an update of the tab text snapshot(); } } bool SessionController::confirmClose() const { if (_session->isForegroundProcessActive()) { QString title = _session->foregroundProcessName(); // hard coded for now. In future make it possible for the user to specify which programs // are ignored when considering whether to display a confirmation QStringList ignoreList; ignoreList << QString(qgetenv("SHELL")).section('/', -1); if (ignoreList.contains(title)) return true; QString question; if (title.isEmpty()) question = i18n("A program is currently running in this session." " Are you sure you want to close it?"); else question = i18n("The program '%1' is currently running in this session." " Are you sure you want to close it?", title); int result = KMessageBox::warningYesNo(_view->window(), question, i18n("Confirm Close")); return (result == KMessageBox::Yes) ? true : false; } return true; } bool SessionController::confirmForceClose() const { if (_session->isRunning()) { QString title = _session->program(); // hard coded for now. In future make it possible for the user to specify which programs // are ignored when considering whether to display a confirmation QStringList ignoreList; ignoreList << QString(qgetenv("SHELL")).section('/', -1); if (ignoreList.contains(title)) return true; QString question; if (title.isEmpty()) question = i18n("A program in this session would not die." " Are you sure you want to kill it by force?"); else question = i18n("The program '%1' is in this session would not die." " Are you sure you want to kill it by force?", title); int result = KMessageBox::warningYesNo(_view->window(), question, i18n("Confirm Close")); return (result == KMessageBox::Yes) ? true : false; } return true; } void SessionController::closeSession() { if (_preventClose) return; if (confirmClose()) { if (_session->closeInNormalWay()) { return; } else if (confirmForceClose()) { if (_session->closeInForceWay()) return; else qWarning() << "Konsole failed to close a session in any way."; } } } // Trying to open a remote Url may produce unexpected results. // Therefore, if a remote url, open the user's home path. // TODO consider: 1) disable menu upon remote session // 2) transform url to get the desired result (ssh -> sftp, etc) void SessionController::openBrowser() { const QUrl currentUrl = url(); if (currentUrl.isLocalFile()) { new KRun(currentUrl, QApplication::activeWindow(), true); } else { new KRun(QUrl::fromLocalFile(QDir::homePath()), QApplication::activeWindow(), true); } } void SessionController::copy() { _view->copyToClipboard(); } void SessionController::paste() { _view->pasteFromClipboard(); } void SessionController::pasteFromX11Selection() { _view->pasteFromX11Selection(); } void SessionController::selectAll() { ScreenWindow * screenWindow = _view->screenWindow(); screenWindow->setSelectionByLineRange(0, _session->emulation()->lineCount()); _view->copyToX11Selection(); } void SessionController::selectLine() { _view->selectCurrentLine(); } static const KXmlGuiWindow* findWindow(const QObject* object) { // Walk up the QObject hierarchy to find a KXmlGuiWindow. while (object != 0) { const KXmlGuiWindow* window = qobject_cast(object); if (window != 0) { return(window); } object = object->parent(); } return(0); } static bool hasTerminalDisplayInSameWindow(const Session* session, const KXmlGuiWindow* window) { // Iterate all TerminalDisplays of this Session ... foreach(const TerminalDisplay* terminalDisplay, session->views()) { // ... and check whether a TerminalDisplay has the same // window as given in the parameter if (window == findWindow(terminalDisplay)) { return(true); } } return(false); } void SessionController::copyInputActionsTriggered(QAction* action) { const int mode = action->data().value(); switch (mode) { case CopyInputToAllTabsMode: copyInputToAllTabs(); break; case CopyInputToSelectedTabsMode: copyInputToSelectedTabs(); break; case CopyInputToNoneMode: copyInputToNone(); break; default: Q_ASSERT(false); } } void SessionController::copyInputToAllTabs() { if (!_copyToGroup) { _copyToGroup = new SessionGroup(this); } // Find our window ... const KXmlGuiWindow* myWindow = findWindow(_view); QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (QSet::iterator iterator = group.begin(); iterator != group.end(); ++iterator) { Session* session = *iterator; // First, ensure that the session is removed // (necessary to avoid duplicates on addSession()!) _copyToGroup->removeSession(session); // Add current session if it is displayed our window if (hasTerminalDisplayInSameWindow(session, myWindow)) { _copyToGroup->addSession(session); } } _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); snapshot(); } void SessionController::copyInputToSelectedTabs() { if (!_copyToGroup) { _copyToGroup = new SessionGroup(this); _copyToGroup->addSession(_session); _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); } QPointer dialog = new CopyInputDialog(_view); dialog->setMasterSession(_session); QSet currentGroup = QSet::fromList(_copyToGroup->sessions()); currentGroup.remove(_session); dialog->setChosenSessions(currentGroup); QPointer guard(_session); int result = dialog->exec(); if (!guard) return; if (result == QDialog::Accepted) { QSet newGroup = dialog->chosenSessions(); newGroup.remove(_session); QSet completeGroup = newGroup | currentGroup; foreach(Session * session, completeGroup) { if (newGroup.contains(session) && !currentGroup.contains(session)) _copyToGroup->addSession(session); else if (!newGroup.contains(session) && currentGroup.contains(session)) _copyToGroup->removeSession(session); } _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); snapshot(); } } void SessionController::copyInputToNone() { if (!_copyToGroup) // No 'Copy To' is active return; QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (QSet::iterator iterator = group.begin(); iterator != group.end(); ++iterator) { Session* session = *iterator; if (session != _session) { _copyToGroup->removeSession(*iterator); } } delete _copyToGroup; _copyToGroup = 0; snapshot(); } void SessionController::searchClosed() { _isSearchBarEnabled = false; searchHistory(false); } +void SessionController::updateFilterList(Profile::Ptr profile) +{ + if (profile != SessionManager::instance()->sessionProfile(_session)) { + return; + } + + bool underlineFiles = profile->underlineFilesEnabled(); + + if (!underlineFiles && _fileFilter) { + _view->filterChain()->removeFilter(_fileFilter); + delete _fileFilter; + _fileFilter = nullptr; + } else if (underlineFiles && !_fileFilter) { + _fileFilter = new FileFilter(_session); + _view->filterChain()->addFilter(_fileFilter); + } + + bool underlineLinks = profile->underlineLinksEnabled(); + if (!underlineLinks && _urlFilter) { + _view->filterChain()->removeFilter(_urlFilter); + delete _urlFilter; + _urlFilter = nullptr; + } else if (underlineLinks && !_urlFilter) { + _urlFilter = new UrlFilter(); + _view->filterChain()->addFilter(_urlFilter); + } +} + void SessionController::setSearchStartToWindowCurrentLine() { setSearchStartTo(-1); } void SessionController::setSearchStartTo(int line) { _searchStartLine = line; _prevSearchResultLine = line; } void SessionController::listenForScreenWindowUpdates() { if (_listenForScreenWindowUpdates) return; connect(_view->screenWindow(), &Konsole::ScreenWindow::outputChanged, this, &Konsole::SessionController::updateSearchFilter); connect(_view->screenWindow(), &Konsole::ScreenWindow::scrolled, this, &Konsole::SessionController::updateSearchFilter); connect(_view->screenWindow(), &Konsole::ScreenWindow::currentResultLineChanged, _view.data(), static_cast(&Konsole::TerminalDisplay::update)); _listenForScreenWindowUpdates = true; } void SessionController::updateSearchFilter() { if (_searchFilter && _searchBar) { _view->processFilters(); } } void SessionController::searchBarEvent() { QString selectedText = _view->screenWindow()->selectedText(true, true); if (!selectedText.isEmpty()) _searchBar->setSearchText(selectedText); if (_searchBar->isVisible()) { _searchBar->focusLineEdit(); } else { searchHistory(true); _isSearchBarEnabled = true; } } void SessionController::enableSearchBar(bool showSearchBar) { if (!_searchBar) return; if (showSearchBar && !_searchBar->isVisible()) { setSearchStartToWindowCurrentLine(); } _searchBar->setVisible(showSearchBar); if (showSearchBar) { connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchChanged, this, &Konsole::SessionController::searchTextChanged); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchReturnPressed, this, &Konsole::SessionController::findPreviousInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchShiftPlusReturnPressed, this, &Konsole::SessionController::findNextInHistory); } else { disconnect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchChanged, this, &Konsole::SessionController::searchTextChanged); disconnect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchReturnPressed, this, &Konsole::SessionController::findPreviousInHistory); disconnect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchShiftPlusReturnPressed, this, &Konsole::SessionController::findNextInHistory); if (_view && _view->screenWindow()) { _view->screenWindow()->setCurrentResultLine(-1); } } } bool SessionController::reverseSearchChecked() const { Q_ASSERT(_searchBar); QBitArray options = _searchBar->optionsChecked(); return options.at(IncrementalSearchBar::ReverseSearch); } QRegularExpression SessionController::regexpFromSearchBarOptions() const { QBitArray options = _searchBar->optionsChecked(); QString text(_searchBar->searchText()); QRegularExpression regExp; if (options.at(IncrementalSearchBar::RegExp)) { regExp.setPattern(text); } else { regExp.setPattern(QRegularExpression::escape(text)); } if (!options.at(IncrementalSearchBar::MatchCase)) { regExp.setPatternOptions(QRegularExpression::CaseInsensitiveOption); } return regExp; } // searchHistory() may be called either as a result of clicking a menu item or // as a result of changing the search bar widget void SessionController::searchHistory(bool showSearchBar) { enableSearchBar(showSearchBar); if (_searchBar) { if (showSearchBar) { removeSearchFilter(); listenForScreenWindowUpdates(); _searchFilter = new RegExpFilter(); _searchFilter->setRegExp(regexpFromSearchBarOptions()); _view->filterChain()->addFilter(_searchFilter); _view->processFilters(); setFindNextPrevEnabled(true); } else { setFindNextPrevEnabled(false); removeSearchFilter(); _view->setFocus(Qt::ActiveWindowFocusReason); } } } void SessionController::setFindNextPrevEnabled(bool enabled) { _findNextAction->setEnabled(enabled); _findPreviousAction->setEnabled(enabled); } void SessionController::searchTextChanged(const QString& text) { Q_ASSERT(_view->screenWindow()); if (_searchText == text) return; _searchText = text; if (text.isEmpty()) { _view->screenWindow()->clearSelection(); _view->screenWindow()->scrollTo(_searchStartLine); } // update search. this is called even when the text is // empty to clear the view's filters beginSearch(text , reverseSearchChecked() ? SearchHistoryTask::BackwardsSearch : SearchHistoryTask::ForwardsSearch); } void SessionController::searchCompleted(bool success) { _prevSearchResultLine = _view->screenWindow()->currentResultLine(); if (_searchBar) _searchBar->setFoundMatch(success); } void SessionController::beginSearch(const QString& text , int direction) { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); QRegularExpression regExp = regexpFromSearchBarOptions(); _searchFilter->setRegExp(regExp); if (_searchStartLine == -1) { if (direction == SearchHistoryTask::ForwardsSearch) { setSearchStartTo(_view->screenWindow()->currentLine()); } else { setSearchStartTo(_view->screenWindow()->currentLine() + _view->screenWindow()->windowLines()); } } if (!regExp.pattern().isEmpty()) { _view->screenWindow()->setCurrentResultLine(-1); SearchHistoryTask* task = new SearchHistoryTask(this); connect(task, &Konsole::SearchHistoryTask::completed, this, &Konsole::SessionController::searchCompleted); task->setRegExp(regExp); task->setSearchDirection((SearchHistoryTask::SearchDirection)direction); task->setAutoDelete(true); task->setStartLine(_searchStartLine); task->addScreenWindow(_session , _view->screenWindow()); task->execute(); } else if (text.isEmpty()) { searchCompleted(false); } _view->processFilters(); } void SessionController::highlightMatches(bool highlight) { if (highlight) { _view->filterChain()->addFilter(_searchFilter); _view->processFilters(); } else { _view->filterChain()->removeFilter(_searchFilter); } _view->update(); } void SessionController::searchFrom() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); if (reverseSearchChecked()) { setSearchStartTo(_view->screenWindow()->lineCount()); } else { setSearchStartTo(0); } beginSearch(_searchBar->searchText(), reverseSearchChecked() ? SearchHistoryTask::BackwardsSearch : SearchHistoryTask::ForwardsSearch); } void SessionController::findNextInHistory() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); setSearchStartTo(_prevSearchResultLine); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? SearchHistoryTask::BackwardsSearch : SearchHistoryTask::ForwardsSearch); } void SessionController::findPreviousInHistory() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); setSearchStartTo(_prevSearchResultLine); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? SearchHistoryTask::ForwardsSearch : SearchHistoryTask::BackwardsSearch); } void SessionController::changeSearchMatch() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); // reset Selection for new case match _view->screenWindow()->clearSelection(); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? SearchHistoryTask::BackwardsSearch : SearchHistoryTask::ForwardsSearch); } void SessionController::showHistoryOptions() { QScopedPointer dialog(new HistorySizeDialog(QApplication::activeWindow())); const HistoryType& currentHistory = _session->historyType(); if (currentHistory.isEnabled()) { if (currentHistory.isUnlimited()) { dialog->setMode(Enum::UnlimitedHistory); } else { dialog->setMode(Enum::FixedSizeHistory); dialog->setLineCount(currentHistory.maximumLineCount()); } } else { dialog->setMode(Enum::NoHistory); } QPointer guard(_session); int result = dialog->exec(); if (!guard) return; if (result) { scrollBackOptionsChanged(dialog->mode(), dialog->lineCount()); } } void SessionController::sessionResizeRequest(const QSize& size) { ////qDebug() << "View resize requested to " << size; _view->setSize(size.width(), size.height()); } void SessionController::scrollBackOptionsChanged(int mode, int lines) { switch (mode) { case Enum::NoHistory: _session->setHistoryType(HistoryTypeNone()); break; case Enum::FixedSizeHistory: _session->setHistoryType(CompactHistoryType(lines)); break; case Enum::UnlimitedHistory: _session->setHistoryType(HistoryTypeFile()); break; } } void SessionController::print_screen() { QPrinter printer; QPointer dialog = new QPrintDialog(&printer, _view); PrintOptions* options = new PrintOptions(); dialog->setOptionTabs(QList() << options); dialog->setWindowTitle(i18n("Print Shell")); connect(dialog.data(), static_cast(&QPrintDialog::accepted), options, &Konsole::PrintOptions::saveSettings); if (dialog->exec() != QDialog::Accepted) return; QPainter painter; painter.begin(&printer); KConfigGroup configGroup(KSharedConfig::openConfig(), "PrintOptions"); if (configGroup.readEntry("ScaleOutput", true)) { double scale = qMin(printer.pageRect().width() / static_cast(_view->width()), printer.pageRect().height() / static_cast(_view->height())); painter.scale(scale, scale); } _view->printContent(painter, configGroup.readEntry("PrinterFriendly", true)); } void SessionController::saveHistory() { SessionTask* task = new SaveHistoryTask(this); task->setAutoDelete(true); task->addSession(_session); task->execute(); } void SessionController::clearHistory() { _session->clearHistory(); _view->updateImage(); // To reset view scrollbar } void SessionController::clearHistoryAndReset() { Profile::Ptr profile = SessionManager::instance()->sessionProfile(_session); QByteArray name = profile->defaultEncoding().toUtf8(); Emulation* emulation = _session->emulation(); emulation->reset(); _session->refresh(); _session->setCodec(QTextCodec::codecForName(name)); clearHistory(); } void SessionController::increaseFontSize() { _view->increaseFontSize(); } void SessionController::decreaseFontSize() { _view->decreaseFontSize(); } void SessionController::monitorActivity(bool monitor) { _session->setMonitorActivity(monitor); } void SessionController::monitorSilence(bool monitor) { _session->setMonitorSilence(monitor); } void SessionController::updateSessionIcon() { // Visualize that the session is broadcasting to others if (_copyToGroup && _copyToGroup->sessions().count() > 1) { // Master Mode: set different icon, to warn the user to be careful setIcon(*_broadcastIcon); } else { if (!_keepIconUntilInteraction) { // Not in Master Mode: use normal icon setIcon(_sessionIcon); } } } void SessionController::sessionTitleChanged() { if (_sessionIconName != _session->iconName()) { _sessionIconName = _session->iconName(); _sessionIcon = QIcon::fromTheme(_sessionIconName); updateSessionIcon(); } QString title = _session->title(Session::DisplayedTitleRole); // special handling for the "%w" marker which is replaced with the // window title set by the shell title.replace("%w", _session->userTitle()); // special handling for the "%#" marker which is replaced with the // number of the shell title.replace("%#", QString::number(_session->sessionId())); if (title.isEmpty()) title = _session->title(Session::NameRole); setTitle(title); emit rawTitleChanged(); } void SessionController::showDisplayContextMenu(const QPoint& position) { // needed to make sure the popup menu is available, even if a hosting // application did not merge our GUI. if (!factory()) { if (!clientBuilder()) { setClientBuilder(new KXMLGUIBuilder(_view)); } KXMLGUIFactory* factory = new KXMLGUIFactory(clientBuilder(), this); factory->addClient(this); ////qDebug() << "Created xmlgui factory" << factory; } QPointer popup = qobject_cast(factory()->container("session-popup-menu", this)); if (popup) { // prepend content-specific actions such as "Open Link", "Copy Email Address" etc. QList contentActions = _view->filterActions(position); QAction* contentSeparator = new QAction(popup); contentSeparator->setSeparator(true); contentActions << contentSeparator; popup->insertActions(popup->actions().value(0, 0), contentActions); // always update this submenu before showing the context menu, // because the available search services might have changed // since the context menu is shown last time updateWebSearchMenu(); _preventClose = true; if (_showMenuAction) { if ( _showMenuAction->isChecked() ) { popup->removeAction( _showMenuAction); } else { popup->insertAction(_switchProfileMenu, _showMenuAction); } } QAction* chosen = popup->exec(_view->mapToGlobal(position)); // check for validity of the pointer to the popup menu if (popup) { // Remove content-specific actions // // If the close action was chosen, the popup menu will be partially // destroyed at this point, and the rest will be destroyed later by // 'chosen->trigger()' foreach(QAction * action, contentActions) { popup->removeAction(action); } delete contentSeparator; } _preventClose = false; if (chosen && chosen->objectName() == "close-session") chosen->trigger(); } else { qWarning() << "Unable to display popup menu for session" << _session->title(Session::NameRole) << ", no GUI factory available to build the popup."; } } void SessionController::movementKeyFromSearchBarReceived(QKeyEvent *event) { QCoreApplication::sendEvent(_view, event); setSearchStartToWindowCurrentLine(); } void SessionController::sessionStateChanged(int state) { if (state == _previousState) return; if (state == NOTIFYACTIVITY) { setIcon(*_activityIcon); _keepIconUntilInteraction = true; } else if (state == NOTIFYSILENCE) { setIcon(*_silenceIcon); _keepIconUntilInteraction = true; } else if (state == NOTIFYNORMAL) { if (_sessionIconName != _session->iconName()) { _sessionIconName = _session->iconName(); _sessionIcon = QIcon::fromTheme(_sessionIconName); } updateSessionIcon(); } _previousState = state; } void SessionController::zmodemDownload() { QString zmodem = QStandardPaths::findExecutable("rz"); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable("lrz"); } if (!zmodem.isEmpty()) { const QString path = QFileDialog::getExistingDirectory(_view, i18n("Save ZModem Download to..."), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (!path.isEmpty()) { _session->startZModem(zmodem, path, QStringList()); return; } } else { KMessageBox::error(_view, i18n("

A ZModem file transfer attempt has been detected, " "but no suitable ZModem software was found on this system.

" "

You may wish to install the 'rzsz' or 'lrzsz' package.

")); } _session->cancelZModem(); return; } void SessionController::zmodemUpload() { if (_session->isZModemBusy()) { KMessageBox::sorry(_view, i18n("

The current session already has a ZModem file transfer in progress.

")); return; } QString zmodem = QStandardPaths::findExecutable("sz"); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable("lsz"); } if (zmodem.isEmpty()) { KMessageBox::sorry(_view, i18n("

No suitable ZModem software was found on this system.

" "

You may wish to install the 'rzsz' or 'lrzsz' package.

")); return; } QStringList files = QFileDialog::getOpenFileNames(_view, i18n("Select Files for ZModem Upload"), QDir::homePath()); if (!files.isEmpty()) { _session->startZModem(zmodem, QString(), files); } } bool SessionController::isKonsolePart() const { // Check to see if we are being called from Konsole or a KPart if (qApp->applicationName() == "konsole") { return false; } else { return true; } } SessionTask::SessionTask(QObject* parent) : QObject(parent) , _autoDelete(false) { } void SessionTask::setAutoDelete(bool enable) { _autoDelete = enable; } bool SessionTask::autoDelete() const { return _autoDelete; } void SessionTask::addSession(Session* session) { _sessions << session; } QList SessionTask::sessions() const { return _sessions; } SaveHistoryTask::SaveHistoryTask(QObject* parent) : SessionTask(parent) { } SaveHistoryTask::~SaveHistoryTask() { } void SaveHistoryTask::execute() { // TODO - think about the UI when saving multiple history sessions, if there are more than two or // three then providing a URL for each one will be tedious // TODO - show a warning ( preferably passive ) if saving the history output fails QFileDialog* dialog = new QFileDialog(QApplication::activeWindow(), QString(), QDir::homePath()); dialog->setAcceptMode(QFileDialog::AcceptSave); QStringList mimeTypes; mimeTypes << "text/plain"; mimeTypes << "text/html"; dialog->setMimeTypeFilters(mimeTypes); // iterate over each session in the task and display a dialog to allow the user to choose where // to save that session's history. // then start a KIO job to transfer the data from the history to the chosen URL foreach(const SessionPtr& session, sessions()) { dialog->setWindowTitle(i18n("Save Output From %1", session->title(Session::NameRole))); int result = dialog->exec(); if (result != QDialog::Accepted) continue; QUrl url = (dialog->selectedUrls()).at(0); if (!url.isValid()) { // UI: Can we make this friendlier? KMessageBox::sorry(0 , i18n("%1 is an invalid URL, the output could not be saved.", url.url())); continue; } KIO::TransferJob* job = KIO::put(url, -1, // no special permissions // overwrite existing files // do not resume an existing transfer // show progress information only for remote // URLs KIO::Overwrite | (url.isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags) // a better solution would be to show progress // information after a certain period of time // instead, since the overall speed of transfer // depends on factors other than just the protocol // used ); SaveJob jobInfo; jobInfo.session = session; jobInfo.lastLineFetched = -1; // when each request for data comes in from the KIO subsystem // lastLineFetched is used to keep track of how much of the history // has already been sent, and where the next request should continue // from. // this is set to -1 to indicate the job has just been started if ((dialog->selectedNameFilter()).contains("html", Qt::CaseInsensitive)) { jobInfo.decoder = new HTMLDecoder(); } else { jobInfo.decoder = new PlainTextDecoder(); } _jobSession.insert(job, jobInfo); connect(job, &KIO::TransferJob::dataReq, this, &Konsole::SaveHistoryTask::jobDataRequested); connect(job, &KIO::TransferJob::result, this, &Konsole::SaveHistoryTask::jobResult); } dialog->deleteLater(); } void SaveHistoryTask::jobDataRequested(KIO::Job* job , QByteArray& data) { // TODO - Report progress information for the job // PERFORMANCE: Do some tests and tweak this value to get faster saving const int LINES_PER_REQUEST = 500; SaveJob& info = _jobSession[job]; // transfer LINES_PER_REQUEST lines from the session's history // to the save location if (info.session) { // note: when retrieving lines from the emulation, // the first line is at index 0. int sessionLines = info.session->emulation()->lineCount(); if (sessionLines - 1 == info.lastLineFetched) return; // if there is no more data to transfer then stop the job int copyUpToLine = qMin(info.lastLineFetched + LINES_PER_REQUEST , sessionLines - 1); QTextStream stream(&data, QIODevice::ReadWrite); info.decoder->begin(&stream); info.session->emulation()->writeToStream(info.decoder , info.lastLineFetched + 1 , copyUpToLine); info.decoder->end(); info.lastLineFetched = copyUpToLine; } } void SaveHistoryTask::jobResult(KJob* job) { if (job->error()) { KMessageBox::sorry(0 , i18n("A problem occurred when saving the output.\n%1", job->errorString())); } TerminalCharacterDecoder * decoder = _jobSession[job].decoder; _jobSession.remove(job); delete decoder; // notify the world that the task is done emit completed(true); if (autoDelete()) deleteLater(); } void SearchHistoryTask::addScreenWindow(Session* session , ScreenWindow* searchWindow) { _windows.insert(session, searchWindow); } void SearchHistoryTask::execute() { QMapIterator< SessionPtr , ScreenWindowPtr > iter(_windows); while (iter.hasNext()) { iter.next(); executeOnScreenWindow(iter.key() , iter.value()); } } void SearchHistoryTask::executeOnScreenWindow(SessionPtr session , ScreenWindowPtr window) { Q_ASSERT(session); Q_ASSERT(window); Emulation* emulation = session->emulation(); if (!_regExp.pattern().isEmpty()) { int pos = -1; const bool forwards = (_direction == ForwardsSearch); const int lastLine = window->lineCount() - 1; int startLine; if (forwards && (_startLine == lastLine)) { startLine = 0; } else if (!forwards && (_startLine == 0)) { startLine = lastLine; } else { startLine = _startLine + (forwards ? 1 : -1); } QString string; //text stream to read history into string for pattern or regular expression searching QTextStream searchStream(&string); PlainTextDecoder decoder; decoder.setRecordLinePositions(true); //setup first and last lines depending on search direction int line = startLine; //read through and search history in blocks of 10K lines. //this balances the need to retrieve lots of data from the history each time //(for efficient searching) //without using silly amounts of memory if the history is very large. const int maxDelta = qMin(window->lineCount(), 10000); int delta = forwards ? maxDelta : -maxDelta; int endLine = line; bool hasWrapped = false; // set to true when we reach the top/bottom // of the output and continue from the other // end //loop through history in blocks of lines. do { // ensure that application does not appear to hang // if searching through a lengthy output QApplication::processEvents(); // calculate lines to search in this iteration if (hasWrapped) { if (endLine == lastLine) line = 0; else if (endLine == 0) line = lastLine; endLine += delta; if (forwards) endLine = qMin(startLine , endLine); else endLine = qMax(startLine , endLine); } else { endLine += delta; if (endLine > lastLine) { hasWrapped = true; endLine = lastLine; } else if (endLine < 0) { hasWrapped = true; endLine = 0; } } decoder.begin(&searchStream); emulation->writeToStream(&decoder, qMin(endLine, line) , qMax(endLine, line)); decoder.end(); // line number search below assumes that the buffer ends with a new-line string.append('\n'); if (forwards) pos = string.indexOf(_regExp); else pos = string.lastIndexOf(_regExp); //if a match is found, position the cursor on that line and update the screen if (pos != -1) { int newLines = 0; QList linePositions = decoder.linePositions(); while (newLines < linePositions.count() && linePositions[newLines] <= pos) newLines++; // ignore the new line at the start of the buffer newLines--; int findPos = qMin(line, endLine) + newLines; highlightResult(window, findPos); emit completed(true); return; } //clear the current block of text and move to the next one string.clear(); line = endLine; } while (startLine != endLine); // if no match was found, clear selection to indicate this window->clearSelection(); window->notifyOutputChanged(); } emit completed(false); } void SearchHistoryTask::highlightResult(ScreenWindowPtr window , int findPos) { //work out how many lines into the current block of text the search result was found //- looks a little painful, but it only has to be done once per search. ////qDebug() << "Found result at line " << findPos; //update display to show area of history containing selection if ((findPos < window->currentLine()) || (findPos >= (window->currentLine() + window->windowLines()))) { int centeredScrollPos = findPos - window->windowLines() / 2; if (centeredScrollPos < 0) { centeredScrollPos = 0; } window->scrollTo(centeredScrollPos); } window->setTrackOutput(false); window->notifyOutputChanged(); window->setCurrentResultLine(findPos); } SearchHistoryTask::SearchHistoryTask(QObject* parent) : SessionTask(parent) , _direction(BackwardsSearch) , _startLine(0) { } void SearchHistoryTask::setSearchDirection(SearchDirection direction) { _direction = direction; } void SearchHistoryTask::setStartLine(int line) { _startLine = line; } SearchHistoryTask::SearchDirection SearchHistoryTask::searchDirection() const { return _direction; } void SearchHistoryTask::setRegExp(const QRegularExpression &expression) { _regExp = expression; } QRegularExpression SearchHistoryTask::regExp() const { return _regExp; } QString SessionController::userTitle() const { if (_session) { return _session->userTitle(); } else { return QString(); } } diff --git a/src/SessionController.h b/src/SessionController.h index 3ac06f5b..30f07e45 100644 --- a/src/SessionController.h +++ b/src/SessionController.h @@ -1,537 +1,543 @@ /* Copyright 2006-2008 by Robert Knight Copyright 2009 by Thomas Dreibholz 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. */ #ifndef SESSIONCONTROLLER_H #define SESSIONCONTROLLER_H // Qt #include #include #include #include #include #include // KDE #include // Konsole #include "ViewProperties.h" #include "Profile.h" namespace KIO { class Job; } class QAction; class QTextCodec; class QKeyEvent; class QTimer; class QUrl; class KCodecAction; class KJob; class QAction; class KActionMenu; namespace Konsole { class Session; class SessionGroup; class ScreenWindow; class TerminalDisplay; class IncrementalSearchBar; class ProfileList; class RegExpFilter; +class UrlFilter; +class FileFilter; class EditProfileDialog; // SaveHistoryTask class TerminalCharacterDecoder; typedef QPointer SessionPtr; /** * Provides the menu actions to manipulate a single terminal session and view pair. * The actions provided by this class are defined in the sessionui.rc XML file. * * SessionController monitors the session and provides access to basic information * about the session such as title(), icon() and currentDir(). SessionController * provides notifications of activity in the session via the activity() signal. * * When the controlled view receives the focus, the focused() signal is emitted * with a pointer to the controller. This can be used by main application window * which contains the view to plug the controller's actions into the menu when * the view is focused. */ class KONSOLEPRIVATE_EXPORT SessionController : public ViewProperties , public KXMLGUIClient { Q_OBJECT public: enum CopyInputToEnum { /** Copy keyboard input to all the other tabs in current window */ CopyInputToAllTabsMode = 0 , /** Copy keyboard input to user selected tabs in current window */ CopyInputToSelectedTabsMode = 1 , /** Do not copy keyboard input to other tabs */ CopyInputToNoneMode = 2 }; /** * Constructs a new SessionController which operates on @p session and @p view. */ SessionController(Session* session , TerminalDisplay* view, QObject* parent); ~SessionController(); /** Returns the session associated with this controller */ QPointer session() { return _session; } /** Returns the view associated with this controller */ QPointer view() { return _view; } /** * Returns the "window title" of the associated session. */ QString userTitle() const; /** * Returns true if the controller is valid. * A valid controller is one which has a non-null session() and view(). * * Equivalent to "!session().isNull() && !view().isNull()" */ bool isValid() const; /** Set the start line from which the next search will be done **/ void setSearchStartTo(int line); /** set start line to the first or last line (depending on the reverse search * setting) in the terminal display **/ void setSearchStartToWindowCurrentLine(); /** * Sets the widget used for searches through the session's output. * * When the user clicks on the "Search Output" menu action the @p searchBar 's * show() method will be called. The SessionController will then connect to the search * bar's signals to update the search when the widget's controls are pressed. */ void setSearchBar(IncrementalSearchBar* searchBar); /** * see setSearchBar() */ IncrementalSearchBar* searchBar() const; /** * Sets the action displayed in the session's context menu to hide or * show the menu bar. */ void setShowMenuAction(QAction* action); EditProfileDialog* profileDialogPointer(); // reimplemented virtual QUrl url() const; virtual QString currentDir() const; virtual void rename(); virtual bool confirmClose() const; virtual bool confirmForceClose() const; // Reimplemented to watch for events happening to the view virtual bool eventFilter(QObject* watched , QEvent* event); /** Returns the set of all controllers that exist. */ static QSet allControllers() { return _allControllers; } signals: /** * Emitted when the view associated with the controller is focused. * This can be used by other classes to plug the controller's actions into a window's * menus. */ void focused(SessionController* controller); void rawTitleChanged(); /** * Emitted when the current working directory of the session associated with * the controller is changed. */ void currentDirectoryChanged(const QString& dir); public slots: /** * Issues a command to the session to navigate to the specified URL. * This may not succeed if the foreground program does not understand * the command sent to it ( 'cd path' for local URLs ) or is not * responding to input. * * openUrl() currently supports urls for local paths and those * using the 'ssh' protocol ( eg. "ssh://joebloggs@hostname" ) */ void openUrl(const QUrl& url); /** * update actions which are meaningful only when primary screen is in use. */ void setupPrimaryScreenSpecificActions(bool use); /** * update actions which are closely related with the selected text. */ void selectionChanged(const QString& selectedText); /** * close the associated session. This might involve user interaction for * confirmation. */ void closeSession(); /** Increase font size */ void increaseFontSize(); /** Decrease font size */ void decreaseFontSize(); private slots: // menu item handlers void openBrowser(); void copy(); void paste(); void selectAll(); void selectLine(); void pasteFromX11Selection(); // shortcut only void copyInputActionsTriggered(QAction* action); void copyInputToAllTabs(); void copyInputToSelectedTabs(); void copyInputToNone(); void editCurrentProfile(); void changeCodec(QTextCodec* codec); void enableSearchBar(bool showSearchBar); void searchHistory(bool showSearchBar); void searchBarEvent(); void searchFrom(); void findNextInHistory(); void findPreviousInHistory(); void changeSearchMatch(); void print_screen(); void saveHistory(); void showHistoryOptions(); void clearHistory(); void clearHistoryAndReset(); void monitorActivity(bool monitor); void monitorSilence(bool monitor); void renameSession(); void switchProfile(Profile::Ptr profile); void handleWebShortcutAction(); void configureWebShortcuts(); void sendSignal(QAction* action); void sendBackgroundColor(); // other void prepareSwitchProfileMenu(); void updateCodecAction(); void showDisplayContextMenu(const QPoint& position); void movementKeyFromSearchBarReceived(QKeyEvent *event); void sessionStateChanged(int state); void sessionTitleChanged(); void searchTextChanged(const QString& text); void searchCompleted(bool success); void searchClosed(); // called when the user clicks on the // history search bar's close button + void updateFilterList(Profile::Ptr profile); // Called when the profile has changed, so we might need to change the list of filters + void interactionHandler(); void snapshot(); // called periodically as the user types // to take a snapshot of the state of the // foreground process in the terminal void highlightMatches(bool highlight); void scrollBackOptionsChanged(int mode , int lines); void sessionResizeRequest(const QSize& size); void trackOutput(QKeyEvent* event); // move view to end of current output // when a key press occurs in the // display area void updateSearchFilter(); void zmodemDownload(); void zmodemUpload(); /* Returns true if called within a KPart; false if called within Konsole. */ bool isKonsolePart() const; // update actions related with selected text void updateCopyAction(const QString& selectedText); void updateWebSearchMenu(); private: // begins the search // text - pattern to search for // direction - value from SearchHistoryTask::SearchDirection enum to specify // the search direction void beginSearch(const QString& text , int direction); QRegularExpression regexpFromSearchBarOptions() const; bool reverseSearchChecked() const; void setupCommonActions(); void setupExtraActions(); void removeSearchFilter(); // remove and delete the current search filter if set void setFindNextPrevEnabled(bool enabled); void listenForScreenWindowUpdates(); private: void updateSessionIcon(); QPointer _session; QPointer _view; SessionGroup* _copyToGroup; ProfileList* _profileList; QIcon _sessionIcon; QString _sessionIconName; int _previousState; RegExpFilter* _searchFilter; + UrlFilter* _urlFilter; + FileFilter* _fileFilter; QAction* _copyInputToAllTabsAction; QAction* _findAction; QAction* _findNextAction; QAction* _findPreviousAction; QTimer* _interactionTimer; int _searchStartLine; int _prevSearchResultLine; QPointer _searchBar; KCodecAction* _codecAction; KActionMenu* _switchProfileMenu; KActionMenu* _webSearchMenu; bool _listenForScreenWindowUpdates; bool _preventClose; bool _keepIconUntilInteraction; QString _selectedText; QAction* _showMenuAction; static QSet _allControllers; static int _lastControllerId; QStringList _bookmarkValidProgramsToClear; bool _isSearchBarEnabled; QPointer _editProfileDialog; QString _searchText; }; inline bool SessionController::isValid() const { return !_session.isNull() && !_view.isNull(); } /** * Abstract class representing a task which can be performed on a group of sessions. * * Create a new instance of the appropriate sub-class for the task you want to perform and * call the addSession() method to add each session which needs to be processed. * * Finally, call the execute() method to perform the sub-class specific action on each * of the sessions. */ class SessionTask : public QObject { Q_OBJECT public: explicit SessionTask(QObject* parent = 0); /** * Sets whether the task automatically deletes itself when the task has been finished. * Depending on whether the task operates synchronously or asynchronously, the deletion * may be scheduled immediately after execute() returns or it may happen some time later. */ void setAutoDelete(bool enable); /** Returns true if the task automatically deletes itself. See setAutoDelete() */ bool autoDelete() const; /** Adds a new session to the group */ void addSession(Session* session); /** * Executes the task on each of the sessions in the group. * The completed() signal is emitted when the task is finished, depending on the specific sub-class * execute() may be synchronous or asynchronous */ virtual void execute() = 0; signals: /** * Emitted when the task has completed. * Depending on the task this may occur just before execute() returns, or it * may occur later * * @param success Indicates whether the task completed successfully or not */ void completed(bool success); protected: /** Returns a list of sessions in the group */ QList< SessionPtr > sessions() const; private: bool _autoDelete; QList< SessionPtr > _sessions; }; /** * A task which prompts for a URL for each session and saves that session's output * to the given URL */ class SaveHistoryTask : public SessionTask { Q_OBJECT public: /** Constructs a new task to save session output to URLs */ explicit SaveHistoryTask(QObject* parent = 0); virtual ~SaveHistoryTask(); /** * Opens a save file dialog for each session in the group and begins saving * each session's history to the given URL. * * The data transfer is performed asynchronously and will continue after execute() returns. */ virtual void execute(); private slots: void jobDataRequested(KIO::Job* job , QByteArray& data); void jobResult(KJob* job); private: class SaveJob // structure to keep information needed to process // incoming data requests from jobs { public: SessionPtr session; // the session associated with a history save job int lastLineFetched; // the last line processed in the previous data request // set this to -1 at the start of the save job TerminalCharacterDecoder* decoder; // decoder used to convert terminal characters // into output }; QHash _jobSession; }; //class SearchHistoryThread; /** * A task which searches through the output of sessions for matches for a given regular expression. * SearchHistoryTask operates on ScreenWindow instances rather than sessions added by addSession(). * A screen window can be added to the list to search using addScreenWindow() * * When execute() is called, the search begins in the direction specified by searchDirection(), * starting at the position of the current selection. * * FIXME - This is not a proper implementation of SessionTask, in that it ignores sessions specified * with addSession() * * TODO - Implementation requirements: * May provide progress feedback to the user when searching very large output logs. */ class SearchHistoryTask : public SessionTask { Q_OBJECT public: /** * This enum describes the strategies available for searching through the * session's output. */ enum SearchDirection { /** Searches forwards through the output, starting at the current selection. */ ForwardsSearch, /** Searches backwards through the output, starting at the current selection. */ BackwardsSearch }; /** * Constructs a new search task. */ explicit SearchHistoryTask(QObject* parent = 0); /** Adds a screen window to the list to search when execute() is called. */ void addScreenWindow(Session* session , ScreenWindow* searchWindow); /** Sets the regular expression which is searched for when execute() is called */ void setRegExp(const QRegularExpression ®Exp); /** Returns the regular expression which is searched for when execute() is called */ QRegularExpression regExp() const; /** Specifies the direction to search in when execute() is called. */ void setSearchDirection(SearchDirection direction); /** Returns the current search direction. See setSearchDirection(). */ SearchDirection searchDirection() const; /** The line from which the search will be done **/ void setStartLine(int startLine); /** * Performs a search through the session's history, starting at the position * of the current selection, in the direction specified by setSearchDirection(). * * If it finds a match, the ScreenWindow specified in the constructor is * scrolled to the position where the match occurred and the selection * is set to the matching text. execute() then returns immediately. * * To continue the search looking for further matches, call execute() again. */ virtual void execute(); private: typedef QPointer ScreenWindowPtr; void executeOnScreenWindow(SessionPtr session , ScreenWindowPtr window); void highlightResult(ScreenWindowPtr window , int position); QMap< SessionPtr , ScreenWindowPtr > _windows; QRegularExpression _regExp; SearchDirection _direction; int _startLine; //static QPointer _thread; }; } #endif //SESSIONCONTROLLER_H diff --git a/src/TerminalDisplay.cpp b/src/TerminalDisplay.cpp index 39a8b843..3ef9ddb7 100644 --- a/src/TerminalDisplay.cpp +++ b/src/TerminalDisplay.cpp @@ -1,3498 +1,3495 @@ /* 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 // KDE #include #include #include #include #include #include #include #include #include #include // Konsole #include "Filter.h" #include "konsoledebug.h" #include "konsole_wcwidth.h" #include "TerminalCharacterDecoder.h" #include "Screen.h" #include "LineFont.h" #include "SessionController.h" #include "ExtendedCharTable.h" #include "TerminalDisplayAccessible.h" #include "SessionManager.h" #include "Session.h" #include "WindowSystemInfo.h" using namespace Konsole; #ifndef loc #define loc(X,Y) ((Y)*_columns+(X)) #endif #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); /* ------------------------------------------------------------------------- */ /* */ /* 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) { disconnect(_screenWindow , 0 , this , 0); } _screenWindow = window; if (_screenWindow) { 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]() { _filterUpdateRequired = true; }); connect(_screenWindow.data(), &Konsole::ScreenWindow::scrolled, [this]() { _filterUpdateRequired = true; }); _screenWindow->setWindowLines(_lines); } } const ColorEntry* TerminalDisplay::colorTable() const { return _colorTable; } void TerminalDisplay::setBackgroundColor(const QColor& color) { _colorTable[DEFAULT_BACK_COLOR].color = color; QPalette p = palette(); p.setColor(backgroundRole(), color); setPalette(p); // Avoid propagating the palette change to the scroll bar _scrollBar->setPalette(QApplication::palette()); update(); } QColor TerminalDisplay::getBackgroundColor() const { QPalette p = palette(); return p.color(backgroundRole()); } void TerminalDisplay::setForegroundColor(const QColor& color) { _colorTable[DEFAULT_FORE_COLOR].color = color; update(); } void TerminalDisplay::setColorTable(const ColorEntry table[]) { for (int i = 0; i < TABLE_COLORS; i++) _colorTable[i] = table[i]; setBackgroundColor(_colorTable[DEFAULT_BACK_COLOR].color); } /* ------------------------------------------------------------------------- */ /* */ /* Font */ /* */ /* ------------------------------------------------------------------------- */ static inline bool isLineCharString(const QString& string) { if (string.length() == 0) return false; return isSupportedLineChar(string.at(0).unicode()); } void TerminalDisplay::fontChange(const QFont&) { QFontMetrics fm(font()); _fontHeight = fm.height() + _lineSpacing; // 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(REPCHAR)) / static_cast(qstrlen(REPCHAR)))); _fixedFont = true; const int fw = fm.width(REPCHAR[0]); for (unsigned int i = 1; i < qstrlen(REPCHAR); i++) { if (fw != fm.width(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); QFontMetrics fontMetrics(newFont); // This check seems extreme and semi-random if ((fontMetrics.height() > height()) || (fontMetrics.maxWidth() > width())) return; // hint that text should be drawn without anti-aliasing. // depending on the user's font configuration, this may not be respected if (!_antialiasText) newFont.setStyleStrategy(QFont::StyleStrategy(newFont.styleStrategy() | QFont::NoAntialias)); // experimental optimization. Konsole assumes that the terminal is using a // mono-spaced font, in which case kerning information should have an effect. // Disabling kerning saves some computation when rendering text. newFont.setKerning(false); // Konsole cannot handle non-integer font metrics newFont.setStyleStrategy(QFont::StyleStrategy(newFont.styleStrategy() | QFont::ForceIntegerMetrics)); QFontInfo fontInfo(newFont); // if (!fontInfo.fixedPitch()) { // qWarning() << "Using a variable-width font - this might cause display problems"; // } // QFontInfo::fixedPitch() appears to not match QFont::fixedPitch() // related? https://bugreports.qt.io/browse/QTBUG-34082 if (!fontInfo.exactMatch()) { const QChar comma(QLatin1Char(',')); QString nonMatching = fontInfo.family() % comma % QString::number(fontInfo.pointSizeF()) % comma % QString::number(fontInfo.pixelSize()) % comma % QString::number((int)fontInfo.styleHint()) % comma % QString::number(fontInfo.weight()) % comma % QString::number((int)fontInfo.style()) % comma % QString::number((int)fontInfo.underline()) % comma % QString::number((int)fontInfo.strikeOut()) % comma % QString::number((int)fontInfo.fixedPitch()) % comma % QString::number((int)fontInfo.rawMode()); qCDebug(KonsoleDebug) << "The font to use in the terminal can not be matched exactly on your system."; qCDebug(KonsoleDebug)<<" Selected: "<(object)) return new TerminalDisplayAccessible(display); return 0; } #endif } /* ------------------------------------------------------------------------- */ /* */ /* Constructor / Destructor */ /* */ /* ------------------------------------------------------------------------- */ TerminalDisplay::TerminalDisplay(QWidget* parent) : QWidget(parent) , _screenWindow(0) , _bellMasked(false) , _gridLayout(0) , _fontHeight(1) , _fontWidth(1) , _fontAscent(1) , _boldIntense(true) , _lines(1) , _columns(1) , _usedLines(1) , _usedColumns(1) , _image(0) , _randomSeed(0) , _resizing(false) , _showTerminalSizeHint(true) , _bidiEnabled(false) , _actSel(0) , _wordSelectionMode(false) , _lineSelectionMode(false) , _preserveLineBreaks(false) , _columnSelectionMode(false) , _autoCopySelectedText(false) , _middleClickPasteMode(Enum::PasteFromX11Selection) , _scrollbarLocation(Enum::ScrollBarRight) , _scrollFullPage(false) , _wordCharacters(":@-./_~") , _bellMode(Enum::NotifyBell) , _allowBlinkingText(true) , _allowBlinkingCursor(false) , _textBlinking(false) , _cursorBlinking(false) , _hasTextBlinker(false) , _urlHintsModifiers(Qt::NoModifier) , _showUrlHint(false) - , _underlineLinks(true) , _openLinksByDirectClick(false) , _ctrlRequiredForDrag(true) , _tripleClickMode(Enum::SelectWholeLine) , _possibleTripleClick(false) , _resizeWidget(0) , _resizeTimer(0) , _flowControlWarningEnabled(false) , _outputSuspendedLabel(0) , _lineSpacing(0) , _blendColor(qRgba(0, 0, 0, 0xff)) , _filterChain(new TerminalImageFilterChain()) , _filterUpdateRequired(true) , _cursorShape(Enum::BlockCursor) , _antialiasText(true) , _useFontLineCharacters(false) , _printerFriendly(false) , _sessionController(0) , _trimTrailingSpaces(false) , _margin(1) , _centerContents(false) , _opacity(1.0) { // 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); // set the scroll bar's slider to occupy the whole area of the scroll bar initially setScroll(0, 0); _scrollBar->setCursor(Qt::ArrowCursor); 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); setUsesMouse(true); 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); _gridLayout = new QGridLayout; _gridLayout->setContentsMargins(0, 0, 0, 0); setLayout(_gridLayout); new AutoScrollHandler(this); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(Konsole::accessibleInterfaceFactory); #endif } TerminalDisplay::~TerminalDisplay() { disconnect(_blinkTextTimer); disconnect(_blinkCursorTimer); delete[] _image; delete _filterChain; } /* ------------------------------------------------------------------------- */ /* */ /* Display Operations */ /* */ /* ------------------------------------------------------------------------- */ /** A table for emulating the simple (single width) unicode drawing chars. It represents the 250x - 257x glyphs. If it's zero, we can't use it. if it's not, it's encoded as follows: imagine a 5x5 grid where the points are numbered 0 to 24 left to top, top to bottom. Each point is represented by the corresponding bit. Then, the pixels basically have the following interpretation: _|||_ -...- -...- -...- _|||_ where _ = none | = vertical line. - = horizontal line. */ enum LineEncode { TopL = (1 << 1), TopC = (1 << 2), TopR = (1 << 3), LeftT = (1 << 5), Int11 = (1 << 6), Int12 = (1 << 7), Int13 = (1 << 8), RightT = (1 << 9), LeftC = (1 << 10), Int21 = (1 << 11), Int22 = (1 << 12), Int23 = (1 << 13), RightC = (1 << 14), LeftB = (1 << 15), Int31 = (1 << 16), Int32 = (1 << 17), Int33 = (1 << 18), RightB = (1 << 19), BotL = (1 << 21), BotC = (1 << 22), BotR = (1 << 23) }; static void drawLineChar(QPainter& paint, int x, int y, int w, int h, uchar code) { //Calculate cell midpoints, end points. const int cx = x + w / 2; const int cy = y + h / 2; const int ex = x + w - 1; const int ey = y + h - 1; const quint32 toDraw = LineChars[code]; //Top _lines: if (toDraw & TopL) paint.drawLine(cx - 1, y, cx - 1, cy - 2); if (toDraw & TopC) paint.drawLine(cx, y, cx, cy - 2); if (toDraw & TopR) paint.drawLine(cx + 1, y, cx + 1, cy - 2); //Bot _lines: if (toDraw & BotL) paint.drawLine(cx - 1, cy + 2, cx - 1, ey); if (toDraw & BotC) paint.drawLine(cx, cy + 2, cx, ey); if (toDraw & BotR) paint.drawLine(cx + 1, cy + 2, cx + 1, ey); //Left _lines: if (toDraw & LeftT) paint.drawLine(x, cy - 1, cx - 2, cy - 1); if (toDraw & LeftC) paint.drawLine(x, cy, cx - 2, cy); if (toDraw & LeftB) paint.drawLine(x, cy + 1, cx - 2, cy + 1); //Right _lines: if (toDraw & RightT) paint.drawLine(cx + 2, cy - 1, ex, cy - 1); if (toDraw & RightC) paint.drawLine(cx + 2, cy, ex, cy); if (toDraw & RightB) paint.drawLine(cx + 2, cy + 1, ex, cy + 1); //Intersection points. if (toDraw & Int11) paint.drawPoint(cx - 1, cy - 1); if (toDraw & Int12) paint.drawPoint(cx, cy - 1); if (toDraw & Int13) paint.drawPoint(cx + 1, cy - 1); if (toDraw & Int21) paint.drawPoint(cx - 1, cy); if (toDraw & Int22) paint.drawPoint(cx, cy); if (toDraw & Int23) paint.drawPoint(cx + 1, cy); if (toDraw & Int31) paint.drawPoint(cx - 1, cy + 1); if (toDraw & Int32) paint.drawPoint(cx, cy + 1); if (toDraw & Int33) paint.drawPoint(cx + 1, cy + 1); } static void drawOtherChar(QPainter& paint, int x, int y, int w, int h, uchar code) { //Calculate cell midpoints, end points. const int cx = x + w / 2; const int cy = y + h / 2; const int ex = x + w - 1; const int ey = y + h - 1; // Double dashes if (0x4C <= code && code <= 0x4F) { const int xHalfGap = qMax(w / 15, 1); const int yHalfGap = qMax(h / 15, 1); switch (code) { case 0x4D: // BOX DRAWINGS HEAVY DOUBLE DASH HORIZONTAL paint.drawLine(x, cy - 1, cx - xHalfGap - 1, cy - 1); paint.drawLine(x, cy + 1, cx - xHalfGap - 1, cy + 1); paint.drawLine(cx + xHalfGap, cy - 1, ex, cy - 1); paint.drawLine(cx + xHalfGap, cy + 1, ex, cy + 1); // No break! case 0x4C: // BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL paint.drawLine(x, cy, cx - xHalfGap - 1, cy); paint.drawLine(cx + xHalfGap, cy, ex, cy); break; case 0x4F: // BOX DRAWINGS HEAVY DOUBLE DASH VERTICAL paint.drawLine(cx - 1, y, cx - 1, cy - yHalfGap - 1); paint.drawLine(cx + 1, y, cx + 1, cy - yHalfGap - 1); paint.drawLine(cx - 1, cy + yHalfGap, cx - 1, ey); paint.drawLine(cx + 1, cy + yHalfGap, cx + 1, ey); // No break! case 0x4E: // BOX DRAWINGS LIGHT DOUBLE DASH VERTICAL paint.drawLine(cx, y, cx, cy - yHalfGap - 1); paint.drawLine(cx, cy + yHalfGap, cx, ey); break; } } // Rounded corner characters else if (0x6D <= code && code <= 0x70) { const int r = w * 3 / 8; const int d = 2 * r; switch (code) { case 0x6D: // BOX DRAWINGS LIGHT ARC DOWN AND RIGHT paint.drawLine(cx, cy + r, cx, ey); paint.drawLine(cx + r, cy, ex, cy); paint.drawArc(cx, cy, d, d, 90 * 16, 90 * 16); break; case 0x6E: // BOX DRAWINGS LIGHT ARC DOWN AND LEFT paint.drawLine(cx, cy + r, cx, ey); paint.drawLine(x, cy, cx - r, cy); paint.drawArc(cx - d, cy, d, d, 0 * 16, 90 * 16); break; case 0x6F: // BOX DRAWINGS LIGHT ARC UP AND LEFT paint.drawLine(cx, y, cx, cy - r); paint.drawLine(x, cy, cx - r, cy); paint.drawArc(cx - d, cy - d, d, d, 270 * 16, 90 * 16); break; case 0x70: // BOX DRAWINGS LIGHT ARC UP AND RIGHT paint.drawLine(cx, y, cx, cy - r); paint.drawLine(cx + r, cy, ex, cy); paint.drawArc(cx, cy - d, d, d, 180 * 16, 90 * 16); break; } } // Diagonals else if (0x71 <= code && code <= 0x73) { switch (code) { case 0x71: // BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT paint.drawLine(ex, y, x, ey); break; case 0x72: // BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT paint.drawLine(x, y, ex, ey); break; case 0x73: // BOX DRAWINGS LIGHT DIAGONAL CROSS paint.drawLine(ex, y, x, ey); paint.drawLine(x, y, ex, ey); break; } } } void TerminalDisplay::drawLineCharString(QPainter& painter, int x, int y, const QString& str, const Character* attributes) { const QPen& originalPen = painter.pen(); if ((attributes->rendition & RE_BOLD) && _boldIntense) { QPen boldPen(originalPen); boldPen.setWidth(3); painter.setPen(boldPen); } for (int i = 0 ; i < str.length(); i++) { const uchar code = str[i].cell(); if (LineChars[code]) drawLineChar(painter, x + (_fontWidth * i), y, _fontWidth, _fontHeight, code); else drawOtherChar(painter, x + (_fontWidth * i), y, _fontWidth, _fontHeight, code); } painter.setPen(originalPen); } void TerminalDisplay::setKeyboardCursorShape(Enum::CursorShapeEnum shape) { _cursorShape = shape; } Enum::CursorShapeEnum TerminalDisplay::keyboardCursorShape() const { return _cursorShape; } 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; // enable automatic background filling to prevent the display // flickering if there is no transparency /*if ( color.alpha() == 255 ) { setAutoFillBackground(true); } else { setAutoFillBackground(false); }*/ _blendColor = color.rgba(); } void TerminalDisplay::setWallpaper(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. // QRect scrollBarArea = _scrollBar->isVisible() ? rect.intersected(_scrollBar->geometry()) : QRect(); QRegion contentsRegion = QRegion(rect).subtracted(scrollBarArea); QRect contentsRect = contentsRegion.boundingRect(); if (useOpacitySetting && !_wallpaper->isNull() && _wallpaper->draw(painter, contentsRect, _opacity)) { } else if (qAlpha(_blendColor) < 0xff && useOpacitySetting) { #if defined(Q_OS_OSX) // TODO - On MacOS, using CompositionMode doesn't work. Altering the // transparency in the color scheme alters the brightness. painter.fillRect(contentsRect, backgroundColor); #else QColor color(backgroundColor); color.setAlpha(qAlpha(_blendColor)); painter.save(); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(contentsRect, color); painter.restore(); #endif } else { painter.fillRect(contentsRect, backgroundColor); } painter.fillRect(scrollBarArea, _scrollBar->palette().background()); } 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 QRect 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(penWidth / 2, penWidth / 2, - penWidth / 2 - penWidth % 2, - penWidth / 2 - penWidth % 2)); // 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) { painter.drawLine(cursorRect.left(), cursorRect.bottom(), cursorRect.right(), cursorRect.bottom()); } else if (_cursorShape == Enum::IBeamCursor) { painter.drawLine(cursorRect.left(), cursorRect.top(), cursorRect.left(), cursorRect.bottom()); } } 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)) return; // don't draw concealed characters if (style->rendition & RE_CONCEAL) return; // setup bold and underline bool useBold = ((style->rendition & RE_BOLD) && _boldIntense) || font().bold(); const bool useUnderline = style->rendition & RE_UNDERLINE || font().underline(); const bool useItalic = style->rendition & RE_ITALIC || font().italic(); const bool useStrikeOut = style->rendition & RE_STRIKEOUT || font().strikeOut(); const bool useOverline = style->rendition & RE_OVERLINE || font().overline(); QFont font = painter.font(); if (font.bold() != useBold || font.underline() != useUnderline || font.italic() != useItalic || font.strikeOut() != useStrikeOut || font.overline() != useOverline) { font.setBold(useBold); font.setUnderline(useUnderline); font.setItalic(useItalic); font.setStrikeOut(useStrikeOut); font.setOverline(useOverline); painter.setFont(font); } // 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); } // 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); // the drawText(rect,flags,string) overload is used here with null flags // instead of drawText(rect,string) because the (rect,string) overload causes // the application's default layout direction to be used instead of // the widget-specific layout direction, which should always be // Qt::LeftToRight for this widget // // This was discussed in: http://lists.kde.org/?t=120552223600002&r=1&w=2 if (_bidiEnabled) { painter.drawText(rect, 0, text); } else { // See bug 280896 for more info painter.drawText(rect, Qt::AlignBottom, LTR_OVERRIDE_CHAR + text); } } } void TerminalDisplay::drawTextFragment(QPainter& painter , const QRect& rect, const QString& text, const Character* style) { painter.save(); // 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 != palette().background().color()) 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) drawCursor(painter, rect, foregroundColor, backgroundColor, invertCharacterColor); // draw text drawCharacters(painter, rect, text, style, invertCharacterColor); painter.restore(); } void TerminalDisplay::drawPrinterFriendlyTextFragment(QPainter& painter, const QRect& rect, const QString& text, const Character* style) { painter.save(); // 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); painter.restore(); } 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) { // 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 (_outputSuspendedLabel && _outputSuspendedLabel->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(), this->_lines - 2)); // return if there is nothing to do if (lines == 0 || _image == 0 || !region.isValid() || (region.top() + abs(lines)) >= region.bottom() || this->_lines <= region.height()) return; // hide terminal size label to prevent it being scrolled if (_resizeWidget && _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() * this->_columns ]; void* lastCharPos = &_image[(region.top() + abs(lines)) * this->_columns ]; const int top = _contentRect.top() + (region.top() * _fontHeight); const int linesToMove = region.height() - abs(lines); const int bytesToMove = linesToMove * this->_columns * sizeof(Character); Q_ASSERT(linesToMove > 0); Q_ASSERT(bytesToMove > 0); //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 + (this->_lines * this->_columns))); Q_ASSERT((lines * this->_columns) < _imageSize); //scroll internal image down memmove(firstCharPos , lastCharPos , bytesToMove); //set region of display to scroll scrollRect.setTop(top); } else { // check that the memory areas that we are going to move are valid Q_ASSERT((char*)firstCharPos + bytesToMove < (char*)(_image + (this->_lines * this->_columns))); //scroll internal image up memmove(lastCharPos , firstCharPos , bytesToMove); //set region of the display to scroll scrollRect.setTop(top + abs(lines) * _fontHeight); } scrollRect.setHeight(linesToMove * _fontHeight); Q_ASSERT(scrollRect.isValid() && !scrollRect.isEmpty()); //scroll the display vertically to match internal _image scroll(0 , _fontHeight * (-lines) , scrollRect); } QRegion TerminalDisplay::hotSpotRegion() const { QRegion region; foreach(Filter::HotSpot * hotSpot , _filterChain->hotSpots()) { QRect r; if (hotSpot->startLine() == hotSpot->endLine()) { r.setLeft(hotSpot->startColumn()); r.setTop(hotSpot->startLine()); r.setRight(hotSpot->endColumn()); r.setBottom(hotSpot->endLine()); region |= imageToWidget(r); } else { r.setLeft(hotSpot->startColumn()); r.setTop(hotSpot->startLine()); r.setRight(_columns); r.setBottom(hotSpot->startLine()); region |= imageToWidget(r); for (int line = hotSpot->startLine() + 1 ; line < hotSpot->endLine() ; line++) { r.setLeft(0); r.setTop(line); r.setRight(_columns); r.setBottom(line); region |= imageToWidget(r); } r.setLeft(0); r.setTop(hotSpot->endLine()); r.setRight(hotSpot->endColumn()); r.setBottom(hotSpot->endLine()); region |= imageToWidget(r); } } return region; } void TerminalDisplay::processFilters() { if (!_screenWindow) { 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) 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 artefacts, see BUG 350651 if (!(WindowSystemInfo::HAVE_TRANSPARENCY && (qApp->devicePixelRatio() > 1.0)) && _wallpaper->isNull()) { scrollImage(_screenWindow->scrollCount() , _screenWindow->scrollRegion()); _screenWindow->resetScrollCount(); } if (!_image) { // 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(this->_usedLines <= this->_lines); Q_ASSERT(this->_usedColumns <= this->_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(this->_lines, qMax(0, lines)); const int columnsToUpdate = qMin(this->_columns, qMax(0, columns)); char* 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 * this->_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] = true; } } 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]) { if (!newLine[x + 0].character) continue; const bool lineDraw = newLine[x + 0].isLineChar(); 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) 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] || ch.isLineChar() != 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 * this->_columns , _fontHeight * (_usedLines - linesToUpdate)); } _usedLines = linesToUpdate; if (columnsToUpdate < _usedColumns) { dirtyRegion |= QRect(_contentRect.left() + tLx + columnsToUpdate * _fontWidth , _contentRect.top() + tLy , _fontWidth * (_usedColumns - columnsToUpdate) , _fontHeight * this->_lines); } _usedColumns = columnsToUpdate; dirtyRegion |= _inputMethodData.previousPreeditRect; // 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 QAccessible::updateAccessibility(new QAccessibleEvent(this, QAccessible::VisibleDataChanged)); QAccessible::updateAccessibility(new QAccessibleTextCursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX())); #endif } void TerminalDisplay::showResizeNotification() { if (_showTerminalSizeHint && isVisible()) { if (!_resizeWidget) { _resizeWidget = new QLabel(i18n("Size: XXX x XXX"), this); _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().width(i18n("Size: XXX x XXX"))); _resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height()); _resizeWidget->setAlignment(Qt::AlignCenter); _resizeWidget->setStyleSheet("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); foreach(const QRect & rect, (pe->region() & contentsRect()).rects()) { drawBackground(paint, rect, palette().background().color(), true /* use opacity setting */); drawContents(paint, rect); } drawCurrentResultRect(paint); drawInputMethodPreeditString(paint, preeditRect()); paintFilters(paint); } 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, size().width(), size().height()); _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) return _screenWindow->cursorPosition(); else return QPoint(0, 0); } 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); Character cursorCharacter = _image[loc(cursorColumn, 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 int urlNumber = 0; QList spots = _filterChain->hotSpots(); foreach(Filter::HotSpot* spot, spots) { urlNumber++; QRegion region; - if (_underlineLinks && spot->type() == Filter::HotSpot::Link) { + 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.rects().first()); 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 (loc(endColumn, line) > _imageSize) 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 (_underlineLinks && spot->type() == Filter::HotSpot::Link) { + 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); } } } } void TerminalDisplay::drawContents(QPainter& paint, const QRect& rect) { const QPoint tL = contentsRect().topLeft(); const int tLx = tL.x(); const int tLy = tL.y(); const int lux = qMin(_usedColumns - 1, qMax(0, (rect.left() - tLx - _contentRect.left()) / _fontWidth)); const int luy = qMin(_usedLines - 1, qMax(0, (rect.top() - tLy - _contentRect.top()) / _fontHeight)); const int rlx = qMin(_usedColumns - 1, qMax(0, (rect.right() - tLx - _contentRect.left()) / _fontWidth)); const int rly = qMin(_usedLines - 1, qMax(0, (rect.bottom() - tLy - _contentRect.top()) / _fontHeight)); const int numberOfColumns = _usedColumns; QString unistr; unistr.reserve(numberOfColumns); for (int y = luy; y <= rly; y++) { int x = lux; if (!_image[loc(lux, y)].character && x) x--; // Search for start of multi-column character for (; x <= rlx; x++) { int len = 1; int p = 0; // reset our buffer to the number of columns int bufferSize = numberOfColumns; unistr.resize(bufferSize); QChar *disstrU = unistr.data(); // is this a single character or a sequence of characters ? if (_image[loc(x, y)].rendition & RE_EXTENDED_CHAR) { // sequence of characters ushort extendedCharLength = 0; const ushort* chars = ExtendedCharTable::instance.lookupExtendedChar(_image[loc(x, y)].character, extendedCharLength); if (chars) { Q_ASSERT(extendedCharLength > 1); bufferSize += extendedCharLength - 1; unistr.resize(bufferSize); disstrU = unistr.data(); for (int index = 0 ; index < extendedCharLength ; index++) { Q_ASSERT(p < bufferSize); disstrU[p++] = chars[index]; } } } else { // single character const quint16 c = _image[loc(x, y)].character; if (c) { Q_ASSERT(p < bufferSize); disstrU[p++] = c; //fontMap(c); } } const bool lineDraw = _image[loc(x, y)].isLineChar(); const bool doubleWidth = (_image[ qMin(loc(x, y) + 1, _imageSize) ].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; while (x + len <= rlx && _image[loc(x + len, y)].foregroundColor == currentForeground && _image[loc(x + len, y)].backgroundColor == currentBackground && (_image[loc(x + len, y)].rendition & ~RE_EXTENDED_CHAR) == (currentRendition & ~RE_EXTENDED_CHAR) && (_image[ qMin(loc(x + len, y) + 1, _imageSize) ].character == 0) == doubleWidth && _image[loc(x + len, y)].isLineChar() == lineDraw) { const quint16 c = _image[loc(x + len, y)].character; if (_image[loc(x + len, y)].rendition & RE_EXTENDED_CHAR) { // sequence of characters ushort extendedCharLength = 0; const ushort* chars = ExtendedCharTable::instance.lookupExtendedChar(c, extendedCharLength); if (chars) { Q_ASSERT(extendedCharLength > 1); bufferSize += extendedCharLength - 1; unistr.resize(bufferSize); disstrU = unistr.data(); for (int index = 0 ; index < extendedCharLength ; index++) { Q_ASSERT(p < bufferSize); disstrU[p++] = chars[index]; } } } else { // single character if (c) { Q_ASSERT(p < bufferSize); disstrU[p++] = c; //fontMap(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++; } if ((x + len < _usedColumns) && (!_image[loc(x + len, y)].character)) len++; // Adjust for trailing part of multi-column character const bool save__fixedFont = _fixedFont; if (lineDraw) _fixedFont = false; if (doubleWidth) _fixedFont = false; unistr.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) textScale.scale(2, 1); if (_lineProperties[y] & LINE_DOUBLEHEIGHT) 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() + tLx + _fontWidth * x , _contentRect.top() + tLy + _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())); //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) y++; } x += len - 1; } } } void TerminalDisplay::drawCurrentResultRect(QPainter& painter) { if(_screenWindow->currentResultLine() == -1) { return; } QRect r(0, (_screenWindow->currentResultLine() - _screenWindow->currentLine())*_fontHeight, contentsRect().width(), _fontHeight); painter.fillRect(r, 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; } /* ------------------------------------------------------------------------- */ /* */ /* 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 blinkCursorEvent(); } Q_ASSERT(_cursorBlinking == false); } } 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 == false); // if text is blinking (hidden), blink it again to make it shown if (_textBlinking) blinkTextEvent(); // suppress further text blinking _blinkTextTimer->stop(); Q_ASSERT(_textBlinking == false); _showUrlHint = false; emit focusLost(); } void TerminalDisplay::focusInEvent(QFocusEvent*) { if (_allowBlinkingCursor) _blinkCursorTimer->start(); updateCursor(); if (_allowBlinkingText && _hasTextBlinker) _blinkTextTimer->start(); 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. update(); } void TerminalDisplay::blinkCursorEvent() { Q_ASSERT(_allowBlinkingCursor); _cursorBlinking = !_cursorBlinking; updateCursor(); } void TerminalDisplay::updateCursor() { int cursorLocation = loc(cursorPosition().x(), cursorPosition().y()); int charWidth = konsole_wcwidth(_image[cursorLocation].character); QRect cursorRect = imageToWidget(QRect(cursorPosition(), QSize(charWidth, 1))); update(cursorRect); } /* ------------------------------------------------------------------------- */ /* */ /* Geometry & Resizing */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::resizeEvent(QResizeEvent*) { updateImageSize(); } void TerminalDisplay::propagateSize() { if (_image) updateImageSize(); } void TerminalDisplay::updateImageSize() { Character* oldImage = _image; const int oldLines = _lines; const int oldColumns = _columns; makeImage(); if (oldImage) { // 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) _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; // We over-commit one character so that we can be more relaxed in dealing with // certain boundary conditions: _image[_imageSize] is a valid but unused position _image = new Character[_imageSize + 1]; clearImage(); } void TerminalDisplay::clearImage() { for (int i = 0; i <= _imageSize; ++i) _image[i] = Screen::DefaultChar; } void TerminalDisplay::calcGeometry() { _scrollBar->resize(_scrollBar->sizeHint().width(), contentsRect().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().topLeft()); break; case Enum::ScrollBarRight: _contentRect.setRight(_contentRect.right() - _scrollBar->width()); _scrollBar->move(contentsRect().topRight() - QPoint(_scrollBar->width() - 1, 0)); break; } // 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*) { 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; if (position == Enum::ScrollBarHidden) _scrollBar->hide(); else _scrollBar->show(); _scrollbarLocation = position; propagateSize(); update(); } void TerminalDisplay::scrollBarPositionChanged(int) { if (!_screenWindow) 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) return; int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn); QPoint pos = QPoint(charColumn, charLine); if (ev->button() == Qt::LeftButton) { // request the software keyboard, if any if (qApp->autoSipEnabled()) { QStyle::RequestSoftwareInputPanel behavior = QStyle::RequestSoftwareInputPanel( style()->styleHint(QStyle::SH_RequestSoftwareInputPanel)); if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) { QEvent event(QEvent::RequestSoftwareInputPanel); QApplication::sendEvent(this, &event); } } _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) && 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) && !(ev->modifiers() & Qt::AltModifier)); _columnSelectionMode = (ev->modifiers() & Qt::AltModifier) && (ev->modifiers() & Qt::ControlModifier); if (_mouseMarks || (ev->modifiers() == Qt::ShiftModifier)) { // Only extend selection for programs not interested in mouse if (_mouseMarks && (ev->modifiers() == Qt::ShiftModifier)) { extendSelection(ev->pos()); } else { _screenWindow->clearSelection(); pos.ry() += _scrollBar->value(); _iPntSel = _pntSel = pos; _actSel = 1; // left mouse button pressed but nothing selected yet. } } else { emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } - if (_underlineLinks && (_openLinksByDirectClick || (ev->modifiers() & Qt::ControlModifier))) { + if ((_openLinksByDirectClick || (ev->modifiers() & Qt::ControlModifier))) { Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if (spot && spot->type() == Filter::HotSpot::Link) { QObject action; action.setObjectName("open-action"); spot->activate(&action); } } } } else if (ev->button() == Qt::MidButton) { processMidButtonClick(ev); } else if (ev->button() == Qt::RightButton) { if (_mouseMarks || (ev->modifiers() & Qt::ShiftModifier)) emit configureRequest(ev->pos()); else 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); Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); return spot ? spot->actions() : QList(); } void TerminalDisplay::mouseMoveEvent(QMouseEvent* ev) { int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn); processFilters(); // handle filters // change link hot-spot appearance on mouse-over Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if (spot && spot->type() == Filter::HotSpot::Link) { - if (_underlineLinks) { - 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; - } + 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(), - spot->endLine()*_fontHeight + _contentRect.top(), - (spot->endColumn())*_fontWidth + _contentRect.left() - 1, - (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); + 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)) && (cursor().shape() != Qt::PointingHandCursor)) - setCursor(Qt::PointingHandCursor); + if ((_openLinksByDirectClick || (ev->modifiers() & Qt::ControlModifier)) && (cursor().shape() != Qt::PointingHandCursor)) + setCursor(Qt::PointingHandCursor); - update(_mouseOverHotspotArea | previousHotspotArea); - } + update(_mouseOverHotspotArea | previousHotspotArea); } else if (!_mouseOverHotspotArea.isEmpty()) { - if ((_underlineLinks && (_openLinksByDirectClick || (ev->modifiers() & Qt::ControlModifier))) || (cursor().shape() == Qt::PointingHandCursor)) + if ((_openLinksByDirectClick || (ev->modifiers() & Qt::ControlModifier)) || (cursor().shape() == Qt::PointingHandCursor)) setCursor(_mouseMarks ? Qt::IBeamCursor : Qt::ArrowCursor); 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 terminal is interested in mouse movements // then emit a mouse movement signal, unless the shift // key is being held down, which overrides this. if (!_mouseMarks && !(ev->modifiers() & Qt::ShiftModifier)) { int button = 3; if (ev->buttons() & Qt::LeftButton) button = 0; if (ev->buttons() & Qt::MidButton) button = 1; if (ev->buttons() & Qt::RightButton) 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) 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) 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); 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; if (left_not_right) { ohere = findWordEnd(_iPntSelCorr); here = findWordStart(here); } else { ohere = findWordStart(_iPntSelCorr); here = findWordEnd(here); } 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) { int i = loc(right.x(), right.y()); if (i >= 0 && i <= _imageSize) { selClass = charClass(_image[i - 1]); /* if (selClass == ' ') { while ( right.x() < _usedColumns-1 && charClass(_image[i+1].character) == selClass && (right.y()<_usedLines-1) && !(_lineProperties[right.y()] & LINE_WRAPPED)) { i++; right.rx()++; } if (right.x() < _usedColumns-1) right = left_not_right ? _iPntSelCorr : here; else right.rx()++; // will be balanced later because of offset=-1; }*/ } } // 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) return; int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn); 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 (!_mouseMarks && !(ev->modifiers() & Qt::ShiftModifier)) emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2); } _dragInfo.state = diNone; } if (!_mouseMarks && (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) const { column = (widgetPoint.x() + _fontWidth / 2 - contentsRect().left() - _contentRect.left()) / _fontWidth; line = (widgetPoint.y() - contentsRect().top() - _contentRect.top()) / _fontHeight; if (line < 0) line = 0; if (column < 0) column = 0; if (line >= _usedLines) line = _usedLines - 1; // the column value returned can be equal to _usedColumns, 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) if (column > _usedColumns) column = _usedColumns; } void TerminalDisplay::updateLineProperties() { if (!_screenWindow) return; _lineProperties = _screenWindow->getLineProperties(); } void TerminalDisplay::processMidButtonClick(QMouseEvent* ev) { if (_mouseMarks || (ev->modifiers() & Qt::ShiftModifier)) { const bool appendEnter = ev->modifiers() & Qt::ControlModifier; if (_middleClickPasteMode == Enum::PasteFromX11Selection) { pasteFromX11Selection(appendEnter); } else if (_middleClickPasteMode == Enum::PasteFromClipboard) { pasteFromClipboard(appendEnter); } else { Q_ASSERT(false); } } else { int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn); 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) return; int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn); // pass on double click as two clicks. if (!_mouseMarks && !(ev->modifiers() & Qt::ShiftModifier)) { // 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(); _wordSelectionMode = true; _actSel = 2; // within selection _iPntSel = QPoint(charColumn, charLine); const QPoint bgnSel = findWordStart(_iPntSel); const QPoint endSel = findWordEnd(_iPntSel); _iPntSel.ry() += _scrollBar->value(); _screenWindow->setSelectionStart(bgnSel.x() , bgnSel.y() , false); _screenWindow->setSelectionEnd(endSel.x() , endSel.y()); copyToX11Selection(); _possibleTripleClick = true; QTimer::singleShot(QApplication::doubleClickInterval(), this, SLOT(tripleClickTimeout())); } void TerminalDisplay::wheelEvent(QWheelEvent* ev) { // Only vertical scrolling is supported if (ev->orientation() != Qt::Vertical) return; const int modifiers = ev->modifiers(); _scrollWheelState.addWheelEvent(ev); // ctrl+ for zooming, like in konqueror and firefox if ((modifiers & Qt::ControlModifier) && mouseWheelZoom()) { 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(); } return; } // if the terminal program is not interested with mouse events: // * send the event to the scrollbar if the slider has room to move // * otherwise, send simulated up / down key presses to the terminal program // for the benefit of programs such as 'less' if (_mouseMarks) { const bool canScroll = _scrollBar->maximum() > 0; if (canScroll) { _scrollBar->event(ev); _sessionController->setSearchStartToWindowCurrentLine(); _scrollWheelState.clearAll(); } else { // 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(_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++) emit keyPressedSignal(&keyEvent); } } else { // terminal program wants notification of mouse activity int charLine; int charColumn; getCharacterPosition(ev->pos() , charLine , charColumn); 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::tripleClickTimeout() { _possibleTripleClick = false; } void TerminalDisplay::viewScrolledByUser() { _sessionController->setSearchStartToWindowCurrentLine(); } /* Moving left/up from the line containing pnt, return the starting offset point which the given line is continiously 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)) { return QPoint(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 QPoint(0, lineInHistory - topVisibleLine); } /* Moving right/down from the line containing pnt, return the ending offset point which the given line is continiously 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)) { return QPoint(_columns - 1, lineInHistory - topVisibleLine); } } line = 0; lineProperties = screen->getLineProperties(lineInHistory, qMin(lineInHistory + visibleScreenLines, maxY)); } return QPoint(_columns - 1, lineInHistory - topVisibleLine); } QPoint TerminalDisplay::findWordStart(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 = NULL; const QChar selClass = charClass(image[j]); const int imageSize = regSize * _columns; while (true) { for (;;j--, x--) { if (x > 0) { if (charClass(image[j - 1]) == selClass) continue; goto out; } else if (i > 0) { if (lineProperties[i - 1] & LINE_WRAPPED && charClass(image[j - 1]) == selClass) { x = _columns; i--; y--; continue; } goto out; } else if (y > 0) { break; } else { goto out; } } int newRegStart = qMax(0, y - regSize); lineProperties = screen->getLineProperties(newRegStart, y - 1); i = y - newRegStart; if (!tmp_image) { tmp_image = new Character[imageSize]; image = tmp_image; } screen->getImage(tmp_image, imageSize, newRegStart, y - 1); j = loc(x, i); } out: if (tmp_image) { delete[] tmp_image; } return QPoint(x, y - curLine); } 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 = NULL; 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) continue; goto out; } else if (i < lineCount - 1) { if (lineProperties[i] & LINE_WRAPPED && charClass(image[j + 1]) == selClass) { x = -1; i++; y++; continue; } goto out; } else if (y < maxY) { if (i < lineCount && !(lineProperties[i] & LINE_WRAPPED)) goto out; break; } else { goto out; } } int newRegEnd = qMin(y + regSize - 1, maxY); lineProperties = screen->getLineProperties(y, newRegEnd); i = 0; if (!tmp_image) { 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) == '@') && (y > pnt.y() || x > pnt.x())) { if (x > 0) { x--; } else { y--; } } if (tmp_image) { delete[] tmp_image; } return QPoint(x, y); } void TerminalDisplay::mouseTripleClickEvent(QMouseEvent* ev) { if (!_screenWindow) return; int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn); 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) return; selectLine(cursorPosition(), true); } 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) { ushort extendedCharLength = 0; const ushort* chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength); if (chars && extendedCharLength > 0) { const QString s = QString::fromUtf16(chars, extendedCharLength); if (_wordCharacters.contains(s, Qt::CaseInsensitive)) return 'a'; bool allLetterOrNumber = true; for (int i = 0; allLetterOrNumber && i < s.size(); ++i) allLetterOrNumber = s.at(i).isLetterOrNumber(); return allLetterOrNumber ? 'a' : s.at(0); } return 0; } else { const QChar qch(ch.character); if (qch.isSpace()) return ' '; if (qch.isLetterOrNumber() || _wordCharacters.contains(qch, Qt::CaseInsensitive)) return 'a'; return qch; } } void TerminalDisplay::setWordCharacters(const QString& wc) { _wordCharacters = wc; } // FIXME: the actual value of _mouseMarks is the opposite of its semantic. // When using programs not interested with mouse(shell, less), it is true. // When using programs interested with mouse(vim,mc), it is false. void TerminalDisplay::setUsesMouse(bool on) { _mouseMarks = on; setCursor(_mouseMarks ? Qt::IBeamCursor : Qt::ArrowCursor); } bool TerminalDisplay::usesMouse() const { return _mouseMarks; } void TerminalDisplay::setBracketedPasteMode(bool on) { _bracketedPasteMode = on; } bool TerminalDisplay::bracketedPasteMode() const { return _bracketedPasteMode; } /* ------------------------------------------------------------------------- */ /* */ /* Clipboard */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::doPaste(QString text, bool appendReturn) { if (!_screenWindow) return; if (appendReturn) text.append("\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(), "ShowPasteHugeTextWarning") == KMessageBox::Cancel) return; } if (!text.isEmpty()) { text.replace('\n', '\r'); if (bracketedPasteMode()) { text.prepend("\e[200~"); text.append("\e[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::copyToX11Selection() { if (!_screenWindow) return; QString text = _screenWindow->selectedText(_preserveLineBreaks, _trimTrailingSpaces); if (text.isEmpty()) return; QString html = _screenWindow->selectedText(_preserveLineBreaks, _trimTrailingSpaces, true); QMimeData *mimeData = new QMimeData; mimeData->setText(text); mimeData->setHtml(html); if (QApplication::clipboard()->supportsSelection()) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection); } if (_autoCopySelectedText) QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void TerminalDisplay::copyToClipboard() { if (!_screenWindow) return; QString text = _screenWindow->selectedText(_preserveLineBreaks, _trimTrailingSpaces); if (text.isEmpty()) return; QString html = _screenWindow->selectedText(_preserveLineBreaks, _trimTrailingSpaces, true); QMimeData *mimeData = new QMimeData; mimeData->setText(text); mimeData->setHtml(html); 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); } _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)); break; case Qt::ImFont: return font(); break; case Qt::ImCursorPosition: // return the cursor position within the current line return cursorPos.x(); break; case Qt::ImSurroundingText: { // return the text from the current line QString lineText; QTextStream stream(&lineText); PlainTextDecoder decoder; decoder.begin(&stream); decoder.decodeLine(&_image[loc(0, cursorPos.y())], _usedColumns, LINE_DEFAULT); decoder.end(); return lineText; } break; case Qt::ImCurrentSelection: return QString(); break; default: break; } return QVariant(); } QRect TerminalDisplay::preeditRect() const { const int preeditLength = string_width(_inputMethodData.preeditString); if (preeditLength == 0) return QRect(); return QRect(_contentRect.left() + _fontWidth * cursorPosition().x(), _contentRect.top() + _fontHeight * cursorPosition().y(), _fontWidth * preeditLength, _fontHeight); } void TerminalDisplay::drawInputMethodPreeditString(QPainter& painter , const QRect& rect) { if (_inputMethodData.preeditString.isEmpty()) return; const QPoint cursorPos = cursorPosition(); bool invertColors = false; const QColor background = _colorTable[DEFAULT_BACK_COLOR].color; const QColor foreground = _colorTable[DEFAULT_FORE_COLOR].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 (!_outputSuspendedLabel) { //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. _outputSuspendedLabel = new QLabel(i18n("Output has been " "suspended" " by pressing Ctrl+S." " Press Ctrl+Q to resume." " Click here to dismiss this message.")); QPalette palette(_outputSuspendedLabel->palette()); KColorScheme::adjustBackground(palette, KColorScheme::NeutralBackground); _outputSuspendedLabel->setPalette(palette); _outputSuspendedLabel->setAutoFillBackground(true); _outputSuspendedLabel->setBackgroundRole(QPalette::Base); _outputSuspendedLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); _outputSuspendedLabel->setContentsMargins(5, 5, 5, 5); _outputSuspendedLabel->setWordWrap(true); _outputSuspendedLabel->setFocusProxy(this); connect(_outputSuspendedLabel, &QLabel::linkActivated, [this](const QString &url) { if (url == "#close") { _outputSuspendedLabel->setVisible(false); } else { QDesktopServices::openUrl(QUrl(url)); } }); //enable activation of "Xon/Xoff" link in label _outputSuspendedLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard); _outputSuspendedLabel->setOpenExternalLinks(false); _outputSuspendedLabel->setVisible(false); _gridLayout->addWidget(_outputSuspendedLabel); _gridLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding), 1, 0); } _outputSuspendedLabel->setVisible(suspended); } void TerminalDisplay::dismissOutputSuspendedMessage() { outputSuspended(false); } 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 && event->modifiers() == _urlHintsModifiers) { int hintSelected = event->key() - 0x31; if (hintSelected >= 0 && hintSelected < 10 && hintSelected < _filterChain->hotSpots().count()) { _filterChain->hotSpots().at(hintSelected)->activate(); _showUrlHint = false; update(); return; } if (!_showUrlHint) { processFilters(); _showUrlHint = true; update(); } } _screenWindow->screen()->setCurrentTerminalDisplay(this); _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 == false); } emit keyPressedSignal(event); #ifndef QT_NO_ACCESSIBILITY QAccessible::updateAccessibility(new QAccessibleTextCursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX())); #endif event->accept(); } void TerminalDisplay::keyReleaseEvent(QKeyEvent *event) { if (_showUrlHint) { _showUrlHint = false; update(); } 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) 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: _scrollBar->setPalette(QApplication::palette()); 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::unmaskBell() { _bellMasked = false; } 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() ? "BellVisible" : "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, SLOT(unmaskBell())); } void TerminalDisplay::visualBell() { swapFGBGColors(); QTimer::singleShot(200, this, SLOT(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; update(); } /* --------------------------------------------------------------------- */ /* */ /* 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() if (event->mimeData()->hasFormat("text/plain") || event->mimeData()->hasFormat("text/uri-list")) { event->acceptProposedAction(); } } void TerminalDisplay::dropEvent(QDropEvent* event) { auto urls = event->mimeData()->urls(); QString dropText; if (!urls.isEmpty()) { for (int i = 0 ; i < urls.count() ; i++) { KIO::StatJob* job = KIO::mostLocalUrl(urls[i], KIO::HideProgressInfo); bool ok = job->exec(); if (!ok) continue; QUrl url = job->mostLocalUrl(); QString urlText; if (url.isLocalFile()) urlText = url.path(); else urlText = url.url(); // 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) urlText = KShell::quoteArg(urlText); dropText += urlText; // Each filename(including the last) should be followed by one space. dropText += ' '; } // 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 && _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); pasteAction->setData(dropText); connect(pasteAction, &QAction::triggered, this, &TerminalDisplay::dropMenuPasteActionTriggered); QList additionalActions; additionalActions.append(pasteAction); if (urls.count() == 1) { KIO::StatJob* job = KIO::mostLocalUrl(urls[0], KIO::HideProgressInfo); bool ok = job->exec(); if (ok) { const QUrl url = job->mostLocalUrl(); if (url.isLocalFile()) { const QFileInfo fileInfo(url.path()); if (fileInfo.isDir()) { QAction* cdAction = new QAction(i18n("Change &Directory To"), this); dropText = QLatin1String(" cd ") + dropText + QChar('\n'); cdAction->setData(dropText); connect(cdAction, &QAction::triggered, this, &TerminalDisplay::dropMenuCdActionTriggered); additionalActions.append(cdAction); } } } } QUrl target = QUrl::fromLocalFile(_sessionController->currentDir()); KIO::DropJob* job = KIO::drop(event, target); KJobWidgets::setWindow(job, this); job->setApplicationActions(additionalActions); return; } } else { dropText = event->mimeData()->text(); } if (event->mimeData()->hasFormat("text/plain") || event->mimeData()->hasFormat("text/uri-list")) { emit sendStringToEmu(dropText.toLocal8Bit()); } } void TerminalDisplay::dropMenuPasteActionTriggered() { if (sender()) { const QAction* action = qobject_cast(sender()); if (action) { emit sendStringToEmu(action->data().toString().toLocal8Bit()); } } } void TerminalDisplay::dropMenuCdActionTriggered() { if (sender()) { const QAction* action = qobject_cast(sender()); if (action) { emit sendStringToEmu(action->data().toString().toLocal8Bit()); } } } void TerminalDisplay::doDrag() { _dragInfo.state = diDragging; _dragInfo.dragObject = new QDrag(this); QMimeData* mimeData = new QMimeData(); mimeData->setText(QApplication::clipboard()->mimeData(QClipboard::Selection)->text()); mimeData->setHtml(QApplication::clipboard()->mimeData(QClipboard::Selection)->html()); _dragInfo.dragObject->setMimeData(mimeData); _dragInfo.dragObject->exec(Qt::CopyAction); } void TerminalDisplay::setSessionController(SessionController* controller) { _sessionController = controller; } SessionController* TerminalDisplay::sessionController() { return _sessionController; } 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); QMouseEvent* mouseEvent = (QMouseEvent*)event; switch (event->type()) { case QEvent::MouseMove: { bool mouseInWidget = widget()->rect().contains(mouseEvent->pos()); if (mouseInWidget) { if (_timerId) killTimer(_timerId); _timerId = 0; } else { if (!_timerId && (mouseEvent->buttons() & Qt::LeftButton)) _timerId = startTimer(100); } break; } case QEvent::MouseButtonRelease: { if (_timerId && (mouseEvent->buttons() & ~Qt::LeftButton)) { killTimer(_timerId); _timerId = 0; } break; } default: break; }; return false; } diff --git a/src/TerminalDisplay.h b/src/TerminalDisplay.h index b3ffba7c..aeee1294 100644 --- a/src/TerminalDisplay.h +++ b/src/TerminalDisplay.h @@ -1,974 +1,958 @@ /* Copyright 2007-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. */ #ifndef TERMINALDISPLAY_H #define TERMINALDISPLAY_H // Qt #include #include #include // Konsole #include "Character.h" #include "konsoleprivate_export.h" #include "ScreenWindow.h" #include "ColorScheme.h" #include "Enumeration.h" #include "ScrollState.h" class QDrag; class QDragEnterEvent; class QDropEvent; class QLabel; class QTimer; class QEvent; class QGridLayout; class QKeyEvent; class QScrollBar; class QShowEvent; class QHideEvent; class QTimerEvent; namespace Konsole { class FilterChain; class TerminalImageFilterChain; class SessionController; /** * A widget which displays output from a terminal emulation and sends input keypresses and mouse activity * to the terminal. * * When the terminal emulation receives new output from the program running in the terminal, * it will update the display by calling updateImage(). * * TODO More documentation */ class KONSOLEPRIVATE_EXPORT TerminalDisplay : public QWidget { Q_OBJECT public: /** Constructs a new terminal display widget with the specified parent. */ explicit TerminalDisplay(QWidget* parent = 0); virtual ~TerminalDisplay(); /** Returns the terminal color palette used by the display. */ const ColorEntry* colorTable() const; /** Sets the terminal color palette used by the display. */ void setColorTable(const ColorEntry table[]); /** * Sets the seed used to generate random colors for the display * (in color schemes that support them). */ void setRandomSeed(uint seed); /** * Returns the seed used to generate random colors for the display * (in color schemes that support them). */ uint randomSeed() const; /** Sets the opacity of the terminal display. */ void setOpacity(qreal opacity); /** Sets the background picture */ void setWallpaper(ColorSchemeWallpaper::Ptr p); /** * Specifies whether the terminal display has a vertical scroll bar, and if so whether it * is shown on the left or right side of the display. */ void setScrollBarPosition(Enum::ScrollBarPositionEnum position); Enum::ScrollBarPositionEnum scrollBarPosition() const { return _scrollbarLocation; } /** * Sets the current position and range of the display's scroll bar. * * @param cursor The position of the scroll bar's thumb. * @param lines The maximum value of the scroll bar. */ void setScroll(int cursor, int lines); void setScrollFullPage(bool fullPage); bool scrollFullPage() const; /** * Returns the display's filter chain. When the image for the display is updated, * the text is passed through each filter in the chain. Each filter can define * hotspots which correspond to certain strings (such as URLs or particular words). * Depending on the type of the hotspots created by the filter ( returned by Filter::Hotspot::type() ) * the view will draw visual cues such as underlines on mouse-over for links or translucent * rectangles for markers. * * To add a new filter to the view, call: * viewWidget->filterChain()->addFilter( filterObject ); */ FilterChain* filterChain() const; /** * Updates the filters in the display's filter chain. This will cause * the hotspots to be updated to match the current image. * * WARNING: This function can be expensive depending on the * image size and number of filters in the filterChain() * * TODO - This API does not really allow efficient usage. Revise it so * that the processing can be done in a better way. * * eg: * - Area of interest may be known ( eg. mouse cursor hovering * over an area ) */ void processFilters(); /** * Returns a list of menu actions created by the filters for the content * at the given @p position. */ QList filterActions(const QPoint& position); /** Specifies whether or not the cursor can blink. */ void setBlinkingCursorEnabled(bool blink); /** Returns true if the cursor is allowed to blink or false otherwise. */ bool blinkingCursorEnabled() const { return _allowBlinkingCursor; } /** Specifies whether or not text can blink. */ void setBlinkingTextEnabled(bool blink); void setControlDrag(bool enable) { _ctrlRequiredForDrag = enable; } bool ctrlRequiredForDrag() const { return _ctrlRequiredForDrag; } void setDropUrlsAsText(bool enable) { _dropUrlsAsText = enable; } bool getDropUrlsAsText() const { return _dropUrlsAsText; } /** Sets how the text is selected when the user triple clicks within the display. */ void setTripleClickMode(Enum::TripleClickModeEnum mode) { _tripleClickMode = mode; } /** See setTripleClickSelectionMode() */ Enum::TripleClickModeEnum tripleClickMode() const { return _tripleClickMode; } - /** - * Specifies whether links and email addresses should be underlined when - * hovered by the mouse. Defaults to true. - */ - void setUnderlineLinks(bool value) { - _underlineLinks = value; - } - /** - * Returns true if links and email addresses should be underlined when - * hovered by the mouse. - */ - bool getUnderlineLinks() const { - return _underlineLinks; - } - /** * Specifies whether links and email addresses should be opened when * clicked with the mouse. Defaults to false. */ void setOpenLinksByDirectClick(bool value) { _openLinksByDirectClick = value; } /** * Returns true if links and email addresses should be opened when * clicked with the mouse. */ bool getOpenLinksByDirectClick() const { return _openLinksByDirectClick; } /** * Sets whether trailing spaces should be trimmed in selected text. */ void setTrimTrailingSpaces(bool enabled) { _trimTrailingSpaces = enabled; } /** * Returns true if trailing spaces should be trimmed in selected text. */ bool trimTrailingSpaces() const { return _trimTrailingSpaces; } void setLineSpacing(uint); uint lineSpacing() const; void setSessionController(SessionController* controller); SessionController* sessionController(); /** * Sets the shape of the keyboard cursor. This is the cursor drawn * at the position in the terminal where keyboard input will appear. * * In addition the terminal display widget also has a cursor for * the mouse pointer, which can be set using the QWidget::setCursor() * method. * * Defaults to BlockCursor */ void setKeyboardCursorShape(Enum::CursorShapeEnum shape); /** * Returns the shape of the keyboard cursor. See setKeyboardCursorShape() */ Enum::CursorShapeEnum keyboardCursorShape() const; /** * Sets the color used to draw the keyboard cursor. * * The keyboard cursor defaults to using the foreground color of the character * underneath it. * * @param color By default, the widget uses the color of the * character under the cursor to draw the cursor, and inverts the * color of that character to make sure it is still readable. If @p * color is a valid QColor, the widget uses that color to draw the * cursor. If @p color is not an valid QColor, the widget falls back * to the default behavior. */ void setKeyboardCursorColor(const QColor& color); /** * Returns the color of the keyboard cursor, or an invalid color if the keyboard * cursor color is set to change according to the foreground color of the character * underneath it. */ QColor keyboardCursorColor() const; /** * Returns the number of lines of text which can be displayed in the widget. * * This will depend upon the height of the widget and the current font. * See fontHeight() */ int lines() const { return _lines; } /** * Returns the number of characters of text which can be displayed on * each line in the widget. * * This will depend upon the width of the widget and the current font. * See fontWidth() */ int columns() const { return _columns; } /** * Returns the height of the characters in the font used to draw the text in the display. */ int fontHeight() const { return _fontHeight; } /** * Returns the width of the characters in the display. * This assumes the use of a fixed-width font. */ int fontWidth() const { return _fontWidth; } void setSize(int columns, int lines); // reimplemented QSize sizeHint() const; /** * Sets which characters, in addition to letters and numbers, * are regarded as being part of a word for the purposes * of selecting words in the display by double clicking on them. * * The word boundaries occur at the first and last characters which * are either a letter, number, or a character in @p wc * * @param wc An array of characters which are to be considered parts * of a word ( in addition to letters and numbers ). */ void setWordCharacters(const QString& wc); /** * Returns the characters which are considered part of a word for the * purpose of selecting words in the display with the mouse. * * @see setWordCharacters() */ QString wordCharacters() const { return _wordCharacters; } /** * Sets the type of effect used to alert the user when a 'bell' occurs in the * terminal session. * * The terminal session can trigger the bell effect by calling bell() with * the alert message. */ void setBellMode(int mode); /** * Returns the type of effect used to alert the user when a 'bell' occurs in * the terminal session. * * See setBellMode() */ int bellMode() const; /** Play a visual bell for prompt or warning. */ void visualBell(); /** * Specified whether zoom terminal on Ctrl+mousewheel is enabled or not. * Defaults to enabled. */ void setMouseWheelZoom(bool value) { _mouseWheelZoom = value; }; /** * Returns the whether zoom terminal on Ctrl+mousewheel is enabled. * * See setMouseWheelZoom() */ bool mouseWheelZoom() { return _mouseWheelZoom; }; /** * Reimplemented. Has no effect. Use setVTFont() to change the font * used to draw characters in the display. */ virtual void setFont(const QFont &); /** Returns the font used to draw characters in the display */ QFont getVTFont() { return font(); } /** * Sets the font used to draw the display. Has no effect if @p font * is larger than the size of the display itself. */ void setVTFont(const QFont& font); /** Increases the font size */ void increaseFontSize(); /** Decreases the font size */ void decreaseFontSize(); /** * Specified whether anti-aliasing of text in the terminal display * is enabled or not. Defaults to enabled. */ void setAntialias(bool value) { _antialiasText = value; } /** * Returns true if anti-aliasing of text in the terminal is enabled. */ bool antialias() const { return _antialiasText; } /** * Specifies whether characters with intense colors should be rendered * as bold. Defaults to true. */ void setBoldIntense(bool value) { _boldIntense = value; } /** * Returns true if characters with intense colors are rendered in bold. */ bool getBoldIntense() const { return _boldIntense; } /** * Specifies whether line characters will be displayed using font instead * of builtin code. * as bold. Defaults to false. */ void setUseFontLineCharacters(bool value) { _useFontLineCharacters = value; } /** * Returns true if font line characters will be used. */ bool getFontLineCharacters() const { return _useFontLineCharacters; } /** * Sets whether or not the current height and width of the * terminal in lines and columns is displayed whilst the widget * is being resized. */ void setShowTerminalSizeHint(bool on) { _showTerminalSizeHint = on; } /** * Returns whether or not the current height and width of * the terminal in lines and columns is displayed whilst the widget * is being resized. */ bool showTerminalSizeHint() const { return _showTerminalSizeHint; } /** * Sets the status of the BiDi rendering inside the terminal display. * Defaults to disabled. */ void setBidiEnabled(bool set) { _bidiEnabled = set; } /** * Returns the status of the BiDi rendering in this widget. */ bool isBidiEnabled() const { return _bidiEnabled; } /** * Sets the modifiers that shows URL hints when they are pressed * Defaults to disabled. */ void setUrlHintsModifiers(int modifiers) { _urlHintsModifiers = Qt::KeyboardModifiers(modifiers); } /** * Sets the terminal screen section which is displayed in this widget. * When updateImage() is called, the display fetches the latest character image from the * the associated terminal screen window. * * In terms of the model-view paradigm, the ScreenWindow is the model which is rendered * by the TerminalDisplay. */ void setScreenWindow(ScreenWindow* window); /** Returns the terminal screen section which is displayed in this widget. See setScreenWindow() */ ScreenWindow* screenWindow() const; // Select the current line. void selectCurrentLine(); void printContent(QPainter& painter, bool friendly); public slots: /** * Scrolls current ScreenWindow * * it's needed for proper handling scroll commands in the Vt102Emulation class */ void scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode , int amount); /** * Causes the terminal display to fetch the latest character image from the associated * terminal screen ( see setScreenWindow() ) and redraw the display. */ void updateImage(); /** * Causes the terminal display to fetch the latest line status flags from the * associated terminal screen ( see setScreenWindow() ). */ void updateLineProperties(); void setAutoCopySelectedText(bool enabled); void setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode); /** Copies the selected text to the X11 Selection. */ void copyToX11Selection(); /** Copies the selected text to the system clipboard. */ void copyToClipboard(); /** * Pastes the content of the clipboard into the * display. */ void pasteFromClipboard(bool appendEnter = false); /** * Pastes the content of the X11 selection into the * display. */ void pasteFromX11Selection(bool appendEnter = false); /** * Changes whether the flow control warning box should be shown when the flow control * stop key (Ctrl+S) are pressed. */ void setFlowControlWarningEnabled(bool enabled); /** * Returns true if the flow control warning box is enabled. * See outputSuspended() and setFlowControlWarningEnabled() */ bool flowControlWarningEnabled() const { return _flowControlWarningEnabled; } /** * Causes the widget to display or hide a message informing the user that terminal * output has been suspended (by using the flow control key combination Ctrl+S) * * @param suspended True if terminal output has been suspended and the warning message should * be shown or false to indicate that terminal output has been resumed and that * the warning message should disappear. */ void outputSuspended(bool suspended); /** * Sets whether the program whose output is being displayed in the view * is interested in mouse events. * * If this is set to true, mouse signals will be emitted by the view when the user clicks, drags * or otherwise moves the mouse inside the view. * The user interaction needed to create selections will also change, and the user will be required * to hold down the shift key to create a selection or perform other mouse activities inside the * view area - since the program running in the terminal is being allowed to handle normal mouse * events itself. * * @param usesMouse Set to true if the program running in the terminal is interested in mouse events * or false otherwise. */ void setUsesMouse(bool usesMouse); /** See setUsesMouse() */ bool usesMouse() const; void setBracketedPasteMode(bool bracketedPasteMode); bool bracketedPasteMode() const; /** * Shows a notification that a bell event has occurred in the terminal. * TODO: More documentation here */ void bell(const QString& message); /** * Gets the background of the display * @see setBackgroundColor(), setColorTable(), setForegroundColor() */ QColor getBackgroundColor() const; /** * Sets the background of the display to the specified color. * @see setColorTable(), getBackgroundColor(), setForegroundColor() */ void setBackgroundColor(const QColor& color); /** * Sets the text of the display to the specified color. * @see setColorTable(), setBackgroundColor(), getBackgroundColor() */ void setForegroundColor(const QColor& color); /** * Sets the display's contents margins. */ void setMargin(int margin); /** * Sets whether the contents are centered between the margins. */ void setCenterContents(bool enable); signals: /** * Emitted when the user presses a key whilst the terminal widget has focus. */ void keyPressedSignal(QKeyEvent* event); /** * A mouse event occurred. * @param button The mouse button (0 for left button, 1 for middle button, 2 for right button, 3 for release) * @param column The character column where the event occurred * @param line The character row where the event occurred * @param eventType The type of event. 0 for a mouse press / release or 1 for mouse motion */ void mouseSignal(int button, int column, int line, int eventType); void changedFontMetricSignal(int height, int width); void changedContentSizeSignal(int height, int width); /** * Emitted when the user right clicks on the display, or right-clicks with the Shift * key held down if usesMouse() is true. * * This can be used to display a context menu. */ void configureRequest(const QPoint& position); /** * When a shortcut which is also a valid terminal key sequence is pressed while * the terminal widget has focus, this signal is emitted to allow the host to decide * whether the shortcut should be overridden. * When the shortcut is overridden, the key sequence will be sent to the terminal emulation instead * and the action associated with the shortcut will not be triggered. * * @p override is set to false by default and the shortcut will be triggered as normal. */ void overrideShortcutCheck(QKeyEvent* keyEvent, bool& override); void sendStringToEmu(const QByteArray& local8BitString); void focusLost(); void focusGained(); protected: virtual bool event(QEvent* event); virtual void paintEvent(QPaintEvent* event); virtual void showEvent(QShowEvent* event); virtual void hideEvent(QHideEvent* event); virtual void resizeEvent(QResizeEvent* event); virtual void contextMenuEvent(QContextMenuEvent* event); virtual void fontChange(const QFont&); virtual void focusInEvent(QFocusEvent* event); virtual void focusOutEvent(QFocusEvent* event); virtual void keyPressEvent(QKeyEvent* event); virtual void keyReleaseEvent(QKeyEvent* event); virtual void leaveEvent(QEvent* event); virtual void mouseDoubleClickEvent(QMouseEvent* event); virtual void mousePressEvent(QMouseEvent* event); virtual void mouseReleaseEvent(QMouseEvent* event); virtual void mouseMoveEvent(QMouseEvent* event); virtual void extendSelection(const QPoint& pos); virtual void wheelEvent(QWheelEvent* event); virtual bool focusNextPrevChild(bool next); // drag and drop virtual void dragEnterEvent(QDragEnterEvent* event); virtual void dropEvent(QDropEvent* event); void doDrag(); enum DragState { diNone, diPending, diDragging }; struct DragInfo { DragState state; QPoint start; QDrag* dragObject; } _dragInfo; // classifies the 'ch' into one of three categories // and returns a character to indicate which category it is in // // - A space (returns ' ') // - Part of a word (returns 'a') // - Other characters (returns the input character) QChar charClass(const Character& ch) const; void clearImage(); void mouseTripleClickEvent(QMouseEvent* event); void selectLine(QPoint pos, bool entireLine); // reimplemented virtual void inputMethodEvent(QInputMethodEvent* event); virtual QVariant inputMethodQuery(Qt::InputMethodQuery query) const; protected slots: void scrollBarPositionChanged(int value); void blinkTextEvent(); void blinkCursorEvent(); private slots: void unmaskBell(); void swapFGBGColors(); void tripleClickTimeout(); // resets possibleTripleClick void viewScrolledByUser(); /** * Called from the drag-and-drop popup. Causes the dropped URLs to be pasted as text. */ void dropMenuPasteActionTriggered(); void dropMenuCdActionTriggered(); void dismissOutputSuspendedMessage(); private: // -- Drawing helpers -- // divides the part of the display specified by 'rect' into // fragments according to their colors and styles and calls // drawTextFragment() or drawPrinterFriendlyTextFragment() // to draw the fragments void drawContents(QPainter& painter, const QRect& rect); // draw a transparent rectangle over the line of the current match void drawCurrentResultRect(QPainter& painter); // draws a section of text, all the text in this section // has a common color and style void drawTextFragment(QPainter& painter, const QRect& rect, const QString& text, const Character* style); void drawPrinterFriendlyTextFragment(QPainter& painter, const QRect& rect, const QString& text, const Character* style); // draws the background for a text fragment // if useOpacitySetting is true then the color's alpha value will be set to // the display's transparency (set with setOpacity()), otherwise the background // will be drawn fully opaque void drawBackground(QPainter& painter, const QRect& rect, const QColor& color, bool useOpacitySetting); // draws the cursor character void drawCursor(QPainter& painter, const QRect& rect , const QColor& foregroundColor, const QColor& backgroundColor , bool& invertColors); // draws the characters or line graphics in a text fragment void drawCharacters(QPainter& painter, const QRect& rect, const QString& text, const Character* style, bool invertCharacterColor); // draws a string of line graphics void drawLineCharString(QPainter& painter, int x, int y, const QString& str, const Character* attributes); // draws the preedit string for input methods void drawInputMethodPreeditString(QPainter& painter , const QRect& rect); // -- // maps an area in the character image to an area on the widget QRect imageToWidget(const QRect& imageArea) const; // maps a point on the widget to the position ( ie. line and column ) // of the character at that point. void getCharacterPosition(const QPoint& widgetPoint, int& line, int& column) const; // the area where the preedit string for input methods will be draw QRect preeditRect() const; // shows a notification window in the middle of the widget indicating the terminal's // current size in columns and lines void showResizeNotification(); // scrolls the image by a number of lines. // 'lines' may be positive ( to scroll the image down ) // or negative ( to scroll the image up ) // 'region' is the part of the image to scroll - currently only // the top, bottom and height of 'region' are taken into account, // the left and right are ignored. void scrollImage(int lines , const QRect& region); void calcGeometry(); void propagateSize(); void updateImageSize(); void makeImage(); void paintFilters(QPainter& painter); // returns a region covering all of the areas of the widget which contain // a hotspot QRegion hotSpotRegion() const; // returns the position of the cursor in columns and lines QPoint cursorPosition() const; // redraws the cursor void updateCursor(); bool handleShortcutOverrideEvent(QKeyEvent* event); void doPaste(QString text, bool appendReturn); void processMidButtonClick(QMouseEvent* event); QPoint findLineStart(const QPoint &pnt); QPoint findLineEnd(const QPoint &pnt); QPoint findWordStart(const QPoint &pnt); QPoint findWordEnd(const QPoint &pnt); // the window onto the terminal screen which this display // is currently showing. QPointer _screenWindow; bool _bellMasked; QGridLayout* _gridLayout; bool _fixedFont; // has fixed pitch int _fontHeight; // height int _fontWidth; // width int _fontAscent; // ascend bool _boldIntense; // Whether intense colors should be rendered with bold font int _leftMargin; // offset int _topMargin; // offset int _lines; // the number of lines that can be displayed in the widget int _columns; // the number of columns that can be displayed in the widget int _usedLines; // the number of lines that are actually being used, this will be less // than 'lines' if the character image provided with setImage() is smaller // than the maximum image size which can be displayed int _usedColumns; // the number of columns that are actually being used, this will be less // than 'columns' if the character image provided with setImage() is smaller // than the maximum image size which can be displayed QRect _contentRect; Character* _image; // [lines][columns] // only the area [usedLines][usedColumns] in the image contains valid data int _imageSize; QVector _lineProperties; ColorEntry _colorTable[TABLE_COLORS]; uint _randomSeed; bool _resizing; bool _showTerminalSizeHint; bool _bidiEnabled; bool _mouseMarks; bool _bracketedPasteMode; QPoint _iPntSel; // initial selection point QPoint _pntSel; // current selection point QPoint _tripleSelBegin; // help avoid flicker int _actSel; // selection state bool _wordSelectionMode; bool _lineSelectionMode; bool _preserveLineBreaks; bool _columnSelectionMode; bool _autoCopySelectedText; Enum::MiddleClickPasteModeEnum _middleClickPasteMode; QScrollBar* _scrollBar; Enum::ScrollBarPositionEnum _scrollbarLocation; bool _scrollFullPage; QString _wordCharacters; int _bellMode; bool _allowBlinkingText; // allow text to blink bool _allowBlinkingCursor; // allow cursor to blink bool _textBlinking; // text is blinking, hide it when drawing bool _cursorBlinking; // cursor is blinking, hide it when drawing bool _hasTextBlinker; // has characters to blink QTimer* _blinkTextTimer; QTimer* _blinkCursorTimer; Qt::KeyboardModifiers _urlHintsModifiers; bool _showUrlHint; - bool _underlineLinks; // Underline URL and hosts on mouse hover bool _openLinksByDirectClick; // Open URL and hosts by single mouse click bool _ctrlRequiredForDrag; // require Ctrl key for drag selected text bool _dropUrlsAsText; // always paste URLs as text without showing copy/move menu Enum::TripleClickModeEnum _tripleClickMode; bool _possibleTripleClick; // is set in mouseDoubleClickEvent and deleted // after QApplication::doubleClickInterval() delay QLabel* _resizeWidget; QTimer* _resizeTimer; bool _flowControlWarningEnabled; //widgets related to the warning message that appears when the user presses Ctrl+S to suspend //terminal output - informing them what has happened and how to resume output QLabel* _outputSuspendedLabel; uint _lineSpacing; QSize _size; QRgb _blendColor; ColorSchemeWallpaper::Ptr _wallpaper; // list of filters currently applied to the display. used for links and // search highlight TerminalImageFilterChain* _filterChain; QRegion _mouseOverHotspotArea; bool _filterUpdateRequired; Enum::CursorShapeEnum _cursorShape; // cursor color. If it is invalid (by default) then the foreground // color of the character under the cursor is used QColor _cursorColor; struct InputMethodData { QString preeditString; QRect previousPreeditRect; }; InputMethodData _inputMethodData; bool _antialiasText; // do we anti-alias or not bool _useFontLineCharacters; bool _printerFriendly; // are we currently painting to a printer in black/white mode //the delay in milliseconds between redrawing blinking text static const int TEXT_BLINK_DELAY = 500; //the duration of the size hint in milliseconds static const int SIZE_HINT_DURATION = 1000; SessionController* _sessionController; bool _trimTrailingSpaces; // trim trailing spaces in selected text bool _mouseWheelZoom; // enable mouse wheel zooming or not int _margin; // the contents margin bool _centerContents; // center the contents between margins qreal _opacity; ScrollState _scrollWheelState; friend class TerminalDisplayAccessible; }; class AutoScrollHandler : public QObject { Q_OBJECT public: explicit AutoScrollHandler(QWidget* parent); protected: virtual void timerEvent(QTimerEvent* event); virtual bool eventFilter(QObject* watched, QEvent* event); private: QWidget* widget() const { return static_cast(parent()); } int _timerId; }; } #endif // TERMINALDISPLAY_H diff --git a/src/ViewManager.cpp b/src/ViewManager.cpp index 0f14ca0a..81ccab13 100644 --- a/src/ViewManager.cpp +++ b/src/ViewManager.cpp @@ -1,1118 +1,1117 @@ /* Copyright 2006-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 "ViewManager.h" #include // Qt #include #include #include #include // KDE #include #include #include #include // Konsole #include #include "ColorScheme.h" #include "ColorSchemeManager.h" #include "Session.h" #include "TerminalDisplay.h" #include "SessionController.h" #include "SessionManager.h" #include "ProfileManager.h" #include "ViewSplitter.h" #include "Enumeration.h" using namespace Konsole; int ViewManager::lastManagerId = 0; ViewManager::ViewManager(QObject* parent , KActionCollection* collection) : QObject(parent) , _viewSplitter(0) , _actionCollection(collection) , _containerSignalMapper(new QSignalMapper(this)) , _navigationMethod(TabbedNavigation) , _navigationVisibility(ViewContainer::AlwaysShowNavigation) , _navigationPosition(ViewContainer::NavigationPositionTop) , _showQuickButtons(false) , _newTabBehavior(PutNewTabAtTheEnd) , _navigationStyleSheet(QString()) , _managerId(0) { // create main view area _viewSplitter = new ViewSplitter(0); KAcceleratorManager::setNoAccel(_viewSplitter); // the ViewSplitter class supports both recursive and non-recursive splitting, // in non-recursive mode, all containers are inserted into the same top-level splitter // widget, and all the divider lines between the containers have the same orientation // // the ViewManager class is not currently able to handle a ViewSplitter in recursive-splitting // mode _viewSplitter->setRecursiveSplitting(false); _viewSplitter->setFocusPolicy(Qt::NoFocus); // setup actions which are related to the views setupActions(); // emit a signal when all of the views held by this view manager are destroyed connect(_viewSplitter.data() , &Konsole::ViewSplitter::allContainersEmpty , this , &Konsole::ViewManager::empty); connect(_viewSplitter.data() , &Konsole::ViewSplitter::empty , this , &Konsole::ViewManager::empty); // listen for addition or removal of views from associated containers connect(_containerSignalMapper , static_cast(&QSignalMapper::mapped) , this , &Konsole::ViewManager::containerViewsChanged); // listen for profile changes connect(ProfileManager::instance() , &Konsole::ProfileManager::profileChanged , this, &Konsole::ViewManager::profileChanged); connect(SessionManager::instance() , &Konsole::SessionManager::sessionUpdated , this, &Konsole::ViewManager::updateViewsForSession); //prepare DBus communication new WindowAdaptor(this); _managerId = ++lastManagerId; QDBusConnection::sessionBus().registerObject(QLatin1String("/Windows/") + QString::number(_managerId), this); } ViewManager::~ViewManager() { } int ViewManager::managerId() const { return _managerId; } QWidget* ViewManager::activeView() const { ViewContainer* container = _viewSplitter->activeContainer(); if (container) { return container->activeView(); } else { return 0; } } QWidget* ViewManager::widget() const { return _viewSplitter; } void ViewManager::setupActions() { KActionCollection* collection = _actionCollection; QAction* nextViewAction = new QAction(i18nc("@action Shortcut entry", "Next Tab") , this); QAction* previousViewAction = new QAction(i18nc("@action Shortcut entry", "Previous Tab") , this); QAction* lastViewAction = new QAction(i18nc("@action Shortcut entry", "Switch to Last Tab") , this); QAction* nextContainerAction = new QAction(i18nc("@action Shortcut entry", "Next View Container") , this); QAction* moveViewLeftAction = new QAction(i18nc("@action Shortcut entry", "Move Tab Left") , this); QAction* moveViewRightAction = new QAction(i18nc("@action Shortcut entry", "Move Tab Right") , this); // list of actions that should only be enabled when there are multiple view // containers open QList multiViewOnlyActions; multiViewOnlyActions << nextContainerAction; if (collection) { QAction* splitLeftRightAction = new QAction(QIcon::fromTheme(QStringLiteral("view-split-left-right")), i18nc("@action:inmenu", "Split View Left/Right"), this); collection->setDefaultShortcut(splitLeftRightAction, Qt::CTRL + Qt::Key_ParenLeft); collection->addAction("split-view-left-right", splitLeftRightAction); connect(splitLeftRightAction , &QAction::triggered , this , &Konsole::ViewManager::splitLeftRight); QAction* splitTopBottomAction = new QAction(QIcon::fromTheme(QStringLiteral("view-split-top-bottom")) , i18nc("@action:inmenu", "Split View Top/Bottom"), this); collection->setDefaultShortcut(splitTopBottomAction, Qt::CTRL + Qt::Key_ParenRight); collection->addAction("split-view-top-bottom", splitTopBottomAction); connect(splitTopBottomAction , &QAction::triggered , this , &Konsole::ViewManager::splitTopBottom); QAction* closeActiveAction = new QAction(i18nc("@action:inmenu Close Active View", "Close Active") , this); closeActiveAction->setIcon(QIcon::fromTheme(QStringLiteral("view-close"))); collection->setDefaultShortcut(closeActiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_S); closeActiveAction->setEnabled(false); collection->addAction("close-active-view", closeActiveAction); connect(closeActiveAction , &QAction::triggered , this , &Konsole::ViewManager::closeActiveContainer); multiViewOnlyActions << closeActiveAction; QAction* closeOtherAction = new QAction(i18nc("@action:inmenu Close Other Views", "Close Others") , this); collection->setDefaultShortcut(closeOtherAction, Qt::CTRL + Qt::SHIFT + Qt::Key_O); closeOtherAction->setEnabled(false); collection->addAction("close-other-views", closeOtherAction); connect(closeOtherAction , &QAction::triggered , this , &Konsole::ViewManager::closeOtherContainers); multiViewOnlyActions << closeOtherAction; // Expand & Shrink Active View QAction* expandActiveAction = new QAction(i18nc("@action:inmenu", "Expand View") , this); collection->setDefaultShortcut(expandActiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_BracketRight); expandActiveAction->setEnabled(false); collection->addAction("expand-active-view", expandActiveAction); connect(expandActiveAction , &QAction::triggered , this , &Konsole::ViewManager::expandActiveContainer); multiViewOnlyActions << expandActiveAction; QAction* shrinkActiveAction = new QAction(i18nc("@action:inmenu", "Shrink View") , this); collection->setDefaultShortcut(shrinkActiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_BracketLeft); shrinkActiveAction->setEnabled(false); collection->addAction("shrink-active-view", shrinkActiveAction); connect(shrinkActiveAction , &QAction::triggered , this , &Konsole::ViewManager::shrinkActiveContainer); multiViewOnlyActions << shrinkActiveAction; #if defined(ENABLE_DETACHING) QAction* detachViewAction = collection->addAction("detach-view"); detachViewAction->setIcon(QIcon::fromTheme(QStringLiteral("tab-detach"))); detachViewAction->setText(i18nc("@action:inmenu", "D&etach Current Tab")); // Ctrl+Shift+D is not used as a shortcut by default because it is too close // to Ctrl+D - which will terminate the session in many cases collection->setDefaultShortcut(detachViewAction, Qt::CTRL + Qt::SHIFT + Qt::Key_H); connect(this , &Konsole::ViewManager::splitViewToggle , this , &Konsole::ViewManager::updateDetachViewState); connect(detachViewAction , &QAction::triggered , this , &Konsole::ViewManager::detachActiveView); #endif // Next / Previous View , Next Container collection->addAction("next-view", nextViewAction); collection->addAction("previous-view", previousViewAction); collection->addAction("last-tab", lastViewAction); collection->addAction("next-container", nextContainerAction); collection->addAction("move-view-left", moveViewLeftAction); collection->addAction("move-view-right", moveViewRightAction); // Switch to tab N shortcuts const int SWITCH_TO_TAB_COUNT = 19; QSignalMapper* switchToTabMapper = new QSignalMapper(this); connect(switchToTabMapper, static_cast(&QSignalMapper::mapped), this, &Konsole::ViewManager::switchToView); for (int i = 0; i < SWITCH_TO_TAB_COUNT; i++) { QAction* switchToTabAction = new QAction(i18nc("@action Shortcut entry", "Switch to Tab %1", i + 1), this); switchToTabMapper->setMapping(switchToTabAction, i); connect(switchToTabAction, &QAction::triggered, switchToTabMapper, static_cast(&QSignalMapper::map)); collection->addAction(QString("switch-to-tab-%1").arg(i), switchToTabAction); } } foreach(QAction* action, multiViewOnlyActions) { connect(this , &Konsole::ViewManager::splitViewToggle , action , &QAction::setEnabled); } // keyboard shortcut only actions collection->setDefaultShortcut(nextViewAction, Qt::SHIFT + Qt::Key_Right); connect(nextViewAction, &QAction::triggered , this , &Konsole::ViewManager::nextView); _viewSplitter->addAction(nextViewAction); collection->setDefaultShortcut(previousViewAction, Qt::SHIFT + Qt::Key_Left); connect(previousViewAction, &QAction::triggered , this , &Konsole::ViewManager::previousView); _viewSplitter->addAction(previousViewAction); collection->setDefaultShortcut(nextContainerAction, Qt::SHIFT + Qt::Key_Tab); connect(nextContainerAction , &QAction::triggered , this , &Konsole::ViewManager::nextContainer); _viewSplitter->addAction(nextContainerAction); collection->setDefaultShortcut(moveViewLeftAction, Qt::CTRL + Qt::SHIFT + Qt::Key_Left); connect(moveViewLeftAction , &QAction::triggered , this , &Konsole::ViewManager::moveActiveViewLeft); _viewSplitter->addAction(moveViewLeftAction); collection->setDefaultShortcut(moveViewRightAction, Qt::CTRL + Qt::SHIFT + Qt::Key_Right); connect(moveViewRightAction , &QAction::triggered , this , &Konsole::ViewManager::moveActiveViewRight); _viewSplitter->addAction(moveViewRightAction); connect(lastViewAction, &QAction::triggered , this , &Konsole::ViewManager::lastView); _viewSplitter->addAction(lastViewAction); } void ViewManager::switchToView(int index) { Q_ASSERT(index >= 0); ViewContainer* container = _viewSplitter->activeContainer(); Q_ASSERT(container); QList containerViews = container->views(); if (index >= containerViews.count()) return; container->setActiveView(containerViews.at(index)); } void ViewManager::updateDetachViewState() { if (!_actionCollection) return; const bool splitView = _viewSplitter->containers().count() >= 2; auto activeContainer = _viewSplitter->activeContainer(); const bool shouldEnable = splitView || (activeContainer && activeContainer->views().count() >= 2); QAction* detachAction = _actionCollection->action("detach-view"); if (detachAction && shouldEnable != detachAction->isEnabled()) detachAction->setEnabled(shouldEnable); } void ViewManager::moveActiveViewLeft() { ViewContainer* container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->moveActiveView(ViewContainer::MoveViewLeft); } void ViewManager::moveActiveViewRight() { ViewContainer* container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->moveActiveView(ViewContainer::MoveViewRight); } void ViewManager::nextContainer() { _viewSplitter->activateNextContainer(); } void ViewManager::nextView() { ViewContainer* container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->activateNextView(); } void ViewManager::previousView() { ViewContainer* container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->activatePreviousView(); } void ViewManager::lastView() { ViewContainer* container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->activateLastView(); } void ViewManager::detachActiveView() { // find the currently active view and remove it from its container ViewContainer* container = _viewSplitter->activeContainer(); detachView(container, container->activeView()); } void ViewManager::detachView(ViewContainer* container, QWidget* widgetView) { #if !defined(ENABLE_DETACHING) return; #endif TerminalDisplay * viewToDetach = qobject_cast(widgetView); if (!viewToDetach) return; emit viewDetached(_sessionMap[viewToDetach]); _sessionMap.remove(viewToDetach); // remove the view from this window container->removeView(viewToDetach); viewToDetach->deleteLater(); // if the container from which the view was removed is now empty then it can be deleted, // unless it is the only container in the window, in which case it is left empty // so that there is always an active container if (_viewSplitter->containers().count() > 1 && container->views().count() == 0) { removeContainer(container); } } void ViewManager::sessionFinished() { // if this slot is called after the view manager's main widget // has been destroyed, do nothing if (!_viewSplitter) return; Session* session = qobject_cast(sender()); Q_ASSERT(session); // close attached views QList children = _viewSplitter->findChildren(); foreach(TerminalDisplay* view , children) { if (_sessionMap[view] == session) { _sessionMap.remove(view); view->deleteLater(); } } // This is needed to remove this controller from factory() in // order to prevent BUG: 185466 - disappearing menu popup if (_pluggedController) emit unplugController(_pluggedController); } void ViewManager::viewActivated(QWidget* view) { Q_ASSERT(view != 0); // focus the activated view, this will cause the SessionController // to notify the world that the view has been focused and the appropriate UI // actions will be plugged in. view->setFocus(Qt::OtherFocusReason); } void ViewManager::splitLeftRight() { splitView(Qt::Horizontal); } void ViewManager::splitTopBottom() { splitView(Qt::Vertical); } void ViewManager::splitView(Qt::Orientation orientation) { ViewContainer* container = createContainer(); // iterate over each session which has a view in the current active // container and create a new view for that session in a new container foreach(QWidget* view, _viewSplitter->activeContainer()->views()) { Session* session = _sessionMap[qobject_cast(view)]; TerminalDisplay* display = createTerminalDisplay(session); const Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); applyProfileToView(display, profile); ViewProperties* properties = createController(session, display); _sessionMap[display] = session; container->addView(display, properties); session->addView(display); } _viewSplitter->addContainer(container, orientation); emit splitViewToggle(_viewSplitter->containers().count() > 0); // focus the new container container->containerWidget()->setFocus(); // ensure that the active view is focused after the split / unsplit ViewContainer* activeContainer = _viewSplitter->activeContainer(); QWidget* activeView = activeContainer ? activeContainer->activeView() : 0; if (activeView) activeView->setFocus(Qt::OtherFocusReason); } void ViewManager::removeContainer(ViewContainer* container) { // remove session map entries for views in this container foreach(QWidget* view , container->views()) { TerminalDisplay* display = qobject_cast(view); Q_ASSERT(display); _sessionMap.remove(display); } _viewSplitter->removeContainer(container); container->deleteLater(); emit splitViewToggle(_viewSplitter->containers().count() > 1); } void ViewManager::expandActiveContainer() { _viewSplitter->adjustContainerSize(_viewSplitter->activeContainer(), 10); } void ViewManager::shrinkActiveContainer() { _viewSplitter->adjustContainerSize(_viewSplitter->activeContainer(), -10); } void ViewManager::closeActiveContainer() { // only do something if there is more than one container active if (_viewSplitter->containers().count() > 1) { ViewContainer* container = _viewSplitter->activeContainer(); removeContainer(container); // focus next container so that user can continue typing // without having to manually focus it themselves nextContainer(); } } void ViewManager::closeOtherContainers() { ViewContainer* active = _viewSplitter->activeContainer(); foreach(ViewContainer* container, _viewSplitter->containers()) { if (container != active) removeContainer(container); } } SessionController* ViewManager::createController(Session* session , TerminalDisplay* view) { // create a new controller for the session, and ensure that this view manager // is notified when the view gains the focus SessionController* controller = new SessionController(session, view, this); connect(controller , &Konsole::SessionController::focused , this , &Konsole::ViewManager::controllerChanged); connect(session , &Konsole::Session::destroyed , controller , &Konsole::SessionController::deleteLater); connect(session , &Konsole::Session::primaryScreenInUse , controller , &Konsole::SessionController::setupPrimaryScreenSpecificActions); connect(session , &Konsole::Session::selectionChanged , controller , &Konsole::SessionController::selectionChanged); connect(view , &Konsole::TerminalDisplay::destroyed , controller , &Konsole::SessionController::deleteLater); // if this is the first controller created then set it as the active controller if (!_pluggedController) controllerChanged(controller); return controller; } void ViewManager::controllerChanged(SessionController* controller) { if (controller == _pluggedController) return; _viewSplitter->setFocusProxy(controller->view()); _pluggedController = controller; emit activeViewChanged(controller); } SessionController* ViewManager::activeViewController() const { return _pluggedController; } IncrementalSearchBar* ViewManager::searchBar() const { return _viewSplitter->activeSplitter()->activeContainer()->searchBar(); } void ViewManager::createView(Session* session, ViewContainer* container, int index) { // notify this view manager when the session finishes so that its view // can be deleted // // Use Qt::UniqueConnection to avoid duplicate connection connect(session, &Konsole::Session::finished, this, &Konsole::ViewManager::sessionFinished, Qt::UniqueConnection); TerminalDisplay* display = createTerminalDisplay(session); const Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); applyProfileToView(display, profile); // set initial size const QSize& preferredSize = session->preferredSize(); // FIXME: +1 is needed here for getting the expected rows // Note that the display shouldn't need to take into account the tabbar. // However, it appears that taking into account the tabbar is needed. // If tabbar is not visible, no +1 is needed here; however, depending on // settings/tabbar style, +2 might be needed. // 1st attempt at fixing the above: // Guess if tabbar will NOT be visible; ignore ShowNavigationAsNeeded int heightAdjustment = 0; if (_navigationVisibility != ViewContainer::AlwaysHideNavigation) { heightAdjustment = 2; } display->setSize(preferredSize.width(), preferredSize.height() + heightAdjustment); ViewProperties* properties = createController(session, display); _sessionMap[display] = session; container->addView(display, properties, index); session->addView(display); // tell the session whether it has a light or dark background session->setDarkBackground(colorSchemeForProfile(profile)->hasDarkBackground()); if (container == _viewSplitter->activeContainer()) { container->setActiveView(display); display->setFocus(Qt::OtherFocusReason); } updateDetachViewState(); } void ViewManager::createView(Session* session) { // create the default container if (_viewSplitter->containers().count() == 0) { ViewContainer* container = createContainer(); _viewSplitter->addContainer(container, Qt::Vertical); emit splitViewToggle(false); } // new tab will be put at the end by default. int index = -1; if (_newTabBehavior == PutNewTabAfterCurrentTab) { QWidget* view = activeView(); if (view) { QList views = _viewSplitter->activeContainer()->views(); index = views.indexOf(view) + 1; } } // iterate over the view containers owned by this view manager // and create a new terminal display for the session in each of them, along with // a controller for the session/display pair foreach(ViewContainer* container, _viewSplitter->containers()) { createView(session, container, index); } } ViewContainer* ViewManager::createContainer() { ViewContainer* container = 0; switch (_navigationMethod) { case TabbedNavigation: { TabbedViewContainer* tabbedContainer = new TabbedViewContainer(_navigationPosition, this, _viewSplitter); container = tabbedContainer; connect(tabbedContainer, &TabbedViewContainer::detachTab, this, &ViewManager::detachView); connect(tabbedContainer, &TabbedViewContainer::closeTab, this, &ViewManager::closeTabFromContainer); } break; case NoNavigation: default: container = new StackedViewContainer(_viewSplitter); } // FIXME: these code feels duplicated container->setNavigationVisibility(_navigationVisibility); container->setNavigationPosition(_navigationPosition); container->setStyleSheet(_navigationStyleSheet); if (_showQuickButtons) { container->setFeatures(container->features() | ViewContainer::QuickNewView | ViewContainer::QuickCloseView); } else { container->setFeatures(container->features() & ~ViewContainer::QuickNewView & ~ViewContainer::QuickCloseView); } // connect signals and slots connect(container , &Konsole::ViewContainer::viewAdded , _containerSignalMapper , static_cast(&QSignalMapper::map)); connect(container , &Konsole::ViewContainer::viewRemoved , _containerSignalMapper , static_cast(&QSignalMapper::map)); _containerSignalMapper->setMapping(container, container); connect(container, static_cast(&Konsole::ViewContainer::newViewRequest), this, static_cast(&Konsole::ViewManager::newViewRequest)); connect(container, static_cast(&Konsole::ViewContainer::newViewRequest), this, static_cast(&Konsole::ViewManager::newViewRequest)); connect(container, &Konsole::ViewContainer::moveViewRequest, this , &Konsole::ViewManager::containerMoveViewRequest); connect(container , &Konsole::ViewContainer::viewRemoved , this , &Konsole::ViewManager::viewDestroyed); connect(container , &Konsole::ViewContainer::activeViewChanged , this , &Konsole::ViewManager::viewActivated); return container; } void ViewManager::containerMoveViewRequest(int index, int id, bool& moved, TabbedViewContainer* sourceTabbedContainer) { ViewContainer* container = qobject_cast(sender()); SessionController* controller = qobject_cast(ViewProperties::propertiesById(id)); if (!controller) return; // do not move the last tab in a split view. if (sourceTabbedContainer) { QPointer sourceContainer = qobject_cast(sourceTabbedContainer); if (_viewSplitter->containers().contains(sourceContainer)) { return; } else { ViewManager* sourceViewManager = sourceTabbedContainer->connectedViewManager(); // do not remove the last tab on the window if (qobject_cast(sourceViewManager->widget())->containers().size() > 1) { return; } } } createView(controller->session(), container, index); controller->session()->refresh(); moved = true; } void ViewManager::setNavigationMethod(NavigationMethod method) { _navigationMethod = method; KActionCollection* collection = _actionCollection; if (collection) { // FIXME: The following disables certain actions for the KPart that it // doesn't actually have a use for, to avoid polluting the action/shortcut // namespace of an application using the KPart (otherwise, a shortcut may // be in use twice, and the user gets to see an "ambiguous shortcut over- // load" error dialog). However, this approach sucks - it's the inverse of // what it should be. Rather than disabling actions not used by the KPart, // a method should be devised to only enable those that are used, perhaps // by using a separate action collection. const bool enable = (_navigationMethod != NoNavigation); QAction* action; action = collection->action("next-view"); if (action) action->setEnabled(enable); action = collection->action("previous-view"); if (action) action->setEnabled(enable); action = collection->action("last-tab"); if (action) action->setEnabled(enable); action = collection->action("split-view-left-right"); if (action) action->setEnabled(enable); action = collection->action("split-view-top-bottom"); if (action) action->setEnabled(enable); action = collection->action("rename-session"); if (action) action->setEnabled(enable); action = collection->action("move-view-left"); if (action) action->setEnabled(enable); action = collection->action("move-view-right"); if (action) action->setEnabled(enable); } } ViewManager::NavigationMethod ViewManager::navigationMethod() const { return _navigationMethod; } void ViewManager::containerViewsChanged(QObject* container) { if (_viewSplitter && container == _viewSplitter->activeContainer()) { emit viewPropertiesChanged(viewProperties()); } } void ViewManager::viewDestroyed(QWidget* view) { // Note: the received QWidget has already been destroyed, so // using dynamic_cast<> or qobject_cast<> does not work here TerminalDisplay* display = static_cast(view); Q_ASSERT(display); // 1. detach view from session // 2. if the session has no views left, close it Session* session = _sessionMap[ display ]; _sessionMap.remove(display); if (session) { if (session->views().count() == 0) session->close(); } //we only update the focus if the splitter is still alive if (_viewSplitter) { updateDetachViewState(); } // The below causes the menus to be messed up // Only happens when using the tab bar close button // if (_pluggedController) // emit unplugController(_pluggedController); } TerminalDisplay* ViewManager::createTerminalDisplay(Session* session) { TerminalDisplay* display = new TerminalDisplay(0); display->setRandomSeed(session->sessionId() * 31); return display; } const ColorScheme* ViewManager::colorSchemeForProfile(const Profile::Ptr profile) { const ColorScheme* colorScheme = ColorSchemeManager::instance()-> findColorScheme(profile->colorScheme()); if (!colorScheme) colorScheme = ColorSchemeManager::instance()->defaultColorScheme(); Q_ASSERT(colorScheme); return colorScheme; } void ViewManager::applyProfileToView(TerminalDisplay* view , const Profile::Ptr profile) { Q_ASSERT(profile); emit updateWindowIcon(); // load color scheme ColorEntry table[TABLE_COLORS]; const ColorScheme* colorScheme = colorSchemeForProfile(profile); colorScheme->getColorTable(table , view->randomSeed()); view->setColorTable(table); view->setOpacity(colorScheme->opacity()); view->setWallpaper(colorScheme->wallpaper()); // load font view->setAntialias(profile->antiAliasFonts()); view->setBoldIntense(profile->boldIntense()); view->setUseFontLineCharacters(profile->useFontLineCharacters()); view->setVTFont(profile->font()); // set scroll-bar position int scrollBarPosition = profile->property(Profile::ScrollBarPosition); if (scrollBarPosition == Enum::ScrollBarLeft) view->setScrollBarPosition(Enum::ScrollBarLeft); else if (scrollBarPosition == Enum::ScrollBarRight) view->setScrollBarPosition(Enum::ScrollBarRight); else if (scrollBarPosition == Enum::ScrollBarHidden) view->setScrollBarPosition(Enum::ScrollBarHidden); bool scrollFullPage = profile->property(Profile::ScrollFullPage); view->setScrollFullPage(scrollFullPage); // show hint about terminal size after resizing view->setShowTerminalSizeHint(profile->showTerminalSizeHint()); // terminal features view->setBlinkingCursorEnabled(profile->blinkingCursorEnabled()); view->setBlinkingTextEnabled(profile->blinkingTextEnabled()); int tripleClickMode = profile->property(Profile::TripleClickMode); view->setTripleClickMode(Enum::TripleClickModeEnum(tripleClickMode)); view->setAutoCopySelectedText(profile->autoCopySelectedText()); - view->setUnderlineLinks(profile->underlineLinksEnabled()); view->setControlDrag(profile->property(Profile::CtrlRequiredForDrag)); view->setDropUrlsAsText(profile->property(Profile::DropUrlsAsText)); view->setBidiEnabled(profile->bidiRenderingEnabled()); view->setLineSpacing(profile->lineSpacing()); view->setTrimTrailingSpaces(profile->property(Profile::TrimTrailingSpacesInSelectedText)); view->setOpenLinksByDirectClick(profile->property(Profile::OpenLinksByDirectClickEnabled)); view->setUrlHintsModifiers(profile->property(Profile::UrlHintsModifiers)); int middleClickPasteMode = profile->property(Profile::MiddleClickPasteMode); if (middleClickPasteMode == Enum::PasteFromX11Selection) view->setMiddleClickPasteMode(Enum::PasteFromX11Selection); else if (middleClickPasteMode == Enum::PasteFromClipboard) view->setMiddleClickPasteMode(Enum::PasteFromClipboard); // margin/center view->setMargin(profile->property(Profile::TerminalMargin)); view->setCenterContents(profile->property(Profile::TerminalCenter)); // cursor shape int cursorShape = profile->property(Profile::CursorShape); if (cursorShape == Enum::BlockCursor) view->setKeyboardCursorShape(Enum::BlockCursor); else if (cursorShape == Enum::IBeamCursor) view->setKeyboardCursorShape(Enum::IBeamCursor); else if (cursorShape == Enum::UnderlineCursor) view->setKeyboardCursorShape(Enum::UnderlineCursor); // cursor color if (profile->useCustomCursorColor()) { const QColor& cursorColor = profile->customCursorColor(); view->setKeyboardCursorColor(cursorColor); } else { // an invalid QColor is used to inform the view widget to // draw the cursor using the default color( matching the text) view->setKeyboardCursorColor(QColor()); } // word characters view->setWordCharacters(profile->wordCharacters()); // bell mode view->setBellMode(profile->property(Profile::BellMode)); // mouse wheel zoom view->setMouseWheelZoom(profile->mouseWheelZoomEnabled()); } void ViewManager::updateViewsForSession(Session* session) { const Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); foreach(TerminalDisplay* view, _sessionMap.keys(session)) { applyProfileToView(view, profile); } } void ViewManager::profileChanged(Profile::Ptr profile) { // update all views associated with this profile QHashIterator iter(_sessionMap); while (iter.hasNext()) { iter.next(); // if session uses this profile, update the display if (iter.key() != 0 && iter.value() != 0 && SessionManager::instance()->sessionProfile(iter.value()) == profile) { applyProfileToView(iter.key(), profile); } } } QList ViewManager::viewProperties() const { QList list; ViewContainer* container = _viewSplitter->activeContainer(); Q_ASSERT(container); foreach(QWidget* view, container->views()) { ViewProperties* properties = container->viewProperties(view); Q_ASSERT(properties); list << properties; } return list; } void ViewManager::saveSessions(KConfigGroup& group) { // find all unique session restore IDs QList ids; QSet unique; // first: sessions in the active container, preserving the order ViewContainer* container = _viewSplitter->activeContainer(); Q_ASSERT(container); TerminalDisplay* activeview = qobject_cast(container->activeView()); QListIterator viewIter(container->views()); int tab = 1; while (viewIter.hasNext()) { TerminalDisplay* view = qobject_cast(viewIter.next()); Q_ASSERT(view); Session* session = _sessionMap[view]; ids << SessionManager::instance()->getRestoreId(session); unique.insert(session); if (view == activeview) { group.writeEntry("Active", tab); } tab++; } // second: all other sessions, in random order // we don't want to have sessions restored that are not connected foreach(Session * session, _sessionMap) { if (!unique.contains(session)) { ids << SessionManager::instance()->getRestoreId(session); unique.insert(session); } } group.writeEntry("Sessions", ids); } void ViewManager::restoreSessions(const KConfigGroup& group) { QList ids = group.readEntry("Sessions", QList()); int activeTab = group.readEntry("Active", 0); TerminalDisplay* display = 0; int tab = 1; foreach(int id, ids) { Session* session = SessionManager::instance()->idToSession(id); if (!session) { qWarning() << "Unable to load session with id" << id; // Force a creation of a default session below ids.clear(); break; } createView(session); if (!session->isRunning()) session->run(); if (tab++ == activeTab) display = qobject_cast(activeView()); } if (display) { _viewSplitter->activeContainer()->setActiveView(display); display->setFocus(Qt::OtherFocusReason); } if (ids.isEmpty()) { // Session file is unusable, start default Profile Profile::Ptr profile = ProfileManager::instance()->defaultProfile(); Session* session = SessionManager::instance()->createSession(profile); createView(session); if (!session->isRunning()) session->run(); } } int ViewManager::sessionCount() { return this->_sessionMap.size(); } int ViewManager::currentSession() { QHash::iterator i; for (i = this->_sessionMap.begin(); i != this->_sessionMap.end(); ++i) if (i.key()->isVisible()) return i.value()->sessionId(); return -1; } int ViewManager::newSession() { Profile::Ptr profile = ProfileManager::instance()->defaultProfile(); Session* session = SessionManager::instance()->createSession(profile); this->createView(session); session->run(); return session->sessionId(); } int ViewManager::newSession(const QString& profile, const QString& directory) { const QList profilelist = ProfileManager::instance()->allProfiles(); Profile::Ptr profileptr = ProfileManager::instance()->defaultProfile(); for (int i = 0; i < profilelist.size(); ++i) { if (profilelist.at(i)->name() == profile) { profileptr = profilelist.at(i); break; } } Session* session = SessionManager::instance()->createSession(profileptr); session->setInitialWorkingDirectory(directory); this->createView(session); session->run(); return session->sessionId(); } QString ViewManager::defaultProfile() { return ProfileManager::instance()->defaultProfile()->name(); } QStringList ViewManager::profileList() { return ProfileManager::instance()->availableProfileNames(); } void ViewManager::nextSession() { this->nextView(); } void ViewManager::prevSession() { this->previousView(); } void ViewManager::moveSessionLeft() { this->moveActiveViewLeft(); } void ViewManager::moveSessionRight() { this->moveActiveViewRight(); } void ViewManager::setTabWidthToText(bool useTextWidth) { ViewContainer* container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->setNavigationTextMode(useTextWidth); } void ViewManager::closeTabFromContainer(ViewContainer* container, QWidget* tab) { SessionController* controller = qobject_cast(container->viewProperties(tab)); Q_ASSERT(controller); if (controller) controller->closeSession(); } void ViewManager::setNavigationVisibility(int visibility) { _navigationVisibility = static_cast(visibility); foreach(ViewContainer* container, _viewSplitter->containers()) { container->setNavigationVisibility(_navigationVisibility); } } void ViewManager::setNavigationPosition(int position) { _navigationPosition = static_cast(position); foreach(ViewContainer* container, _viewSplitter->containers()) { Q_ASSERT(container->supportedNavigationPositions().contains(_navigationPosition)); container->setNavigationPosition(_navigationPosition); } } void ViewManager::setNavigationStyleSheet(const QString& styleSheet) { _navigationStyleSheet = styleSheet; foreach(ViewContainer* container, _viewSplitter->containers()) { container->setStyleSheet(_navigationStyleSheet); } } void ViewManager::setShowQuickButtons(bool show) { _showQuickButtons = show; foreach(ViewContainer* container, _viewSplitter->containers()) { if (_showQuickButtons) { container->setFeatures(container->features() | ViewContainer::QuickNewView | ViewContainer::QuickCloseView); } else { container->setFeatures(container->features() & ~ViewContainer::QuickNewView & ~ViewContainer::QuickCloseView); } } } void ViewManager::setNavigationBehavior(int behavior) { _newTabBehavior = static_cast(behavior); }