diff --git a/src/SessionController.cpp b/src/SessionController.cpp index bd905d9e..b9db38ac 100644 --- a/src/SessionController.cpp +++ b/src/SessionController.cpp @@ -1,2074 +1,2080 @@ /* 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" #include "konsoledebug.h" // Qt #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(QLatin1String("dialog-information")))) Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _silenceIcon, (QIcon::fromTheme(QLatin1String("dialog-information")))) Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _broadcastIcon, (QIcon::fromTheme(QLatin1String("emblem-important")))) QSet SessionController::_allControllers; int SessionController::_lastControllerId; SessionController::SessionController(Session* session , TerminalDisplay* view, QObject* parent) : ViewProperties(parent) , KXMLGUIClient() , _session(session) , _view(view) , _copyToGroup(nullptr) , _profileList(nullptr) , _previousState(-1) , _searchFilter(nullptr) , _urlFilter(nullptr) , _fileFilter(nullptr) , _copyInputToAllTabsAction(nullptr) , _findAction(nullptr) , _findNextAction(nullptr) , _findPreviousAction(nullptr) , _searchStartLine(0) , _prevSearchResultLine(0) , _searchBar(nullptr) , _codecAction(nullptr) , _switchProfileMenu(nullptr) , _webSearchMenu(nullptr) , _listenForScreenWindowUpdates(false) , _preventClose(false) , _keepIconUntilInteraction(false) , _showMenuAction(nullptr) , _isSearchBarEnabled(false) { Q_ASSERT(session); Q_ASSERT(view); // handle user interface related to session (menus etc.) if (isKonsolePart()) { setComponentName(QStringLiteral("konsole"), i18n("Konsole")); setXMLFile(QStringLiteral("partui.rc")); setupCommonActions(); } else { setXMLFile(QStringLiteral("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 and files updateFilterList(SessionManager::instance()->sessionProfile(_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::zmodemDownloadDetected, this, &Konsole::SessionController::zmodemDownload); connect(_session.data(), &Konsole::Session::zmodemUploadDetected, this, &Konsole::SessionController::zmodemUpload); // 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 auto 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 << QStringLiteral("bash") << QStringLiteral("fish") << QStringLiteral("sh"); _bookmarkValidProgramsToClear << QStringLiteral("tcsh") << QStringLiteral("zsh"); } SessionController::~SessionController() { if (_view != nullptr) { _view->setScreenWindow(nullptr); } _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() != 0u)) { 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 != nullptr); QString title = _session->getDynamicTitle(); title = title.simplified(); // Visualize that the session is broadcasting to others if ((_copyToGroup != nullptr) && _copyToGroup->sessions().count() > 1) { title.append(QLatin1Char('*')); } // 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), QLatin1Char('\n')); // Ctrl+C } // handle local paths if (url.isLocalFile()) { QString path = url.toLocalFile(); _session->sendTextToTerminal(QStringLiteral("cd ") + KShell::quoteArg(path), QLatin1Char('\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, QLatin1Char('\r')); } } else if (url.scheme() == QLatin1String("ssh")) { QString sshCommand = QStringLiteral("ssh "); if (url.port() > -1) { sshCommand += QStringLiteral("-p %1 ").arg(url.port()); } if (!url.userName().isEmpty()) { sshCommand += (url.userName() + QLatin1Char('@')); } if (!url.host().isEmpty()) { sshCommand += url.host(); } _session->sendTextToTerminal(sshCommand, QLatin1Char('\r')); } else if (url.scheme() == QLatin1String("telnet")) { QString telnetCommand = QStringLiteral("telnet "); if (!url.userName().isEmpty()) { telnetCommand += QStringLiteral("-l %1 ").arg(url.userName()); } if (!url.host().isEmpty()) { telnetCommand += (url.host() + QLatin1Char(' ')); } if (url.port() > -1) { telnetCommand += QString::number(url.port()); } _session->sendTextToTerminal(telnetCommand, QLatin1Char('\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()); qCDebug(KonsoleDebug) << "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(QStringLiteral("clear-history")); QAction* resetAction = collection->action(QStringLiteral("clear-history-and-reset")); QAction* selectAllAction = collection->action(QStringLiteral("select-all")); QAction* selectLineAction = collection->action(QStringLiteral("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(QStringLiteral("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(QLatin1Char('\n'), QLatin1Char(' ')).replace(QLatin1Char('\r'), QLatin1Char(' ')).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 = nullptr; 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 == nullptr) { return; } KUriFilterData filterData(action->data().toString()); if (KUriFilter::self()->filterUri(filterData, QStringList() << QStringLiteral("kurisearchfilter"))) { const QUrl& url = filterData.uri(); new KRun(url, QApplication::activeWindow()); } } void SessionController::configureWebShortcuts() { KToolInvocation::kdeinitExec(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("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); } void SessionController::toggleReadOnly() { QAction *action = qobject_cast(sender()); if (action != nullptr) { bool readonly = !isReadOnly(); updateReadOnlyActionState(action, readonly); _session->setReadOnly(readonly); } } bool SessionController::eventFilter(QObject* watched , QEvent* event) { if (event->type() == QEvent::FocusIn && watched == _view) { // 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, nullptr, nullptr); // 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 != nullptr) && _copyInputToAllTabsAction->isChecked()) { // A session with "Copy To All Tabs" has come into focus: // Ensure that newly created sessions are included in _copyToGroup! copyInputToAllTabs(); } } return Konsole::ViewProperties::eventFilter(watched, event); } void SessionController::removeSearchFilter() { if (_searchFilter == nullptr) { return; } _view->filterChain()->removeFilter(_searchFilter); delete _searchFilter; _searchFilter = nullptr; } void SessionController::setSearchBar(IncrementalSearchBar* searchBar) { // disconnect the existing search bar if (_searchBar != nullptr) { disconnect(this, nullptr, _searchBar, nullptr); disconnect(_searchBar, nullptr, this, nullptr); } // connect new search bar _searchBar = searchBar; if (_searchBar != nullptr) { 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() { KActionCollection* collection = actionCollection(); // Close Session QAction* action = collection->addAction(QStringLiteral("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, Konsole::ACCEL + Qt::SHIFT + Qt::Key_W); // Open Browser action = collection->addAction(QStringLiteral("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); #ifdef Q_OS_MACOS // Don't use the Konsole::ACCEL const here, we really want the Command key (Qt::META) // TODO: check what happens if we leave it to Qt to assign the default? collection->setDefaultShortcut(action, Qt::META + Qt::Key_C); #else collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_C); #endif // disabled at first, since nothing has been selected now action->setEnabled(false); action = KStandardAction::paste(this, SLOT(paste()), collection); QList pasteShortcut; #ifdef Q_OS_MACOS pasteShortcut.append(QKeySequence(Qt::META + Qt::Key_V)); // No Insert key on Mac keyboards #else pasteShortcut.append(QKeySequence(Konsole::ACCEL + Qt::SHIFT + Qt::Key_V)); pasteShortcut.append(QKeySequence(Qt::SHIFT + Qt::Key_Insert)); #endif collection->setDefaultShortcuts(action, pasteShortcut); action = collection->addAction(QStringLiteral("paste-selection"), this, SLOT(pasteFromX11Selection())); action->setText(i18n("Paste Selection")); #ifdef Q_OS_MACOS collection->setDefaultShortcut(action, Qt::META + Qt::SHIFT + Qt::Key_V); #else collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Insert); #endif _webSearchMenu = new KActionMenu(i18n("Web Search"), this); _webSearchMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts"))); _webSearchMenu->setVisible(false); collection->addAction(QStringLiteral("web-search"), _webSearchMenu); action = collection->addAction(QStringLiteral("select-all"), this, SLOT(selectAll())); action->setText(i18n("&Select All")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all"))); action = collection->addAction(QStringLiteral("select-line"), this, SLOT(selectLine())); action->setText(i18n("Select &Line")); action = KStandardAction::saveAs(this, SLOT(saveHistory()), collection); action->setText(i18n("Save Output &As...")); #ifdef Q_OS_MACOS action->setShortcut(QKeySequence(Qt::META + Qt::Key_S)); #endif action = KStandardAction::print(this, SLOT(print_screen()), collection); action->setText(i18n("&Print Screen...")); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_P); action = collection->addAction(QStringLiteral("adjust-history"), this, SLOT(showHistoryOptions())); action->setText(i18n("Adjust Scrollback...")); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); action = collection->addAction(QStringLiteral("clear-history"), this, SLOT(clearHistory())); action->setText(i18n("Clear Scrollback")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); action = collection->addAction(QStringLiteral("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, Konsole::ACCEL + Qt::SHIFT + Qt::Key_K); // Profile Options action = collection->addAction(QStringLiteral("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(QStringLiteral("switch-profile"), _switchProfileMenu); connect(_switchProfileMenu->menu(), &QMenu::aboutToShow, this, &Konsole::SessionController::prepareSwitchProfileMenu); // Read-only action = collection->addAction(QStringLiteral("view-readonly"), this, SLOT(toggleReadOnly())); action->setText(i18n("Read-only")); action->setCheckable(true); updateReadOnlyActionState(action, isReadOnly()); // 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(QStringLiteral("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() { KActionCollection* collection = actionCollection(); // Rename Session QAction* action = collection->addAction(QStringLiteral("rename-session"), this, SLOT(renameSession())); action->setText(i18n("&Rename Tab...")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::ALT + Qt::Key_S); // Copy input to ==> all tabs KToggleAction* copyInputToAllTabsAction = collection->add(QStringLiteral("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(QStringLiteral("copy-input-to-selected-tabs")); copyInputToSelectedTabsAction->setText(i18n("&Select Tabs...")); collection->setDefaultShortcut(copyInputToSelectedTabsAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Period); copyInputToSelectedTabsAction->setData(CopyInputToSelectedTabsMode); // Copy input to ==> none KToggleAction* copyInputToNoneAction = collection->add(QStringLiteral("copy-input-to-none")); copyInputToNoneAction->setText(i18nc("@action:inmenu Do not select any tabs", "&None")); collection->setDefaultShortcut(copyInputToNoneAction, Konsole::ACCEL + 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(QStringLiteral("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(QStringLiteral("zmodem-upload"), this, SLOT(zmodemUpload())); action->setText(i18n("&ZModem Upload...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::ALT + Qt::Key_U); // Monitor KToggleAction* toggleAction = new KToggleAction(i18n("Monitor for &Activity"), this); collection->setDefaultShortcut(toggleAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_A); action = collection->addAction(QStringLiteral("monitor-activity"), toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorActivity); toggleAction = new KToggleAction(i18n("Monitor for &Silence"), this); collection->setDefaultShortcut(toggleAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_I); action = collection->addAction(QStringLiteral("monitor-silence"), toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorSilence); // Text Size action = collection->addAction(QStringLiteral("enlarge-font"), this, SLOT(increaseFontSize())); action->setText(i18n("Enlarge Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-more"))); QList enlargeFontShortcut; enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL + Qt::Key_Plus)); enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL + Qt::Key_Equal)); collection->setDefaultShortcuts(action, enlargeFontShortcut); action = collection->addAction(QStringLiteral("shrink-font"), this, SLOT(decreaseFontSize())); action->setText(i18n("Shrink Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-less"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::Key_Minus); // Send signal KSelectAction* sendSignalActions = collection->add(QStringLiteral("send-signal")); sendSignalActions->setText(i18n("Send Signal")); connect(sendSignalActions, static_cast(&KSelectAction::triggered), this, &Konsole::SessionController::sendSignal); action = collection->addAction(QStringLiteral("sigstop-signal")); action->setText(i18n("&Suspend Task") + QStringLiteral(" (STOP)")); action->setData(SIGSTOP); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigcont-signal")); action->setText(i18n("&Continue Task") + QStringLiteral(" (CONT)")); action->setData(SIGCONT); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sighup-signal")); action->setText(i18n("&Hangup") + QStringLiteral(" (HUP)")); action->setData(SIGHUP); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigint-signal")); action->setText(i18n("&Interrupt Task") + QStringLiteral(" (INT)")); action->setData(SIGINT); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigterm-signal")); action->setText(i18n("&Terminate Task") + QStringLiteral(" (TERM)")); action->setData(SIGTERM); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigkill-signal")); action->setText(i18n("&Kill Task") + QStringLiteral(" (KILL)")); action->setData(SIGKILL); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigusr1-signal")); action->setText(i18n("User Signal &1") + QStringLiteral(" (USR1)")); action->setData(SIGUSR1); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigusr2-signal")); action->setText(i18n("User Signal &2") + QStringLiteral(" (USR2)")); action->setData(SIGUSR2); sendSignalActions->addAction(action); #ifdef Q_OS_MACOS collection->setDefaultShortcut(_findAction, Qt::META + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::META + Qt::Key_G); collection->setDefaultShortcut(_findPreviousAction, Qt::META + Qt::SHIFT + Qt::Key_G); #else collection->setDefaultShortcut(_findAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::Key_F3); collection->setDefaultShortcut(_findPreviousAction, Qt::SHIFT + Qt::Key_F3); #endif } 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::fromUtf8(_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 const QList allSessionsControllers = _allControllers.values(); foreach (SessionController* session, allSessionsControllers) { if ((session->profileDialogPointer() != nullptr) && 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 == nullptr) { return; } if (result != 0) { 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::fromUtf8(qgetenv("SHELL")).section(QLatin1Char('/'), -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; } 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::fromUtf8(qgetenv("SHELL")).section(QLatin1Char('/'), -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; } return true; } void SessionController::closeSession() { if (_preventClose) { return; } if (confirmClose()) { if (_session->closeInNormalWay()) { return; } else if (confirmForceClose()) { if (_session->closeInForceWay()) { return; } else { qCDebug(KonsoleDebug) << "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 != nullptr) { const KXmlGuiWindow* window = qobject_cast(object); if (window != nullptr) { return(window); } object = object->parent(); } return(nullptr); } 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 == nullptr) { _copyToGroup = new SessionGroup(this); } // Find our window ... const KXmlGuiWindow* myWindow = findWindow(_view); QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (auto session : group) { // 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 == nullptr) { _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 == nullptr) { 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 == nullptr) { // No 'Copy To' is active return; } QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (auto iterator : group) { Session* session = iterator; if (session != _session) { _copyToGroup->removeSession(iterator); } } delete _copyToGroup; _copyToGroup = nullptr; 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 != nullptr)) { _view->filterChain()->removeFilter(_fileFilter); delete _fileFilter; _fileFilter = nullptr; } else if (underlineFiles && (_fileFilter == nullptr)) { _fileFilter = new FileFilter(_session); _view->filterChain()->addFilter(_fileFilter); } bool underlineLinks = profile->underlineLinksEnabled(); if (!underlineLinks && (_urlFilter != nullptr)) { _view->filterChain()->removeFilter(_urlFilter); delete _urlFilter; _urlFilter = nullptr; } else if (underlineLinks && (_urlFilter == nullptr)) { _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 != nullptr) && (_searchBar != nullptr)) { _view->processFilters(); } } void SessionController::searchBarEvent() { QString selectedText = _view->screenWindow()->selectedText(Screen::PreserveLineBreaks | Screen::TrimLeadingWhitespace | Screen::TrimTrailingWhitespace); if (!selectedText.isEmpty()) { _searchBar->setSearchText(selectedText); } if (_searchBar->isVisible()) { _searchBar->focusLineEdit(); } else { searchHistory(true); _isSearchBarEnabled = true; } } void SessionController::enableSearchBar(bool showSearchBar) { if (_searchBar == nullptr) { 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 != nullptr) && (_view->screenWindow() != nullptr)) { _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 != nullptr) { 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() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::searchCompleted(bool success) { _prevSearchResultLine = _view->screenWindow()->currentResultLine(); if (_searchBar != nullptr) { _searchBar->setFoundMatch(success); } } void SessionController::beginSearch(const QString& text, Enum::SearchDirection direction) { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); QRegularExpression regExp = regexpFromSearchBarOptions(); _searchFilter->setRegExp(regExp); if (_searchStartLine == -1) { if (direction == Enum::ForwardsSearch) { setSearchStartTo(_view->screenWindow()->currentLine()); } else { setSearchStartTo(_view->screenWindow()->currentLine() + _view->screenWindow()->windowLines()); } } if (!regExp.pattern().isEmpty()) { _view->screenWindow()->setCurrentResultLine(-1); auto task = new SearchHistoryTask(this); connect(task, &Konsole::SearchHistoryTask::completed, this, &Konsole::SessionController::searchCompleted); task->setRegExp(regExp); task->setSearchDirection(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() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::findNextInHistory() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); setSearchStartTo(_prevSearchResultLine); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::ForwardsSearch); } void SessionController::findPreviousInHistory() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); setSearchStartTo(_prevSearchResultLine); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::ForwardsSearch : Enum::BackwardsSearch); } void SessionController::changeSearchMatch() { Q_ASSERT(_searchBar); Q_ASSERT(_searchFilter); // reset Selection for new case match _view->screenWindow()->clearSelection(); beginSearch(_searchBar->searchText(), reverseSearchChecked() ? Enum::BackwardsSearch : Enum::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 == nullptr) { return; } if (result != 0) { 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); auto 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 != nullptr) && _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::updateReadOnlyActionState(QAction *action, bool readonly) { action->setIcon(QIcon::fromTheme(readonly ? QStringLiteral("object-locked") : QStringLiteral("object-unlocked"))); action->setChecked(readonly); QAction *editAction = actionCollection()->action(QStringLiteral("edit_paste")); if (editAction != nullptr) { editAction->setEnabled(!readonly); } + + // Without the timer, when detaching a tab while the message widget is visible, + // the size of the terminal becomes really small... + QTimer::singleShot(0, this, [this, readonly]() { + _view->updateReadOnlyState(readonly); + }); } bool SessionController::isReadOnly() const { return _session->isReadOnly(); } 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(QLatin1String("%w"), _session->userTitle()); // special handling for the "%#" marker which is replaced with the // number of the shell title.replace(QLatin1String("%#"), 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() == nullptr) { if (clientBuilder() == nullptr) { setClientBuilder(new KXMLGUIBuilder(_view)); } auto factory = new KXMLGUIFactory(clientBuilder(), this); factory->addClient(this); ////qDebug() << "Created xmlgui factory" << factory; } QPointer popup = qobject_cast(factory()->container(QStringLiteral("session-popup-menu"), this)); if (popup != nullptr) { // prepend content-specific actions such as "Open Link", "Copy Email Address" etc. QList contentActions = _view->filterActions(position); auto contentSeparator = new QAction(popup); contentSeparator->setSeparator(true); contentActions << contentSeparator; popup->insertActions(popup->actions().value(0, nullptr), 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 != nullptr) { 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 != nullptr) { // 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 != nullptr) && chosen->objectName() == QLatin1String("close-session")) { chosen->trigger(); } } else { qCDebug(KonsoleDebug) << "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(QStringLiteral("rz")); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable(QStringLiteral("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; } _session->setZModemBusy(true); QString zmodem = QStandardPaths::findExecutable(QStringLiteral("sz")); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable(QStringLiteral("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 return !(qApp->applicationName() == QLatin1String("konsole")); } 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() = default; 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 << QStringLiteral("text/plain"); mimeTypes << QStringLiteral("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(nullptr , 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(QLatin1String("html"), Qt::CaseInsensitive)) || ((dialog->selectedFiles()).at(0).endsWith(QLatin1String("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 != nullptr) { // 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() != 0) { KMessageBox::sorry(nullptr , 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 == Enum::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(QLatin1Char('\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(Enum::BackwardsSearch) , _startLine(0) { } void SearchHistoryTask::setSearchDirection(Enum::SearchDirection direction) { _direction = direction; } void SearchHistoryTask::setStartLine(int line) { _startLine = line; } Enum::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 != nullptr) { return _session->userTitle(); } else { return QString(); } } diff --git a/src/TerminalDisplay.cpp b/src/TerminalDisplay.cpp index 3d3dce99..3fcd3124 100644 --- a/src/TerminalDisplay.cpp +++ b/src/TerminalDisplay.cpp @@ -1,3764 +1,3813 @@ /* This file is part of Konsole, a terminal emulator for KDE. Copyright 2006-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "TerminalDisplay.h" // Config #include // Qt #include #include #include #include #include -#include +#include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include +#include #include // Konsole #include "Filter.h" #include "konsoledebug.h" #include "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 != nullptr) { disconnect(_screenWindow , nullptr , this , nullptr); } _screenWindow = window; if (_screenWindow != nullptr) { connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateLineProperties); connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateImage); connect(_screenWindow.data() , &Konsole::ScreenWindow::currentResultLineChanged , this , &Konsole::TerminalDisplay::updateImage); connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, [this]() { _filterUpdateRequired = true; }); connect(_screenWindow.data(), &Konsole::ScreenWindow::scrolled, this, [this]() { _filterUpdateRequired = true; }); _screenWindow->setWindowLines(_lines); } } const ColorEntry* TerminalDisplay::colorTable() const { return _colorTable; } void TerminalDisplay::setBackgroundColor(const QColor& color) { _colorTable[DEFAULT_BACK_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; update(); } void TerminalDisplay::setColorTable(const ColorEntry table[]) { for (int i = 0; i < TABLE_COLORS; i++) { _colorTable[i] = table[i]; } setBackgroundColor(_colorTable[DEFAULT_BACK_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(QStringLiteral(REPCHAR))) / static_cast(qstrlen(REPCHAR)))); _fixedFont = true; const int fw = fm.width(QLatin1Char(REPCHAR[0])); for (unsigned int i = 1; i < qstrlen(REPCHAR); i++) { if (fw != fm.width(QLatin1Char(REPCHAR[i]))) { _fixedFont = false; break; } } if (_fontWidth < 1) { _fontWidth = 1; } _fontAscent = fm.ascent(); emit changedFontMetricSignal(_fontHeight, _fontWidth); propagateSize(); update(); } void TerminalDisplay::setVTFont(const QFont& f) { QFont newFont(f); 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(static_cast(fontInfo.styleHint())) % comma % QString::number(fontInfo.weight()) % comma % QString::number(static_cast(fontInfo.style())) % comma % QString::number(static_cast(fontInfo.underline())) % comma % QString::number(static_cast(fontInfo.strikeOut())) % comma % QString::number(static_cast(fontInfo.fixedPitch())) % comma % QString::number(static_cast(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 nullptr; } #endif } /* ------------------------------------------------------------------------- */ /* */ /* Constructor / Destructor */ /* */ /* ------------------------------------------------------------------------- */ TerminalDisplay::TerminalDisplay(QWidget* parent) : QWidget(parent) , _screenWindow(nullptr) , _bellMasked(false) - , _gridLayout(nullptr) + , _verticalLayout(new QVBoxLayout(this)) , _fontHeight(1) , _fontWidth(1) , _fontAscent(1) , _boldIntense(true) , _lines(1) , _columns(1) , _usedLines(1) , _usedColumns(1) , _image(nullptr) , _randomSeed(0) , _resizing(false) , _showTerminalSizeHint(true) , _bidiEnabled(false) , _actSel(0) , _wordSelectionMode(false) , _lineSelectionMode(false) , _preserveLineBreaks(false) , _columnSelectionMode(false) , _autoCopySelectedText(false) , _copyTextAsHTML(true) , _middleClickPasteMode(Enum::PasteFromX11Selection) , _scrollbarLocation(Enum::ScrollBarRight) , _scrollFullPage(false) , _wordCharacters(QStringLiteral(":@-./_~")) , _bellMode(Enum::NotifyBell) , _allowBlinkingText(true) , _allowBlinkingCursor(false) , _textBlinking(false) , _cursorBlinking(false) , _hasTextBlinker(false) , _urlHintsModifiers(Qt::NoModifier) , _showUrlHint(false) , _openLinksByDirectClick(false) , _ctrlRequiredForDrag(true) , _tripleClickMode(Enum::SelectWholeLine) , _possibleTripleClick(false) , _resizeWidget(nullptr) , _resizeTimer(nullptr) , _flowControlWarningEnabled(false) - , _outputSuspendedLabel(nullptr) + , _outputSuspendedMessageWidget(nullptr) , _lineSpacing(0) , _blendColor(qRgba(0, 0, 0, 0xff)) , _filterChain(new TerminalImageFilterChain()) , _filterUpdateRequired(true) , _cursorShape(Enum::BlockCursor) , _antialiasText(true) , _useFontLineCharacters(false) , _printerFriendly(false) , _sessionController(nullptr) , _trimLeadingSpaces(false) , _trimTrailingSpaces(false) , _margin(1) , _centerContents(false) + , _readOnlyMessageWidget(nullptr) , _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); + // Add the stretch item once, the KMessageWidgets are inserted at index 0. + _verticalLayout->addStretch(); + _verticalLayout->setSpacing(0); - setLayout(_gridLayout); + setLayout(_verticalLayout); + + // Take the scrollbar into account and add a margin to the layout. Without the timer the scrollbar width + // is garbage. + QTimer::singleShot(0, this, [this]() { + const int scrollBarWidth = _scrollBar->isVisible() ? geometry().intersected(_scrollBar->geometry()).width() : 0; + _verticalLayout->setContentsMargins(0, 0, scrollBarWidth, 0); + }); new AutoScrollHandler(this); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(Konsole::accessibleInterfaceFactory); #endif } TerminalDisplay::~TerminalDisplay() { disconnect(_blinkTextTimer); disconnect(_blinkCursorTimer); delete[] _image; delete _filterChain; + delete _readOnlyMessageWidget; + delete _outputSuspendedMessageWidget; + + _readOnlyMessageWidget = nullptr; + _outputSuspendedMessageWidget = nullptr; } /* ------------------------------------------------------------------------- */ /* */ /* 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) != 0u) { paint.drawLine(cx - 1, y, cx - 1, cy - 2); } if ((toDraw & TopC) != 0u) { paint.drawLine(cx, y, cx, cy - 2); } if ((toDraw & TopR) != 0u) { paint.drawLine(cx + 1, y, cx + 1, cy - 2); } //Bot _lines: if ((toDraw & BotL) != 0u) { paint.drawLine(cx - 1, cy + 2, cx - 1, ey); } if ((toDraw & BotC) != 0u) { paint.drawLine(cx, cy + 2, cx, ey); } if ((toDraw & BotR) != 0u) { paint.drawLine(cx + 1, cy + 2, cx + 1, ey); } //Left _lines: if ((toDraw & LeftT) != 0u) { paint.drawLine(x, cy - 1, cx - 2, cy - 1); } if ((toDraw & LeftC) != 0u) { paint.drawLine(x, cy, cx - 2, cy); } if ((toDraw & LeftB) != 0u) { paint.drawLine(x, cy + 1, cx - 2, cy + 1); } //Right _lines: if ((toDraw & RightT) != 0u) { paint.drawLine(cx + 2, cy - 1, ex, cy - 1); } if ((toDraw & RightC) != 0u) { paint.drawLine(cx + 2, cy, ex, cy); } if ((toDraw & RightB) != 0u) { paint.drawLine(cx + 2, cy + 1, ex, cy + 1); } //Intersection points. if ((toDraw & Int11) != 0u) { paint.drawPoint(cx - 1, cy - 1); } if ((toDraw & Int12) != 0u) { paint.drawPoint(cx, cy - 1); } if ((toDraw & Int13) != 0u) { paint.drawPoint(cx + 1, cy - 1); } if ((toDraw & Int21) != 0u) { paint.drawPoint(cx - 1, cy); } if ((toDraw & Int22) != 0u) { paint.drawPoint(cx, cy); } if ((toDraw & Int23) != 0u) { paint.drawPoint(cx + 1, cy); } if ((toDraw & Int31) != 0u) { paint.drawPoint(cx - 1, cy + 1); } if ((toDraw & Int32) != 0u) { paint.drawPoint(cx, cy + 1); } if ((toDraw & Int33) != 0u) { 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! #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) Q_FALLTHROUGH(); #endif 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! #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) Q_FALLTHROUGH(); #endif 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) != 0) && _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] != 0u) { 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_MACOS) // 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) != 0)) { return; } // don't draw concealed characters if ((style->rendition & RE_CONCEAL) != 0) { return; } // setup bold and underline bool useBold = (((style->rendition & RE_BOLD) != 0) && _boldIntense) || font().bold(); const bool useUnderline = ((style->rendition & RE_UNDERLINE) != 0) || font().underline(); const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || font().italic(); const bool useStrikeOut = ((style->rendition & RE_STRIKEOUT) != 0) || font().strikeOut(); const bool useOverline = ((style->rendition & RE_OVERLINE) != 0) || font().overline(); QFont 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); if (_bidiEnabled) { painter.drawText(rect.x(), rect.y() + _fontAscent + _lineSpacing, text); } else { painter.drawText(rect.x(), rect.y() + _fontAscent + _lineSpacing, 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) != 0) { 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 != nullptr) && _outputSuspendedLabel->isVisible()) { + if ((_outputSuspendedMessageWidget != nullptr) && _outputSuspendedMessageWidget->isVisible()) { + return; + } + + if ((_readOnlyMessageWidget != nullptr) && _readOnlyMessageWidget->isVisible()) { return; } // constrain the region to the display // the bottom of the region is capped to the number of lines in the display's // internal image - 2, so that the height of 'region' is strictly less // than the height of the internal image. QRect region = screenWindowRegion; region.setBottom(qMin(region.bottom(), this->_lines - 2)); // return if there is nothing to do if (lines == 0 || _image == nullptr || !region.isValid() || (region.top() + abs(lines)) >= region.bottom() || this->_lines <= region.height()) { return; } // hide terminal size label to prevent it being scrolled if ((_resizeWidget != nullptr) && _resizeWidget->isVisible()) { _resizeWidget->hide(); } // Note: With Qt 4.4 the left edge of the scrolled area must be at 0 // to get the correct (newly exposed) part of the widget repainted. // // The right edge must be before the left edge of the scroll bar to // avoid triggering a repaint of the entire widget, the distance is // given by SCROLLBAR_CONTENT_GAP // // Set the QT_FLUSH_PAINT environment variable to '1' before starting the // application to monitor repainting. // const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->width(); const int SCROLLBAR_CONTENT_GAP = 1; QRect scrollRect; if (_scrollbarLocation == Enum::ScrollBarLeft) { scrollRect.setLeft(scrollBarWidth + SCROLLBAR_CONTENT_GAP); scrollRect.setRight(width()); } else { scrollRect.setLeft(0); scrollRect.setRight(width() - scrollBarWidth - SCROLLBAR_CONTENT_GAP); } void* firstCharPos = &_image[ region.top() * 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 == nullptr) { 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 == nullptr) { 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 == nullptr) { // Create _image. // The emitted changedContentSizeSignal also leads to getImage being recreated, so do this first. updateImageSize(); } Character* const newimg = _screenWindow->getImage(); const int lines = _screenWindow->windowLines(); const int columns = _screenWindow->windowColumns(); setScroll(_screenWindow->currentLine() , _screenWindow->lineCount()); Q_ASSERT(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)); auto dirtyMask = new char[columnsToUpdate + 2]; QRegion dirtyRegion; // debugging variable, this records the number of lines that are found to // be 'dirty' ( ie. have changed from the old _image to the new _image ) and // which therefore need to be repainted int dirtyLineCount = 0; for (y = 0; y < linesToUpdate; ++y) { const Character* currentLine = &_image[y * 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] = 1; } } if (!_resizing) { // not while _resizing, we're expecting a paintEvent for (x = 0; x < columnsToUpdate; ++x) { _hasTextBlinker |= (newLine[x].rendition & RE_BLINK); // Start drawing if this character or the next one differs. // We also take the next one into account to handle the situation // where characters exceed their cell width. if (dirtyMask[x] != 0) { if (newLine[x + 0].character == 0u) { continue; } const bool lineDraw = 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 == 0u) { continue; // Skip trailing part of multi-col chars. } const bool nextIsDoubleWidth = (x + len + 1 == columnsToUpdate) ? false : (newLine[x + len + 1].character == 0); if (ch.foregroundColor != cf || ch.backgroundColor != clipboard || (ch.rendition & ~RE_EXTENDED_CHAR) != (cr & ~RE_EXTENDED_CHAR) || (dirtyMask[x + len] == 0) || 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 QAccessibleEvent dataChangeEvent(this, QAccessible::VisibleDataChanged); QAccessible::updateAccessibility(&dataChangeEvent); QAccessibleTextCursorEvent cursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX()); QAccessible::updateAccessibility(&cursorEvent); #endif } void TerminalDisplay::showResizeNotification() { if (_showTerminalSizeHint && isVisible()) { if (_resizeWidget == nullptr) { _resizeWidget = new QLabel(i18n("Size: XXX x XXX"), this); _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().width(i18n("Size: XXX x XXX"))); _resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height()); _resizeWidget->setAlignment(Qt::AlignCenter); _resizeWidget->setStyleSheet(QStringLiteral("background-color:palette(window);border-style:solid;border-width:1px;border-color:palette(dark)")); _resizeTimer = new QTimer(this); _resizeTimer->setInterval(SIZE_HINT_DURATION); _resizeTimer->setSingleShot(true); connect(_resizeTimer, &QTimer::timeout, _resizeWidget, &QLabel::hide); } QString sizeStr = i18n("Size: %1 x %2", _columns, _lines); _resizeWidget->setText(sizeStr); _resizeWidget->move((width() - _resizeWidget->width()) / 2, (height() - _resizeWidget->height()) / 2 + 20); _resizeWidget->show(); _resizeTimer->start(); } } void TerminalDisplay::paintEvent(QPaintEvent* pe) { QPainter paint(this); 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 != nullptr) { 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 (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 const QVector regionRects = region.rects(); QRect hintRect(regionRects.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 (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 == 0u) && (x != 0)) { 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) != 0) { // sequence of characters ushort extendedCharLength = 0; const ushort* chars = ExtendedCharTable::instance.lookupExtendedChar(_image[loc(x, y)].character, extendedCharLength); if (chars != nullptr) { 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 != 0u) { 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) != 0) { // sequence of characters ushort extendedCharLength = 0; const ushort* chars = ExtendedCharTable::instance.lookupExtendedChar(c, extendedCharLength); if (chars != nullptr) { 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 != 0u) { 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 == 0u)) { 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) != 0) { textScale.scale(2, 1); } if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) { textScale.scale(1, 2); } } //Apply text scaling matrix. paint.setWorldMatrix(textScale, true); //calculate the area in which the text will be drawn QRect textArea = QRect(_contentRect.left() + 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) != 0) { 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); } } void TerminalDisplay::setBlinkingTextEnabled(bool blink) { _allowBlinkingText = blink; if (blink && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!blink && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } } void TerminalDisplay::focusOutEvent(QFocusEvent*) { // trigger a repaint of the cursor so that it is both: // // * visible (in case it was hidden during blinking) // * drawn in a focused out state _cursorBlinking = false; updateCursor(); // suppress further cursor blinking _blinkCursorTimer->stop(); Q_ASSERT(!_cursorBlinking); // if text is blinking (hidden), blink it again to make it shown if (_textBlinking) { blinkTextEvent(); } // suppress further text blinking _blinkTextTimer->stop(); Q_ASSERT(!_textBlinking); _showUrlHint = false; 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*) { if (contentsRect().isValid()) { updateImageSize(); } } void TerminalDisplay::propagateSize() { if (_image != nullptr) { updateImageSize(); } } void TerminalDisplay::updateImageSize() { Character* oldImage = _image; const int oldLines = _lines; const int oldColumns = _columns; makeImage(); if (oldImage != nullptr) { // copy the old image to reduce flicker int lines = qMin(oldLines, _lines); int columns = qMin(oldColumns, _columns); for (int line = 0; line < lines; line++) { memcpy((void*)&_image[_columns * line], (void*)&oldImage[oldColumns * line], columns * sizeof(Character)); } delete[] oldImage; } if (_screenWindow != nullptr) { _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 == nullptr) { 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 == nullptr) { return; } + // Ignore clicks on the message widget + if (_readOnlyMessageWidget != nullptr) { + if (_readOnlyMessageWidget->isVisible() && _readOnlyMessageWidget->frameGeometry().contains(ev->pos())) { + return; + } + } + + if (_outputSuspendedMessageWidget != nullptr) { + if (_outputSuspendedMessageWidget->isVisible() && _outputSuspendedMessageWidget->frameGeometry().contains(ev->pos())) { + return; + } + } + int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn); 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) != 0u)) && selected) { _dragInfo.state = diPending; _dragInfo.start = ev->pos(); } else { // No reason to ever start a drag event _dragInfo.state = diNone; _preserveLineBreaks = !(((ev->modifiers() & Qt::ControlModifier) != 0u) && !(ev->modifiers() & Qt::AltModifier)); _columnSelectionMode = ((ev->modifiers() & Qt::AltModifier) != 0u) && ((ev->modifiers() & Qt::ControlModifier) != 0u); 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 ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u))) { Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) { QObject action; action.setObjectName(QStringLiteral("open-action")); spot->activate(&action); } } } } else if (ev->button() == Qt::MidButton) { processMidButtonClick(ev); } else if (ev->button() == Qt::RightButton) { if (_mouseMarks || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) { 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 != nullptr ? 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 != nullptr) && spot->type() == Filter::HotSpot::Link) { QRegion previousHotspotArea = _mouseOverHotspotArea; _mouseOverHotspotArea = QRegion(); QRect r; if (spot->startLine() == spot->endLine()) { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } else { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) { r.setCoords(0 * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } r.setCoords(0 * _fontWidth + _contentRect.left(), spot->endLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && (cursor().shape() != Qt::PointingHandCursor)) { setCursor(Qt::PointingHandCursor); } update(_mouseOverHotspotArea | previousHotspotArea); } else if (!_mouseOverHotspotArea.isEmpty()) { if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) || (cursor().shape() == Qt::PointingHandCursor)) { setCursor(_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) != 0u) { button = 0; } if ((ev->buttons() & Qt::MidButton) != 0u) { button = 1; } if ((ev->buttons() & Qt::RightButton) != 0u) { button = 2; } emit mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 1); return; } if (_dragInfo.state == diPending) { // we had a mouse down, but haven't confirmed a drag yet // if the mouse has moved sufficiently, we will confirm const int distance = QApplication::startDragDistance(); if (ev->x() > _dragInfo.start.x() + distance || ev->x() < _dragInfo.start.x() - distance || ev->y() > _dragInfo.start.y() + distance || ev->y() < _dragInfo.start.y() - distance) { // we've left the drag square, we can start a real drag operation now _screenWindow->clearSelection(); doDrag(); } return; } else if (_dragInfo.state == diDragging) { // this isn't technically needed because mouseMoveEvent is suppressed during // Qt drag operations, replaced by dragMoveEvent return; } if (_actSel == 0) { return; } // don't extend selection while pasting if ((ev->buttons() & Qt::MidButton) != 0u) { return; } extendSelection(ev->pos()); } void TerminalDisplay::leaveEvent(QEvent *) { // remove underline from an active link when cursor leaves the widget area if(!_mouseOverHotspotArea.isEmpty()) { update(_mouseOverHotspotArea); _mouseOverHotspotArea = QRegion(); } } void TerminalDisplay::extendSelection(const QPoint& position) { if (_screenWindow == nullptr) { 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 int i; 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) QPoint left = left_not_right ? here : _iPntSelCorr; i = loc(left.x(), left.y()); if (i >= 0 && i <= _imageSize) { selClass = charClass(_image[i]); while (((left.x() > 0) || (left.y() > 0 && (_lineProperties[left.y() - 1] & LINE_WRAPPED))) && charClass(_image[i - 1]) == selClass) { i--; if (left.x() > 0) { left.rx()--; } else { left.rx() = _usedColumns - 1; left.ry()--; } } } // Find left (left_not_right ? from start : from here) QPoint right = left_not_right ? _iPntSelCorr : here; i = loc(right.x(), right.y()); if (i >= 0 && i <= _imageSize) { selClass = charClass(_image[i]); while (((right.x() < _usedColumns - 1) || (right.y() < _usedLines - 1 && (_lineProperties[right.y()] & LINE_WRAPPED))) && charClass(_image[i + 1]) == selClass) { i++; if (right.x() < _usedColumns - 1) { right.rx()++; } else { right.rx() = 0; right.ry()++; } } } // Pick which is start (ohere) and which is extension (here) if (left_not_right) { here = left; ohere = right; } else { here = right; ohere = left; } ohere.rx()++; } if (_lineSelectionMode) { // Extend to complete line const bool above_not_below = (here.y() < _iPntSelCorr.y()); if (above_not_below) { ohere = findLineEnd(_iPntSelCorr); here = findLineStart(here); } else { ohere = findLineStart(_iPntSelCorr); here = findLineEnd(here); } swapping = !(_tripleSelBegin == ohere); _tripleSelBegin = ohere; ohere.rx()++; } int offset = 0; if (!_wordSelectionMode && !_lineSelectionMode) { QChar selClass; const bool left_not_right = (here.y() < _iPntSelCorr.y() || (here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x())); const bool old_left_not_right = (_pntSelCorr.y() < _iPntSelCorr.y() || (_pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x())); swapping = left_not_right != old_left_not_right; // Find left (left_not_right ? from here : from start) const QPoint left = left_not_right ? here : _iPntSelCorr; // Find left (left_not_right ? from start : from here) QPoint right = left_not_right ? _iPntSelCorr : here; if (right.x() > 0 && !_columnSelectionMode) { 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 == nullptr) { 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 == nullptr) { return; } _lineProperties = _screenWindow->getLineProperties(); } void TerminalDisplay::processMidButtonClick(QMouseEvent* ev) { if (_mouseMarks || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) { const bool appendEnter = (ev->modifiers() & Qt::ControlModifier) != 0u; if (_middleClickPasteMode == Enum::PasteFromX11Selection) { pasteFromX11Selection(appendEnter); } else if (_middleClickPasteMode == Enum::PasteFromClipboard) { pasteFromClipboard(appendEnter); } else { Q_ASSERT(false); } } else { 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 == nullptr) { return; } int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn); QPoint pos(charColumn, charLine); // 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(); QPoint bgnSel = pos; QPoint endSel = pos; int i = loc(bgnSel.x(), bgnSel.y()); _iPntSel = bgnSel; _iPntSel.ry() += _scrollBar->value(); _wordSelectionMode = true; _actSel = 2; // within selection // find word boundaries... const QChar selClass = charClass(_image[i]); { // find the start of the word int x = bgnSel.x(); while (((x > 0) || (bgnSel.y() > 0 && (_lineProperties[bgnSel.y() - 1] & LINE_WRAPPED))) && charClass(_image[i - 1]) == selClass) { i--; if (x > 0) { x--; } else { x = _usedColumns - 1; bgnSel.ry()--; } } bgnSel.setX(x); _screenWindow->setSelectionStart(bgnSel.x() , bgnSel.y() , false); // find the end of the word i = loc(endSel.x(), endSel.y()); x = endSel.x(); while (((x < _usedColumns - 1) || (endSel.y() < _usedLines - 1 && (_lineProperties[endSel.y()] & LINE_WRAPPED))) && charClass(_image[i + 1]) == selClass) { i++; if (x < _usedColumns - 1) { x++; } else { x = 0; endSel.ry()++; } } endSel.setX(x); // In word selection mode don't select @ (64) if at end of word. if (((_image[i].rendition & RE_EXTENDED_CHAR) == 0) && (QChar(_image[i].character) == QLatin1Char('@')) && ((endSel.x() - bgnSel.x()) > 0)) { endSel.setX(x - 1); } _actSel = 2; // within selection _screenWindow->setSelectionEnd(endSel.x() , endSel.y()); copyToX11Selection(); } _possibleTripleClick = true; QTimer::singleShot(QApplication::doubleClickInterval(), [this]() { _possibleTripleClick = false; }); } 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) != 0u) && 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(static_cast(_fontHeight * qApp->devicePixelRatio()), ScrollState::degreesToAngle(5)); const int keyCode = lines > 0 ? Qt::Key_Up : Qt::Key_Down; QKeyEvent keyEvent(QEvent::KeyPress, keyCode, Qt::NoModifier); for (int i = 0; i < abs(lines); i++) { 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::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) == 0) { 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) == 0) { 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 = nullptr; 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) != 0) && 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 == nullptr) { tmp_image = new Character[imageSize]; image = tmp_image; } screen->getImage(tmp_image, imageSize, newRegStart, y - 1); j = loc(x, i); } out: if (tmp_image != nullptr) { 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 = nullptr; const QChar selClass = charClass(image[j]); const int imageSize = regSize * _columns; const int maxY = _screenWindow->lineCount() - 1; const int maxX = _columns - 1; while (true) { const int lineCount = lineProperties.count(); for (;;j++, x++) { if (x < maxX) { if (charClass(image[j + 1]) == selClass) { continue; } goto out; } else if (i < lineCount - 1) { if (((lineProperties[i] & LINE_WRAPPED) != 0) && charClass(image[j + 1]) == selClass) { x = -1; i++; y++; continue; } goto out; } else if (y < maxY) { if (i < lineCount && ((lineProperties[i] & LINE_WRAPPED) == 0)) { goto out; } break; } else { goto out; } } int newRegEnd = qMin(y + regSize - 1, maxY); lineProperties = screen->getLineProperties(y, newRegEnd); i = 0; if (tmp_image == nullptr) { tmp_image = new Character[imageSize]; image = tmp_image; } screen->getImage(tmp_image, imageSize, y, newRegEnd); x--; j = loc(x, i); } out: y -= curLine; // In word selection mode don't select @ (64) if at end of word. if (((image[j].rendition & RE_EXTENDED_CHAR) == 0) && (QChar(image[j].character) == QLatin1Char('@')) && (y > pnt.y() || x > pnt.x())) { if (x > 0) { x--; } else { y--; } } if (tmp_image != nullptr) { delete[] tmp_image; } return QPoint(x, y); } Screen::DecodingOptions TerminalDisplay::currentDecodingOptions() { Screen::DecodingOptions decodingOptions; if (_preserveLineBreaks) { decodingOptions |= Screen::PreserveLineBreaks; } if (_trimLeadingSpaces) { decodingOptions |= Screen::TrimLeadingWhitespace; } if (_trimTrailingSpaces) { decodingOptions |= Screen::TrimTrailingWhitespace; } return decodingOptions; } void TerminalDisplay::mouseTripleClickEvent(QMouseEvent* ev) { if (_screenWindow == nullptr) { 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 == nullptr) { 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) != 0) { ushort extendedCharLength = 0; const ushort* chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength); if ((chars != nullptr) && extendedCharLength > 0) { const QString s = QString::fromUtf16(chars, extendedCharLength); if (_wordCharacters.contains(s, Qt::CaseInsensitive)) { return QLatin1Char('a'); } bool letterOrNumber = false; for (int i = 0; !letterOrNumber && i < s.size(); ++i) { letterOrNumber = s.at(i).isLetterOrNumber(); } return letterOrNumber ? QLatin1Char('a') : s.at(0); } return 0; } else { const QChar qch(ch.character); if (qch.isSpace()) { return QLatin1Char(' '); } if (qch.isLetterOrNumber() || _wordCharacters.contains(qch, Qt::CaseInsensitive)) { return QLatin1Char('a'); } return qch; } } void TerminalDisplay::setWordCharacters(const QString& wc) { _wordCharacters = wc; } // 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 == nullptr) { return; } if (_sessionController->isReadOnly()) { return; } if (appendReturn) { text.append(QLatin1String("\r")); } if (text.length() > 8000) { if (KMessageBox::warningContinueCancel(window(), i18np("Are you sure you want to paste %1 character?", "Are you sure you want to paste %1 characters?", text.length()), i18n("Confirm Paste"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("ShowPasteHugeTextWarning")) == KMessageBox::Cancel) { return; } } if (!text.isEmpty()) { text.replace(QLatin1Char('\n'), QLatin1Char('\r')); if (bracketedPasteMode()) { text.prepend(QLatin1String("\033[200~")); text.append(QLatin1String("\033[201~")); } // perform paste by simulating keypress events QKeyEvent e(QEvent::KeyPress, 0, Qt::NoModifier, text); emit keyPressedSignal(&e); } } void TerminalDisplay::setAutoCopySelectedText(bool enabled) { _autoCopySelectedText = enabled; } void TerminalDisplay::setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode) { _middleClickPasteMode = mode; } void TerminalDisplay::setCopyTextAsHTML(bool enabled) { _copyTextAsHTML = enabled; } void TerminalDisplay::copyToX11Selection() { if (_screenWindow == nullptr) { return; } const QString &text = _screenWindow->selectedText(currentDecodingOptions()); if (text.isEmpty()) { return; } auto mimeData = new QMimeData; mimeData->setText(text); if (_copyTextAsHTML) { mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml)); } if (QApplication::clipboard()->supportsSelection()) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection); } if (_autoCopySelectedText) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } } void TerminalDisplay::copyToClipboard() { if (_screenWindow == nullptr) { return; } const QString &text = _screenWindow->selectedText(currentDecodingOptions()); if (text.isEmpty()) { return; } auto mimeData = new QMimeData; mimeData->setText(text); if (_copyTextAsHTML) { mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml)); } QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void TerminalDisplay::pasteFromClipboard(bool appendEnter) { QString text = QApplication::clipboard()->text(QClipboard::Clipboard); doPaste(text, appendEnter); } void TerminalDisplay::pasteFromX11Selection(bool appendEnter) { if (QApplication::clipboard()->supportsSelection()) { QString text = QApplication::clipboard()->text(QClipboard::Selection); doPaste(text, appendEnter); } } /* ------------------------------------------------------------------------- */ /* */ /* Input Method */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::inputMethodEvent(QInputMethodEvent* event) { if (!event->commitString().isEmpty()) { QKeyEvent keyEvent(QEvent::KeyPress, 0, Qt::NoModifier, event->commitString()); emit keyPressedSignal(&keyEvent); } if (!_sessionController->isReadOnly()) { _inputMethodData.preeditString = event->preeditString(); update(preeditRect() | _inputMethodData.previousPreeditRect); } event->accept(); } QVariant TerminalDisplay::inputMethodQuery(Qt::InputMethodQuery query) const { const QPoint cursorPos = cursorPosition(); switch (query) { case Qt::ImMicroFocus: return imageToWidget(QRect(cursorPos.x(), cursorPos.y(), 1, 1)); case Qt::ImFont: return font(); case Qt::ImCursorPosition: // return the cursor position within the current line return cursorPos.x(); case Qt::ImSurroundingText: { // return the text from the current line QString lineText; QTextStream stream(&lineText); PlainTextDecoder decoder; decoder.begin(&stream); decoder.decodeLine(&_image[loc(0, cursorPos.y())], _usedColumns, LINE_DEFAULT); decoder.end(); return lineText; } case Qt::ImCurrentSelection: return QString(); default: break; } return QVariant(); } QRect TerminalDisplay::preeditRect() const { const int preeditLength = 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]; const QColor foreground = _colorTable[DEFAULT_FORE_COLOR]; const Character* style = &_image[loc(cursorPos.x(), cursorPos.y())]; drawBackground(painter, rect, background, true); drawCursor(painter, rect, foreground, background, invertColors); drawCharacters(painter, rect, _inputMethodData.preeditString, style, invertColors); _inputMethodData.previousPreeditRect = rect; } /* ------------------------------------------------------------------------- */ /* */ /* Keyboard */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setFlowControlWarningEnabled(bool enable) { _flowControlWarningEnabled = enable; // if the dialog is currently visible and the flow control warning has // been disabled then hide the dialog if (!enable) { outputSuspended(false); } } void TerminalDisplay::outputSuspended(bool suspended) { //create the label when this function is first called - if (_outputSuspendedLabel == nullptr) { + if (_outputSuspendedMessageWidget == nullptr) { //This label includes a link to an English language website //describing the 'flow control' (Xon/Xoff) feature found in almost //all terminal emulators. //If there isn't a suitable article available in the target language the link //can simply be removed. - _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, [this](const QString &url) { + auto linkHandler = [this](const QString &url) { if (url == QLatin1String("#close")) { - _outputSuspendedLabel->setVisible(false); + _outputSuspendedMessageWidget->hide(); } 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); + _outputSuspendedMessageWidget = createMessageWidget(i18n("Output has been " + "suspended" + " by pressing Ctrl+S." + " Press Ctrl+Q to resume." + " Click here to dismiss this message."), linkHandler); - _gridLayout->addWidget(_outputSuspendedLabel); - _gridLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, - QSizePolicy::Expanding), - 1, 0); + _outputSuspendedMessageWidget->setMessageType(KMessageWidget::Warning); + _outputSuspendedMessageWidget->hide(); } - _outputSuspendedLabel->setVisible(suspended); + _outputSuspendedMessageWidget->setVisible(suspended); } void TerminalDisplay::dismissOutputSuspendedMessage() { outputSuspended(false); } +KMessageWidget* TerminalDisplay::createMessageWidget(const QString &text, std::function linkHandler) { + auto widget = new KMessageWidget(text); + widget->setWordWrap(true); + widget->setFocusProxy(this); + widget->setCloseButtonVisible(false); + widget->setCursor(Qt::ArrowCursor); + + connect(widget, &KMessageWidget::linkActivated, this, linkHandler); + + _verticalLayout->insertWidget(0, widget); + return widget; +} + +void TerminalDisplay::updateReadOnlyState(bool readonly) { + + if (readonly) { + // Lazy create the readonly messagewidget + if (_readOnlyMessageWidget == nullptr) { + + auto linkHandler = [this](const QString &url) { + if (url == QLatin1String("#close")) { + _readOnlyMessageWidget->hide(); + } + }; + + _readOnlyMessageWidget = createMessageWidget(i18n("This terminal is read-only. Dismiss"), linkHandler); + _readOnlyMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("object-locked"))); + } + } + + if (_readOnlyMessageWidget != nullptr) { + _readOnlyMessageWidget->setVisible(readonly); + } +} + void TerminalDisplay::scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode, int amount) { _screenWindow->scrollBy(mode, amount, _scrollFullPage); _screenWindow->setTrackOutput(_screenWindow->atEndOfOutput()); updateLineProperties(); updateImage(); viewScrolledByUser(); } void TerminalDisplay::keyPressEvent(QKeyEvent* event) { if (_sessionController->isReadOnly()) { event->accept(); return; } if ((_urlHintsModifiers != 0u) && 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); } emit keyPressedSignal(event); #ifndef QT_NO_ACCESSIBILITY QAccessibleTextCursorEvent textCursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX()); QAccessible::updateAccessibility(&textCursorEvent); #endif event->accept(); } void TerminalDisplay::keyReleaseEvent(QKeyEvent *event) { if (_showUrlHint) { _showUrlHint = false; update(); } if (_sessionController->isReadOnly()) { event->accept(); return; } QWidget::keyReleaseEvent(event); } bool TerminalDisplay::handleShortcutOverrideEvent(QKeyEvent* keyEvent) { const int modifiers = keyEvent->modifiers(); // When a possible shortcut combination is pressed, // emit the overrideShortcutCheck() signal to allow the host // to decide whether the terminal should override it or not. if (modifiers != Qt::NoModifier) { int modifierCount = 0; unsigned int currentModifier = Qt::ShiftModifier; while (currentModifier <= Qt::KeypadModifier) { if ((modifiers & currentModifier) != 0u) { modifierCount++; } currentModifier <<= 1; } if (modifierCount < 2) { bool override = false; emit overrideShortcutCheck(keyEvent, override); if (override) { keyEvent->accept(); return true; } } } // Override any of the following shortcuts because // they are needed by the terminal int keyCode = keyEvent->key() | modifiers; switch (keyCode) { // list is taken from the QLineEdit::event() code case Qt::Key_Tab: case Qt::Key_Delete: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Backspace: case Qt::Key_Left: case Qt::Key_Right: case Qt::Key_Slash: case Qt::Key_Period: case Qt::Key_Space: keyEvent->accept(); return true; } return false; } bool TerminalDisplay::event(QEvent* event) { bool eventHandled = false; switch (event->type()) { case QEvent::ShortcutOverride: eventHandled = handleShortcutOverrideEvent(static_cast(event)); break; case QEvent::PaletteChange: case QEvent::ApplicationPaletteChange: _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::bell(const QString& message) { if (_bellMasked) { return; } switch (_bellMode) { case Enum::SystemBeepBell: KNotification::beep(); break; case Enum::NotifyBell: // STABLE API: // Please note that these event names, "BellVisible" and "BellInvisible", // should not change and should be kept stable, because other applications // that use this code via KPart rely on these names for notifications. KNotification::event(hasFocus() ? QStringLiteral("BellVisible") : QStringLiteral("BellInvisible"), message, QPixmap(), this); break; case Enum::VisualBell: visualBell(); break; default: break; } // limit the rate at which bells can occur. // ...mainly for sound effects where rapid bells in sequence // produce a horrible noise. _bellMasked = true; QTimer::singleShot(500, [this]() { _bellMasked = false; }); } void TerminalDisplay::visualBell() { swapFGBGColors(); QTimer::singleShot(200, this, &Konsole::TerminalDisplay::swapFGBGColors); } void TerminalDisplay::swapFGBGColors() { // swap the default foreground & background color ColorEntry color = _colorTable[DEFAULT_BACK_COLOR]; _colorTable[DEFAULT_BACK_COLOR] = _colorTable[DEFAULT_FORE_COLOR]; _colorTable[DEFAULT_FORE_COLOR] = color; 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() const auto mimeData = event->mimeData(); if ((!_sessionController->isReadOnly()) && (mimeData != nullptr) && (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list")))) { event->acceptProposedAction(); } } void TerminalDisplay::dropEvent(QDropEvent* event) { if (_sessionController->isReadOnly()) { event->accept(); return; } const auto mimeData = event->mimeData(); if (mimeData == nullptr) { return; } auto urls = 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 += QLatin1Char(' '); } // If our target is local we will open a popup - otherwise the fallback kicks // in and the URLs will simply be pasted as text. if (!_dropUrlsAsText && (_sessionController != nullptr) && _sessionController->url().isLocalFile()) { // A standard popup with Copy, Move and Link as options - // plus an additional Paste option. QAction* pasteAction = new QAction(i18n("&Paste Location"), this); 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 + QLatin1Char('\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 = mimeData->text(); } if (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list"))) { emit sendStringToEmu(dropText.toLocal8Bit()); } } void TerminalDisplay::dropMenuPasteActionTriggered() { if (sender() != nullptr) { const QAction* action = qobject_cast(sender()); if (action != nullptr) { emit sendStringToEmu(action->data().toString().toLocal8Bit()); } } } void TerminalDisplay::dropMenuCdActionTriggered() { if (sender() != nullptr) { const QAction* action = qobject_cast(sender()); if (action != nullptr) { emit sendStringToEmu(action->data().toString().toLocal8Bit()); } } } void TerminalDisplay::doDrag() { _dragInfo.state = diDragging; _dragInfo.dragObject = new QDrag(this); auto 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); switch (event->type()) { case QEvent::MouseMove: { QMouseEvent* mouseEvent = static_cast(event); bool mouseInWidget = widget()->rect().contains(mouseEvent->pos()); if (mouseInWidget) { if (_timerId != 0) { killTimer(_timerId); } _timerId = 0; } else { if ((_timerId == 0) && ((mouseEvent->buttons() & Qt::LeftButton) != 0u)) { _timerId = startTimer(100); } } break; } case QEvent::MouseButtonRelease: { QMouseEvent* mouseEvent = static_cast(event); if ((_timerId != 0) && ((mouseEvent->buttons() & ~Qt::LeftButton) != 0u)) { killTimer(_timerId); _timerId = 0; } break; } default: break; }; return false; } diff --git a/src/TerminalDisplay.h b/src/TerminalDisplay.h index 9c652959..69a208e5 100644 --- a/src/TerminalDisplay.h +++ b/src/TerminalDisplay.h @@ -1,1029 +1,1041 @@ /* 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 +#include + // 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 QVBoxLayout; class QKeyEvent; class QScrollBar; class QShowEvent; class QHideEvent; class QTimerEvent; +class KMessageWidget; + 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 = nullptr); ~TerminalDisplay() Q_DECL_OVERRIDE; /** 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 randomSeed); /** * 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 slines The maximum value of the scroll bar. */ void setScroll(int cursor, int slines); 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 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 leading spaces should be trimmed in selected text. */ void setTrimLeadingSpaces(bool enabled) { _trimLeadingSpaces = enabled; } /** * Returns true if leading spaces should be trimmed in selected text. */ bool trimLeadingSpaces() const { return _trimLeadingSpaces; } /** * 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 Q_DECL_OVERRIDE; /** * 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 &f); /** 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); /** * Gets the background of the display * @see setBackgroundColor(), setColorTable(), setForegroundColor() */ QColor getBackgroundColor() const; bool bracketedPasteMode() const; /** * Returns true if the flow control warning box is enabled. * See outputSuspended() and setFlowControlWarningEnabled() */ bool flowControlWarningEnabled() const { return _flowControlWarningEnabled; } /** See setUsesMouse() */ bool usesMouse() const; public Q_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 setCopyTextAsHTML(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 enable); /** * 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 on Set to true if the program running in the terminal is interested in mouse events * or false otherwise. */ void setUsesMouse(bool on); void setBracketedPasteMode(bool on); /** * Shows a notification that a bell event has occurred in the terminal. * TODO: More documentation here */ void bell(const QString &message); /** * 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); + // Used to show/hide the message widget + void updateReadOnlyState(bool readonly); + Q_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: bool event(QEvent *event) Q_DECL_OVERRIDE; void paintEvent(QPaintEvent *pe) Q_DECL_OVERRIDE; void showEvent(QShowEvent *event) Q_DECL_OVERRIDE; void hideEvent(QHideEvent *event) Q_DECL_OVERRIDE; void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE; void contextMenuEvent(QContextMenuEvent *event) Q_DECL_OVERRIDE; virtual void fontChange(const QFont &); void focusInEvent(QFocusEvent *event) Q_DECL_OVERRIDE; void focusOutEvent(QFocusEvent *event) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; void keyReleaseEvent(QKeyEvent *event) Q_DECL_OVERRIDE; void leaveEvent(QEvent *event) Q_DECL_OVERRIDE; void mouseDoubleClickEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mousePressEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mouseReleaseEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mouseMoveEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; virtual void extendSelection(const QPoint &position); void wheelEvent(QWheelEvent *ev) Q_DECL_OVERRIDE; bool focusNextPrevChild(bool next) Q_DECL_OVERRIDE; // drag and drop void dragEnterEvent(QDragEnterEvent *event) Q_DECL_OVERRIDE; void dropEvent(QDropEvent *event) Q_DECL_OVERRIDE; 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 *ev); void selectLine(QPoint pos, bool entireLine); // reimplemented void inputMethodEvent(QInputMethodEvent *event) Q_DECL_OVERRIDE; QVariant inputMethodQuery(Qt::InputMethodQuery query) const Q_DECL_OVERRIDE; protected Q_SLOTS: void scrollBarPositionChanged(int value); void blinkTextEvent(); void blinkCursorEvent(); private Q_SLOTS: void swapFGBGColors(); 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: Q_DISABLE_COPY(TerminalDisplay) // -- 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 &backgroundColor, bool useOpacitySetting); // draws the cursor character void drawCursor(QPainter &painter, const QRect &rect, const QColor &foregroundColor, const QColor &backgroundColor, bool &invertCharacterColor); // 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 &screenWindowRegion); 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 *keyEvent); void doPaste(QString text, bool appendReturn); void processMidButtonClick(QMouseEvent *ev); QPoint findLineStart(const QPoint &pnt); QPoint findLineEnd(const QPoint &pnt); QPoint findWordStart(const QPoint &pnt); QPoint findWordEnd(const QPoint &pnt); // Uses the current settings for trimming whitespace and preserving linebreaks to create a proper flag value for Screen Screen::DecodingOptions currentDecodingOptions(); + // Boilerplate setup for MessageWidget + KMessageWidget* createMessageWidget(const QString &text, std::function linkHandler); + // the window onto the terminal screen which this display // is currently showing. QPointer _screenWindow; bool _bellMasked; - QGridLayout *_gridLayout; + QVBoxLayout *_verticalLayout; 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; bool _copyTextAsHTML; 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 _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; + KMessageWidget *_outputSuspendedMessageWidget; 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 _trimLeadingSpaces; // trim leading spaces in selected text 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 + KMessageWidget *_readOnlyMessageWidget; // Message shown at the top when read-only mode gets activated + qreal _opacity; ScrollState _scrollWheelState; friend class TerminalDisplayAccessible; }; class AutoScrollHandler : public QObject { Q_OBJECT public: explicit AutoScrollHandler(QWidget *parent); protected: void timerEvent(QTimerEvent *event) Q_DECL_OVERRIDE; bool eventFilter(QObject *watched, QEvent *event) Q_DECL_OVERRIDE; private: QWidget *widget() const { return static_cast(parent()); } int _timerId; }; } #endif // TERMINALDISPLAY_H