diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 461b9e54..b0a070cf 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,921 +1,928 @@ /* Copyright 2006-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "MainWindow.h" // Qt #include // KDE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Konsole #include "BookmarkHandler.h" #include "SessionController.h" #include "ProfileList.h" #include "Session.h" #include "ViewContainer.h" #include "ViewManager.h" #include "SessionManager.h" #include "ProfileManager.h" #include "KonsoleSettings.h" #include "WindowSystemInfo.h" #include "TerminalDisplay.h" #include "settings/ConfigurationDialog.h" #include "settings/TemporaryFilesSettings.h" #include "settings/GeneralSettings.h" #include "settings/ProfileSettings.h" #include "settings/TabBarSettings.h" using namespace Konsole; MainWindow::MainWindow() : KXmlGuiWindow(), _viewManager(nullptr), _bookmarkHandler(nullptr), _toggleMenuBarAction(nullptr), _newTabMenuAction(nullptr), _pluggedController(nullptr), _menuBarInitialVisibility(true), _menuBarInitialVisibilityApplied(false) { if (!KonsoleSettings::saveGeometryOnExit()) { // If we are not using the global Konsole save geometry on exit, // remove all Height and Width from [MainWindow] from konsolerc // Each screen resolution will have entries (Width 1280=619) KSharedConfigPtr konsoleConfig = KSharedConfig::openConfig(QStringLiteral("konsolerc")); KConfigGroup group = konsoleConfig->group("MainWindow"); QMap configEntries = group.entryMap(); QMapIterator i(configEntries); while (i.hasNext()) { i.next(); if (i.key().startsWith(QLatin1String("Width")) || i.key().startsWith(QLatin1String("Height"))) { group.deleteEntry(i.key()); } } } updateUseTransparency(); // create actions for menus setupActions(); // create view manager _viewManager = new ViewManager(this, actionCollection()); connect(_viewManager, &Konsole::ViewManager::empty, this, &Konsole::MainWindow::close); connect(_viewManager, &Konsole::ViewManager::activeViewChanged, this, &Konsole::MainWindow::activeViewChanged); connect(_viewManager, &Konsole::ViewManager::unplugController, this, &Konsole::MainWindow::disconnectController); connect(_viewManager, &Konsole::ViewManager::viewPropertiesChanged, bookmarkHandler(), &Konsole::BookmarkHandler::setViews); connect(_viewManager, &Konsole::ViewManager::blurSettingChanged, this, &Konsole::MainWindow::setBlur); connect(_viewManager, &Konsole::ViewManager::updateWindowIcon, this, &Konsole::MainWindow::updateWindowIcon); connect(_viewManager, &Konsole::ViewManager::newViewWithProfileRequest, this, &Konsole::MainWindow::newFromProfile); connect(_viewManager, &Konsole::ViewManager::newViewRequest, this, &Konsole::MainWindow::newTab); connect(_viewManager, &Konsole::ViewManager::terminalsDetached, this, &Konsole::MainWindow::terminalsDetached); setCentralWidget(_viewManager->widget()); // disable automatically generated accelerators in top-level // menu items - to avoid conflicting with Alt+[Letter] shortcuts // in terminal applications KAcceleratorManager::setNoAccel(menuBar()); // create menus createGUI(); // remember the original menu accelerators for later use rememberMenuAccelerators(); // replace standard shortcuts which cannot be used in a terminal // emulator (as they are reserved for use by terminal applications) correctStandardShortcuts(); setProfileList(new ProfileList(true, this)); // this must come at the end applyKonsoleSettings(); connect(KonsoleSettings::self(), &Konsole::KonsoleSettings::configChanged, this, &Konsole::MainWindow::applyKonsoleSettings); } void MainWindow::updateUseTransparency() { if (!WindowSystemInfo::HAVE_TRANSPARENCY) { return; } bool useTranslucency = KWindowSystem::compositingActive(); setAttribute(Qt::WA_TranslucentBackground, useTranslucency); setAttribute(Qt::WA_NoSystemBackground, false); WindowSystemInfo::HAVE_TRANSPARENCY = useTranslucency; } void MainWindow::rememberMenuAccelerators() { - foreach (QAction *menuItem, menuBar()->actions()) { + const QList actions = menuBar()->actions(); + for (QAction *menuItem : actions) { QString itemText = menuItem->text(); menuItem->setData(itemText); } } // remove accelerators for standard menu items (eg. &File, &View, &Edit) // etc. which are defined in kdelibs/kdeui/xmlgui/ui_standards.rc, again, // to avoid conflicting with Alt+[Letter] terminal shortcuts // // TODO - Modify XMLGUI so that it allows the text for standard actions // defined in ui_standards.rc to be re-defined in the local application // XMLGUI file (konsoleui.rc in this case) - the text for standard items // can then be redefined there to exclude the standard accelerators void MainWindow::removeMenuAccelerators() { - foreach (QAction *menuItem, menuBar()->actions()) { + const QList actions = menuBar()->actions(); + for (QAction *menuItem : actions) { menuItem->setText(menuItem->text().replace(QLatin1Char('&'), QString())); } } void MainWindow::restoreMenuAccelerators() { - foreach (QAction *menuItem, menuBar()->actions()) { + const QList actions = menuBar()->actions(); + for (QAction *menuItem : actions) { QString itemText = menuItem->data().toString(); menuItem->setText(itemText); } } void MainWindow::correctStandardShortcuts() { // replace F1 shortcut for help contents QAction *helpAction = actionCollection()->action(QStringLiteral("help_contents")); if (helpAction != nullptr) { actionCollection()->setDefaultShortcut(helpAction, QKeySequence()); } // replace Ctrl+B shortcut for bookmarks only if user hasn't already // changed the shortcut; however, if the user changed it to Ctrl+B // this will still get changed to Ctrl+Shift+B QAction *bookmarkAction = actionCollection()->action(QStringLiteral("add_bookmark")); if ((bookmarkAction != nullptr) && bookmarkAction->shortcut() == QKeySequence(Konsole::ACCEL + Qt::Key_B)) { actionCollection()->setDefaultShortcut(bookmarkAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_B); } } ViewManager *MainWindow::viewManager() const { return _viewManager; } void MainWindow::disconnectController(SessionController *controller) { disconnect(controller, &Konsole::SessionController::titleChanged, this, &Konsole::MainWindow::activeViewTitleChanged); disconnect(controller, &Konsole::SessionController::rawTitleChanged, this, &Konsole::MainWindow::updateWindowCaption); disconnect(controller, &Konsole::SessionController::iconChanged, this, &Konsole::MainWindow::updateWindowIcon); if (auto view = controller->view()) { view->removeEventFilter(this); } // KXmlGuiFactory::removeClient() will try to access actions associated // with the controller internally, which may not be valid after the controller // itself is no longer valid (after the associated session and or view have // been destroyed) if (controller->isValid()) { guiFactory()->removeClient(controller); } if (_pluggedController == controller) { _pluggedController = nullptr; } } void MainWindow::activeViewChanged(SessionController *controller) { if (!SessionManager::instance()->sessionProfile(controller->session())) { return; } // associate bookmark menu with current session bookmarkHandler()->setActiveView(controller); disconnect(bookmarkHandler(), &Konsole::BookmarkHandler::openUrl, nullptr, nullptr); connect(bookmarkHandler(), &Konsole::BookmarkHandler::openUrl, controller, &Konsole::SessionController::openUrl); if (!_pluggedController.isNull()) { disconnectController(_pluggedController); } Q_ASSERT(controller); _pluggedController = controller; _pluggedController->view()->installEventFilter(this); setBlur(ViewManager::profileHasBlurEnabled(SessionManager::instance()->sessionProfile(_pluggedController->session()))); // listen for title changes from the current session connect(controller, &Konsole::SessionController::titleChanged, this, &Konsole::MainWindow::activeViewTitleChanged); connect(controller, &Konsole::SessionController::rawTitleChanged, this, &Konsole::MainWindow::updateWindowCaption); connect(controller, &Konsole::SessionController::iconChanged, this, &Konsole::MainWindow::updateWindowIcon); controller->setShowMenuAction(_toggleMenuBarAction); guiFactory()->addClient(controller); // update session title to match newly activated session activeViewTitleChanged(controller); // Update window icon to newly activated session's icon updateWindowIcon(); } void MainWindow::activeViewTitleChanged(ViewProperties *properties) { Q_UNUSED(properties); updateWindowCaption(); } void MainWindow::updateWindowCaption() { if (_pluggedController.isNull()) { return; } const QString &title = _pluggedController->title(); const QString &userTitle = _pluggedController->userTitle(); // use tab title as caption by default QString caption = title; // use window title as caption when this setting is enabled // if the userTitle is empty, use a blank space (using an empty string // removes the dash — before the application name; leaving the dash // looks better) if (KonsoleSettings::showWindowTitleOnTitleBar()) { !userTitle.isEmpty() ? caption = userTitle : caption = QStringLiteral(" "); } setCaption(caption); } void MainWindow::updateWindowIcon() { if ((!_pluggedController.isNull()) && !_pluggedController->icon().isNull()) { setWindowIcon(_pluggedController->icon()); } } void MainWindow::setupActions() { KActionCollection *collection = actionCollection(); // File Menu _newTabMenuAction = new KActionMenu(QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@action:inmenu", "&New Tab"), collection); collection->setDefaultShortcut(_newTabMenuAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_T); collection->setShortcutsConfigurable(_newTabMenuAction, true); _newTabMenuAction->setAutoRepeat(false); connect(_newTabMenuAction, &KActionMenu::triggered, this, &MainWindow::newTab); collection->addAction(QStringLiteral("new-tab"), _newTabMenuAction); collection->setShortcutsConfigurable(_newTabMenuAction, true); QAction* menuAction = collection->addAction(QStringLiteral("clone-tab")); menuAction->setIcon(QIcon::fromTheme(QStringLiteral("tab-duplicate"))); menuAction->setText(i18nc("@action:inmenu", "&Clone Tab")); collection->setDefaultShortcut(menuAction, QKeySequence()); menuAction->setAutoRepeat(false); connect(menuAction, &QAction::triggered, this, &Konsole::MainWindow::cloneTab); menuAction = collection->addAction(QStringLiteral("new-window")); menuAction->setIcon(QIcon::fromTheme(QStringLiteral("window-new"))); menuAction->setText(i18nc("@action:inmenu", "New &Window")); collection->setDefaultShortcut(menuAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_N); menuAction->setAutoRepeat(false); connect(menuAction, &QAction::triggered, this, &Konsole::MainWindow::newWindow); menuAction = collection->addAction(QStringLiteral("close-window")); menuAction->setIcon(QIcon::fromTheme(QStringLiteral("window-close"))); menuAction->setText(i18nc("@action:inmenu", "Close Window")); collection->setDefaultShortcut(menuAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Q); connect(menuAction, &QAction::triggered, this, &Konsole::MainWindow::close); // Bookmark Menu KActionMenu *bookmarkMenu = new KActionMenu(i18nc("@title:menu", "&Bookmarks"), collection); _bookmarkHandler = new BookmarkHandler(collection, bookmarkMenu->menu(), true, this); collection->addAction(QStringLiteral("bookmark"), bookmarkMenu); connect(_bookmarkHandler, &Konsole::BookmarkHandler::openUrls, this, &Konsole::MainWindow::openUrls); // Settings Menu _toggleMenuBarAction = KStandardAction::showMenubar(menuBar(), SLOT(setVisible(bool)), collection); collection->setDefaultShortcut(_toggleMenuBarAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_M); // Full Screen menuAction = KStandardAction::fullScreen(this, SLOT(viewFullScreen(bool)), this, collection); collection->setDefaultShortcut(menuAction, Qt::Key_F11); KStandardAction::configureNotifications(this, SLOT(configureNotifications()), collection); KStandardAction::keyBindings(this, SLOT(showShortcutsDialog()), collection); KStandardAction::preferences(this, SLOT(showSettingsDialog()), collection); menuAction = collection->addAction(QStringLiteral("manage-profiles")); menuAction->setText(i18nc("@action:inmenu", "Manage Profiles...")); menuAction->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); connect(menuAction, &QAction::triggered, this, &Konsole::MainWindow::showManageProfilesDialog); // Set up an shortcut-only action for activating menu bar. menuAction = collection->addAction(QStringLiteral("activate-menu")); menuAction->setText(i18nc("@item", "Activate Menu")); collection->setDefaultShortcut(menuAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_F10); connect(menuAction, &QAction::triggered, this, &Konsole::MainWindow::activateMenuBar); } void MainWindow::viewFullScreen(bool fullScreen) { if (fullScreen) { setWindowState(windowState() | Qt::WindowFullScreen); } else { setWindowState(windowState() & ~Qt::WindowFullScreen); } } BookmarkHandler *MainWindow::bookmarkHandler() const { return _bookmarkHandler; } void MainWindow::setProfileList(ProfileList *list) { profileListChanged(list->actions()); connect(list, &Konsole::ProfileList::profileSelected, this, &MainWindow::newFromProfile); connect(list, &Konsole::ProfileList::actionsChanged, this, &Konsole::MainWindow::profileListChanged); } void MainWindow::profileListChanged(const QList &sessionActions) { // If only 1 profile is to be shown in the menu, only display // it if it is the non-default profile. if (sessionActions.size() > 2) { // Update the 'New Tab' KActionMenu if (_newTabMenuAction->menu() != nullptr) { _newTabMenuAction->menu()->clear(); } else { _newTabMenuAction->setMenu(new QMenu()); } - foreach (QAction *sessionAction, sessionActions) { + for (QAction *sessionAction : sessionActions) { _newTabMenuAction->menu()->addAction(sessionAction); // NOTE: defaultProfile seems to not work here, sigh. Profile::Ptr profile = ProfileManager::instance()->defaultProfile(); if (profile && profile->name() == sessionAction->text().remove(QLatin1Char('&'))) { QIcon icon(KIconLoader::global()->loadIcon(profile->icon(), KIconLoader::Small, 0, KIconLoader::DefaultState, QStringList(QStringLiteral("emblem-favorite")))); sessionAction->setIcon(icon); _newTabMenuAction->menu()->setDefaultAction(sessionAction); QFont actionFont = sessionAction->font(); actionFont.setBold(true); sessionAction->setFont(actionFont); } } } else { if (_newTabMenuAction->menu() != nullptr) { _newTabMenuAction->menu()->clear(); } else { _newTabMenuAction->setMenu(new QMenu()); } Profile::Ptr profile = ProfileManager::instance()->defaultProfile(); // NOTE: Compare names w/o any '&' if (sessionActions.size() == 2 && sessionActions[1]->text().remove(QLatin1Char('&')) != profile->name()) { _newTabMenuAction->menu()->addAction(sessionActions[1]); } else { _newTabMenuAction->menu()->deleteLater(); } } } QString MainWindow::activeSessionDir() const { if (!_pluggedController.isNull()) { if (Session *session = _pluggedController->session()) { // For new tabs to get the correct working directory, // force the updating of the currentWorkingDirectory. session->getDynamicTitle(); } return _pluggedController->currentDir(); } else { return QString(); } } void MainWindow::openUrls(const QList &urls) { Profile::Ptr defaultProfile = ProfileManager::instance()->defaultProfile(); for (const auto &url : urls) { if (url.isLocalFile()) { createSession(defaultProfile, url.path()); } else if (url.scheme() == QLatin1String("ssh")) { createSSHSession(defaultProfile, url); } } } void MainWindow::newTab() { Profile::Ptr defaultProfile = ProfileManager::instance()->defaultProfile(); createSession(defaultProfile, activeSessionDir()); } void MainWindow::cloneTab() { Q_ASSERT(_pluggedController); Session *session = _pluggedController->session(); Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); if (profile) { createSession(profile, activeSessionDir()); } else { // something must be wrong: every session should be associated with profile Q_ASSERT(false); newTab(); } } Session *MainWindow::createSession(Profile::Ptr profile, const QString &directory) { if (!profile) { profile = ProfileManager::instance()->defaultProfile(); } const QString newSessionDirectory = profile->startInCurrentSessionDir() ? directory : QString(); Session *session = _viewManager->createSession(profile, newSessionDirectory); // create view before starting the session process so that the session // doesn't suffer a change in terminal size right after the session // starts. Some applications such as GNU Screen and Midnight Commander // don't like this happening auto newView = _viewManager->createView(session); _viewManager->activeContainer()->addView(newView); return session; } Session *MainWindow::createSSHSession(Profile::Ptr profile, const QUrl &url) { if (!profile) { profile = ProfileManager::instance()->defaultProfile(); } Session *session = SessionManager::instance()->createSession(profile); 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')); // create view before starting the session process so that the session // doesn't suffer a change in terminal size right after the session // starts. some applications such as GNU Screen and Midnight Commander // don't like this happening auto newView = _viewManager->createView(session); _viewManager->activeContainer()->addView(newView); return session; } void MainWindow::setFocus() { _viewManager->activeView()->setFocus(); } void MainWindow::newWindow() { Profile::Ptr defaultProfile = ProfileManager::instance()->defaultProfile(); emit newWindowRequest(defaultProfile, activeSessionDir()); } bool MainWindow::queryClose() { // Do not ask for confirmation during log out and power off // TODO: rework the dealing of this case to make it has its own confirmation // dialog. if (qApp->isSavingSession()) { return true; } // Check what processes are running, excluding the shell QStringList processesRunning; const auto uniqueSessions = QSet::fromList(_viewManager->sessions()); - foreach (Session *session, uniqueSessions) { + for (Session *session : uniqueSessions) { if ((session == nullptr) || !session->isForegroundProcessActive()) { continue; } const QString defaultProc = session->program().split(QLatin1Char('/')).last(); const QString currentProc = session->foregroundProcessName().split(QLatin1Char('/')).last(); if (currentProc.isEmpty()) { continue; } if (defaultProc != currentProc) { processesRunning.append(currentProc); } } // Get number of open tabs const int openTabs = _viewManager->viewProperties().count(); // If no processes running (except the shell) and no extra tabs, just close if (processesRunning.count() == 0 && openTabs < 2) { return true; } // NOTE: Some, if not all, of the below KWindowSystem calls are only // implemented under x11 (KDE4.8 kdelibs/kdeui/windowmanagement). // make sure the window is shown on current desktop and is not minimized KWindowSystem::setOnDesktop(winId(), KWindowSystem::currentDesktop()); if (isMinimized()) { KWindowSystem::unminimizeWindow(winId()); } int result; if (!processesRunning.isEmpty()) { if (openTabs == 1) { result = KMessageBox::warningYesNoList(this, i18ncp("@info", "There is a process running in this window. " "Do you still want to quit?", "There are %1 processes running in this window. " "Do you still want to quit?", processesRunning.count()), processesRunning, i18nc("@title", "Confirm Close"), KGuiItem(i18nc("@action:button", "Close &Window"), QStringLiteral("window-close")), KStandardGuiItem::cancel(), // don't ask again name is wrong but I can't update. // this is not about tabs anymore. it's about empty tabs *or* splits. QStringLiteral("CloseAllTabs")); if (result == KMessageBox::No) // No is equal to cancel closing result = KMessageBox::Cancel; } else { result = KMessageBox::warningYesNoCancelList(this, i18ncp("@info", "There is a process running in this window. " "Do you still want to quit?", "There are %1 processes running in this window. " "Do you still want to quit?", processesRunning.count()), processesRunning, i18nc("@title", "Confirm Close"), KGuiItem(i18nc("@action:button", "Close &Window"), QStringLiteral("window-close")), KGuiItem(i18nc("@action:button", "Close Current &Tab"), QStringLiteral("tab-close")), KStandardGuiItem::cancel(), // don't ask again name is wrong but I can't update. // this is not about tabs anymore. it's about empty tabs *or* splits. QStringLiteral("CloseAllTabs")); } } else { result = KMessageBox::warningYesNoCancel(this, i18nc("@info", "There are %1 open terminals in this window. " "Do you still want to quit?", openTabs), i18nc("@title", "Confirm Close"), KGuiItem(i18nc("@action:button", "Close &Window"), QStringLiteral("window-close")), KGuiItem(i18nc("@action:button", "Close Current &Tab"), QStringLiteral("tab-close")), KStandardGuiItem::cancel(), // don't ask again name is wrong but I can't update. // this is not about tabs anymore. it's about empty tabs *or* splits. QStringLiteral("CloseAllEmptyTabs")); } switch (result) { case KMessageBox::Yes: return true; case KMessageBox::No: if ((!_pluggedController.isNull()) && (!_pluggedController->session().isNull())) { if (!(_pluggedController->session()->closeInNormalWay())) { if (_pluggedController->confirmForceClose()) { _pluggedController->session()->closeInForceWay(); } } } return false; case KMessageBox::Cancel: return false; } return true; } void MainWindow::saveProperties(KConfigGroup &group) { _viewManager->saveSessions(group); } void MainWindow::readProperties(const KConfigGroup &group) { _viewManager->restoreSessions(group); } void MainWindow::saveGlobalProperties(KConfig *config) { SessionManager::instance()->saveSessions(config); } void MainWindow::readGlobalProperties(KConfig *config) { SessionManager::instance()->restoreSessions(config); } void MainWindow::syncActiveShortcuts(KActionCollection *dest, const KActionCollection *source) { - foreach (QAction *qAction, source->actions()) { + const QList actionsList = source->actions(); + for (QAction *qAction : actionsList) { if (QAction *destQAction = dest->action(qAction->objectName())) { destQAction->setShortcut(qAction->shortcut()); } } } void MainWindow::showShortcutsDialog() { KShortcutsDialog dialog(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsDisallowed, this); // add actions from this window and the current session controller - foreach (KXMLGUIClient *client, guiFactory()->clients()) { + const QList clientsList = guiFactory()->clients(); + for (KXMLGUIClient *client : clientsList) { dialog.addCollection(client->actionCollection()); } if (dialog.configure()) { // sync shortcuts for non-session actions (defined in "konsoleui.rc") in other main windows - foreach (QWidget *mainWindowWidget, QApplication::topLevelWidgets()) { + const QList widgets = QApplication::topLevelWidgets(); + for (QWidget *mainWindowWidget : widgets) { auto *mainWindow = qobject_cast(mainWindowWidget); if ((mainWindow != nullptr) && mainWindow != this) { syncActiveShortcuts(mainWindow->actionCollection(), actionCollection()); } } // sync shortcuts for session actions (defined in "sessionui.rc") in other session controllers. // Controllers which are currently plugged in (ie. their actions are part of the current menu) // must be updated immediately via syncActiveShortcuts(). Other controllers will be updated // when they are plugged into a main window. - foreach (SessionController *controller, SessionController::allControllers()) { + const QSet allControllers = SessionController::allControllers(); + for (SessionController *controller : allControllers) { controller->reloadXML(); if ((controller->factory() != nullptr) && controller != _pluggedController) { syncActiveShortcuts(controller->actionCollection(), _pluggedController->actionCollection()); } } } } void MainWindow::newFromProfile(const Profile::Ptr &profile) { createSession(profile, activeSessionDir()); } void MainWindow::showManageProfilesDialog() { showSettingsDialog(true); } void MainWindow::showSettingsDialog(const bool showProfilePage) { static ConfigurationDialog *confDialog = nullptr; if (confDialog != nullptr) { confDialog->show(); return; } confDialog = new ConfigurationDialog(this, KonsoleSettings::self()); const QString generalPageName = i18nc("@title Preferences page name", "General"); auto *generalPage = new KPageWidgetItem(new GeneralSettings(confDialog), generalPageName); generalPage->setIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal"))); confDialog->addPage(generalPage, true); const QString profilePageName = i18nc("@title Preferences page name", "Profiles"); auto profilePage = new KPageWidgetItem(new ProfileSettings(confDialog), profilePageName); profilePage->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-profiles"))); confDialog->addPage(profilePage, true); const QString tabBarPageName = i18nc("@title Preferences page name", "Tab Bar"); auto tabBarPage = new KPageWidgetItem(new TabBarSettings(confDialog), tabBarPageName); tabBarPage->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); confDialog->addPage(tabBarPage, true); const QString temporaryFilesPageName = i18nc("@title Preferences page name", "Temporary Files"); auto temporaryFilesPage = new KPageWidgetItem(new TemporaryFilesSettings(confDialog), temporaryFilesPageName); temporaryFilesPage->setIcon(QIcon::fromTheme(QStringLiteral("folder-temp"))); confDialog->addPage(temporaryFilesPage, true); if (showProfilePage) { confDialog->setCurrentPage(profilePage); } confDialog->show(); } void MainWindow::applyKonsoleSettings() { setMenuBarInitialVisibility(KonsoleSettings::showMenuBarByDefault()); setRemoveWindowTitleBarAndFrame(KonsoleSettings::removeWindowTitleBarAndFrame()); if (KonsoleSettings::allowMenuAccelerators()) { restoreMenuAccelerators(); } else { removeMenuAccelerators(); } _viewManager->activeContainer()->setNavigationBehavior(KonsoleSettings::newTabBehavior()); setAutoSaveSettings(QStringLiteral("MainWindow"), KonsoleSettings::saveGeometryOnExit()); updateWindowCaption(); } void MainWindow::activateMenuBar() { const QList menuActions = menuBar()->actions(); if (menuActions.isEmpty()) { return; } // Show menubar if it is hidden at the moment if (menuBar()->isHidden()) { menuBar()->setVisible(true); _toggleMenuBarAction->setChecked(true); } // First menu action should be 'File' QAction *menuAction = menuActions.first(); // TODO: Handle when menubar is top level (MacOS) menuBar()->setActiveAction(menuAction); } void MainWindow::configureNotifications() { KNotifyConfigWidget::configure(this); } void MainWindow::setBlur(bool blur) { if (_pluggedController.isNull()) { return; } if (!_pluggedController->isKonsolePart()) { KWindowEffects::enableBlurBehind(winId(), blur); } } void MainWindow::setMenuBarInitialVisibility(bool visible) { _menuBarInitialVisibility = visible; } void MainWindow::setRemoveWindowTitleBarAndFrame(bool frameless) { Qt::WindowFlags newFlags = frameless ? Qt::FramelessWindowHint : Qt::Window; // The window is not yet visible if (!isVisible()) { setWindowFlags(newFlags); // The window is visible and the setting changed } else if (windowFlags().testFlag(Qt::FramelessWindowHint) != frameless) { const auto oldGeometry = saveGeometry(); // This happens for every Konsole window. It depends on // the fact that every window is processed in single thread const auto oldActiveWindow = KWindowSystem::activeWindow(); setWindowFlags(newFlags); // The setWindowFlags() has hidden the window. Show it again // with previous geometry restoreGeometry(oldGeometry); setVisible(true); KWindowSystem::activateWindow(oldActiveWindow); } } void MainWindow::showEvent(QShowEvent *event) { // Make sure the 'initial' visibility is applied only once. if (!_menuBarInitialVisibilityApplied) { // the initial visibility of menubar should be applied at this last // moment. Otherwise, the initial visibility will be determined by // what KMainWindow has automatically stored in konsolerc, but not by // what users has explicitly configured . menuBar()->setVisible(_menuBarInitialVisibility); _toggleMenuBarAction->setChecked(_menuBarInitialVisibility); _menuBarInitialVisibilityApplied = true; if (!KonsoleSettings::saveGeometryOnExit()) { resize(sizeHint()); } } // Call parent method KXmlGuiWindow::showEvent(event); } void MainWindow::triggerAction(const QString &name) const { if (auto action = actionCollection()->action(name)) { if (action->isEnabled()) { action->trigger(); } } } bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (!_pluggedController.isNull() && obj == _pluggedController->view()) { switch(event->type()) { case QEvent::MouseButtonPress: case QEvent::MouseButtonDblClick: switch(static_cast(event)->button()) { case Qt::ForwardButton: triggerAction(QStringLiteral("next-view")); break; case Qt::BackButton: triggerAction(QStringLiteral("previous-view")); break; default: ; } default: ; } } return KXmlGuiWindow::eventFilter(obj, event); } bool MainWindow::focusNextPrevChild(bool) { // In stand-alone konsole, always disable implicit focus switching // through 'Tab' and 'Shift+Tab' // // Kpart is another different story return false; } diff --git a/src/Part.cpp b/src/Part.cpp index 3f144fa6..97885b77 100644 --- a/src/Part.cpp +++ b/src/Part.cpp @@ -1,436 +1,437 @@ /* Copyright 2007-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "Part.h" // Qt #include #include #include #include #include // KDE #include #include #include #include #include // Konsole #include "EditProfileDialog.h" #include "Emulation.h" #include "Session.h" #include "SessionController.h" #include "SessionManager.h" #include "ProfileManager.h" #include "TerminalDisplay.h" #include "ViewManager.h" #include "ViewContainer.h" #include "KonsoleSettings.h" #include "settings/PartInfo.h" #include "settings/ProfileSettings.h" using namespace Konsole; K_PLUGIN_FACTORY_WITH_JSON(KonsolePartFactory, "konsolepart.json", registerPlugin();) Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList &) : KParts::ReadOnlyPart(parent), _viewManager(nullptr), _pluggedController(nullptr) { // create view widget _viewManager = new ViewManager(this, actionCollection()); _viewManager->setNavigationMethod(ViewManager::NoNavigation); connect(_viewManager, &Konsole::ViewManager::activeViewChanged, this, &Konsole::Part::activeViewChanged); connect(_viewManager, &Konsole::ViewManager::empty, this, &Konsole::Part::terminalExited); connect(_viewManager, &Konsole::ViewManager::newViewRequest, this, &Konsole::Part::newTab); _viewManager->widget()->setParent(parentWidget); setWidget(_viewManager->widget()); actionCollection()->addAssociatedWidget(_viewManager->widget()); - foreach (QAction *action, actionCollection()->actions()) { + const QList actionsList = actionCollection()->actions(); + for (QAction *action : actionsList) { action->setShortcutContext(Qt::WidgetWithChildrenShortcut); } // Enable translucency support. _viewManager->widget()->setAttribute(Qt::WA_TranslucentBackground, true); // create basic session createSession(); } Part::~Part() { ProfileManager::instance()->saveSettings(); delete _viewManager; } bool Part::openFile() { return false; } void Part::terminalExited() { deleteLater(); } void Part::newTab() { createSession(); } Session *Part::activeSession() const { if (_viewManager->activeViewController() != nullptr) { Q_ASSERT(_viewManager->activeViewController()->session()); return _viewManager->activeViewController()->session(); } else { return nullptr; } } void Part::startProgram(const QString &program, const QStringList &arguments) { Q_ASSERT(activeSession()); // do nothing if the session has already started running if (activeSession()->isRunning()) { return; } if (!program.isEmpty() && !arguments.isEmpty()) { activeSession()->setProgram(program); activeSession()->setArguments(arguments); } activeSession()->run(); } void Part::openTeletype(int ptyMasterFd) { Q_ASSERT(activeSession()); activeSession()->openTeletype(ptyMasterFd); } void Part::showShellInDir(const QString &dir) { Q_ASSERT(activeSession()); // do nothing if the session has already started running if (activeSession()->isRunning()) { return; } // All other checking is done in setInitialWorkingDirectory() if (!dir.isEmpty()) { activeSession()->setInitialWorkingDirectory(dir); } activeSession()->run(); } void Part::sendInput(const QString &text) { Q_ASSERT(activeSession()); activeSession()->sendTextToTerminal(text); } int Part::terminalProcessId() { Q_ASSERT(activeSession()); return activeSession()->processId(); } int Part::foregroundProcessId() { Q_ASSERT(activeSession()); if (activeSession()->isForegroundProcessActive()) { return activeSession()->foregroundProcessId(); } else { return -1; } } QString Part::foregroundProcessName() { Q_ASSERT(activeSession()); if (activeSession()->isForegroundProcessActive()) { return activeSession()->foregroundProcessName(); } else { return QString(); } } QString Part::currentWorkingDirectory() const { Q_ASSERT(activeSession()); return activeSession()->currentWorkingDirectory(); } #ifdef USE_TERMINALINTERFACEV2 QVariant Part::profileProperty(const QString &profileProperty) const { const auto metaEnum = QMetaEnum::fromType(); const auto value = metaEnum.keyToValue(profileProperty.toStdString().c_str()); if (value == -1) { return QString(); } const auto p = static_cast(value); return SessionManager::instance()->sessionProfile(activeSession())->property(p); } QStringList Part::availableProfiles() const { return ProfileManager::instance()->availableProfileNames(); } QString Part::currentProfileName() const { return SessionManager::instance()->sessionProfile(activeSession())->name(); } bool Part::setCurrentProfile(const QString &profileName) { Profile::Ptr profile; for(auto p : ProfileManager::instance()->allProfiles()) { if (p->name() == profileName) { profile = p; break; } } if (!profile) { profile = ProfileManager::instance()->loadProfile(profileName); } SessionManager::instance()->setSessionProfile(activeSession(), profile); return currentProfileName() == profileName; } #endif void Part::createSession(const QString &profileName, const QString &directory) { Profile::Ptr profile = ProfileManager::instance()->defaultProfile(); if (!profileName.isEmpty()) { profile = ProfileManager::instance()->loadProfile(profileName); } Q_ASSERT(profile); Session *session = SessionManager::instance()->createSession(profile); // override the default directory specified in the profile if (!directory.isEmpty() && profile->startInCurrentSessionDir()) { session->setInitialWorkingDirectory(directory); } auto newView = _viewManager->createView(session); _viewManager->activeContainer()->addView(newView); } void Part::activeViewChanged(SessionController *controller) { Q_ASSERT(controller); Q_ASSERT(controller->view()); // remove existing controller if (_pluggedController != nullptr) { removeChildClient(_pluggedController); disconnect(_pluggedController, &Konsole::SessionController::titleChanged, this, &Konsole::Part::activeViewTitleChanged); disconnect(_pluggedController, &Konsole::SessionController::currentDirectoryChanged, this, &Konsole::Part::currentDirectoryChanged); } // insert new controller insertChildClient(controller); connect(controller, &Konsole::SessionController::titleChanged, this, &Konsole::Part::activeViewTitleChanged); activeViewTitleChanged(controller); connect(controller, &Konsole::SessionController::currentDirectoryChanged, this, &Konsole::Part::currentDirectoryChanged); const char *displaySignal = SIGNAL(overrideShortcutCheck(QKeyEvent*,bool&)); const char *partSlot = SLOT(overrideTerminalShortcut(QKeyEvent*,bool&)); disconnect(controller->view(), displaySignal, this, partSlot); connect(controller->view(), displaySignal, this, partSlot); _pluggedController = controller; } void Part::overrideTerminalShortcut(QKeyEvent *event, bool &override) { // Shift+Insert is commonly used as the alternate shortcut for // pasting in KDE apps(including konsole), so it deserves some // special treatment. if (((event->modifiers() & Qt::ShiftModifier) != 0u) && (event->key() == Qt::Key_Insert)) { override = false; return; } // override all shortcuts in the embedded terminal by default override = true; emit overrideShortcut(event, override); } void Part::activeViewTitleChanged(ViewProperties *properties) { emit setWindowCaption(properties->title()); } void Part::showManageProfilesDialog(QWidget *parent) { // Make sure this string is unique among all users of this part if (KConfigDialog::showDialog(QStringLiteral("konsolepartmanageprofiles"))) { return; } KConfigDialog *settingsDialog = new KConfigDialog(parent, QStringLiteral("konsolepartmanageprofiles"), KonsoleSettings::self()); settingsDialog->setFaceType(KPageDialog::Tabbed); auto profileSettings = new ProfileSettings(settingsDialog); settingsDialog->addPage(profileSettings, i18nc("@title Preferences page name", "Profiles"), QStringLiteral("configure")); auto partInfoSettings = new PartInfoSettings(settingsDialog); settingsDialog->addPage(partInfoSettings, i18nc("@title Preferences page name", "Part Info"), QStringLiteral("dialog-information")); settingsDialog->show(); } void Part::showEditCurrentProfileDialog(QWidget *parent) { Q_ASSERT(activeSession()); auto dialog = new EditProfileDialog(parent); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setProfile(SessionManager::instance()->sessionProfile(activeSession())); dialog->show(); } void Part::changeSessionSettings(const QString &text) { Q_ASSERT(activeSession()); // send a profile change command, the escape code format // is the same as the normal X-Term commands used to change the window title or icon, // but with a magic value of '50' for the parameter which specifies what to change QString command = QStringLiteral("\033]50;%1\a").arg(text); sendInput(command); } // Konqueror integration bool Part::openUrl(const QUrl &url) { if (KParts::ReadOnlyPart::url() == url) { emit completed(); return true; } setUrl(url); emit setWindowCaption(url.toDisplayString(QUrl::PreferLocalFile)); ////qDebug() << "Set Window Caption to " << url.pathOrUrl(); emit started(nullptr); if (url.isLocalFile()) { showShellInDir(url.path()); } else { showShellInDir(QDir::homePath()); } emit completed(); return true; } void Part::setMonitorSilenceEnabled(bool enabled) { Q_ASSERT(activeSession()); if (enabled) { activeSession()->setMonitorSilence(true); connect(activeSession(), &Konsole::Session::stateChanged, this, &Konsole::Part::sessionStateChanged, Qt::UniqueConnection); } else { activeSession()->setMonitorSilence(false); disconnect(activeSession(), &Konsole::Session::stateChanged, this, &Konsole::Part::sessionStateChanged); } } void Part::setMonitorActivityEnabled(bool enabled) { Q_ASSERT(activeSession()); if (enabled) { activeSession()->setMonitorActivity(true); connect(activeSession(), &Konsole::Session::stateChanged, this, &Konsole::Part::sessionStateChanged, Qt::UniqueConnection); } else { activeSession()->setMonitorActivity(false); disconnect(activeSession(), &Konsole::Session::stateChanged, this, &Konsole::Part::sessionStateChanged); } } bool Part::isBlurEnabled() { return ViewManager::profileHasBlurEnabled(SessionManager::instance()->sessionProfile(activeSession())); } void Part::sessionStateChanged(int state) { if (state == NOTIFYSILENCE) { emit silenceDetected(); } else if (state == NOTIFYACTIVITY) { emit activityDetected(); } } #include "Part.moc" diff --git a/src/ProcessInfo.cpp b/src/ProcessInfo.cpp index 2e9633a5..b3889914 100644 --- a/src/ProcessInfo.cpp +++ b/src/ProcessInfo.cpp @@ -1,1195 +1,1195 @@ /* Copyright 2007-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Config #include "config-konsole.h" // Own #include "ProcessInfo.h" // Unix #include #include #include #include #include #include #include // Qt #include #include #include #include #include // KDE #include #include #include #if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) || defined(Q_OS_MACOS) #include #endif #if defined(Q_OS_MACOS) #include #include #endif #if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #include #include #include # if defined(Q_OS_FREEBSD) # include # include # include # endif #endif using namespace Konsole; ProcessInfo::ProcessInfo(int pid) : _fields(ARGUMENTS) // arguments // are currently always valid, // they just return an empty // vector / map respectively // if no arguments // have been explicitly set , _pid(pid), _parentPid(0), _foregroundPid(0), _userId(0), _lastError(NoError), _name(QString()), _userName(QString()), _userHomeDir(QString()), _currentDir(QString()), _userNameRequired(true), _arguments(QVector()) { } ProcessInfo::Error ProcessInfo::error() const { return _lastError; } void ProcessInfo::setError(Error error) { _lastError = error; } void ProcessInfo::update() { readCurrentDir(_pid); } QString ProcessInfo::validCurrentDir() const { bool ok = false; // read current dir, if an error occurs try the parent as the next // best option int currentPid = parentPid(&ok); QString dir = currentDir(&ok); while (!ok && currentPid != 0) { ProcessInfo *current = ProcessInfo::newInstance(currentPid); current->update(); currentPid = current->parentPid(&ok); dir = current->currentDir(&ok); delete current; } return dir; } QSet ProcessInfo::_commonDirNames; QSet ProcessInfo::commonDirNames() { static bool forTheFirstTime = true; if (forTheFirstTime) { const KSharedConfigPtr &config = KSharedConfig::openConfig(); const KConfigGroup &configGroup = config->group("ProcessInfo"); _commonDirNames = QSet::fromList(configGroup.readEntry("CommonDirNames", QStringList())); forTheFirstTime = false; } return _commonDirNames; } QString ProcessInfo::formatShortDir(const QString &input) const { if(input == QLatin1String("/")) { return QStringLiteral("/"); } QString result; const QStringList &parts = input.split(QDir::separator()); QSet dirNamesToShorten = commonDirNames(); QListIterator iter(parts); iter.toBack(); // go backwards through the list of the path's parts // adding abbreviations of common directory names // and stopping when we reach a dir name which is not // in the commonDirNames set while (iter.hasPrevious()) { const QString &part = iter.previous(); if (dirNamesToShorten.contains(part)) { result.prepend(QDir::separator() + part[0]); } else { result.prepend(part); break; } } return result; } QVector ProcessInfo::arguments(bool *ok) const { *ok = _fields.testFlag(ARGUMENTS); return _arguments; } bool ProcessInfo::isValid() const { return _fields.testFlag(PROCESS_ID); } int ProcessInfo::pid(bool *ok) const { *ok = _fields.testFlag(PROCESS_ID); return _pid; } int ProcessInfo::parentPid(bool *ok) const { *ok = _fields.testFlag(PARENT_PID); return _parentPid; } int ProcessInfo::foregroundPid(bool *ok) const { *ok = _fields.testFlag(FOREGROUND_PID); return _foregroundPid; } QString ProcessInfo::name(bool *ok) const { *ok = _fields.testFlag(NAME); return _name; } int ProcessInfo::userId(bool *ok) const { *ok = _fields.testFlag(UID); return _userId; } QString ProcessInfo::userName() const { return _userName; } QString ProcessInfo::userHomeDir() const { return _userHomeDir; } QString ProcessInfo::localHost() { return QHostInfo::localHostName(); } void ProcessInfo::setPid(int pid) { _pid = pid; _fields |= PROCESS_ID; } void ProcessInfo::setUserId(int uid) { _userId = uid; _fields |= UID; } void ProcessInfo::setUserName(const QString &name) { _userName = name; setUserHomeDir(); } void ProcessInfo::setUserHomeDir() { const QString &usersName = userName(); if (!usersName.isEmpty()) { _userHomeDir = KUser(usersName).homeDir(); } else { _userHomeDir = QDir::homePath(); } } void ProcessInfo::setParentPid(int pid) { _parentPid = pid; _fields |= PARENT_PID; } void ProcessInfo::setForegroundPid(int pid) { _foregroundPid = pid; _fields |= FOREGROUND_PID; } void ProcessInfo::setUserNameRequired(bool need) { _userNameRequired = need; } bool ProcessInfo::userNameRequired() const { return _userNameRequired; } QString ProcessInfo::currentDir(bool *ok) const { if (ok != nullptr) { *ok = (_fields & CURRENT_DIR) != 0; } return _currentDir; } void ProcessInfo::setCurrentDir(const QString &dir) { _fields |= CURRENT_DIR; _currentDir = dir; } void ProcessInfo::setName(const QString &name) { _name = name; _fields |= NAME; } void ProcessInfo::addArgument(const QString &argument) { _arguments << argument; } void ProcessInfo::clearArguments() { _arguments.clear(); } void ProcessInfo::setFileError(QFile::FileError error) { switch (error) { case QFile::PermissionsError: setError(ProcessInfo::PermissionsError); break; case QFile::NoError: setError(ProcessInfo::NoError); break; default: setError(ProcessInfo::UnknownError); } } // // ProcessInfo::newInstance() is way at the bottom so it can see all of the // implementations of the UnixProcessInfo abstract class. // NullProcessInfo::NullProcessInfo(int pid) : ProcessInfo(pid) { } void NullProcessInfo::readProcessInfo(int /*pid*/) { } bool NullProcessInfo::readCurrentDir(int /*pid*/) { return false; } void NullProcessInfo::readUserName() { } #if !defined(Q_OS_WIN) UnixProcessInfo::UnixProcessInfo(int pid) : ProcessInfo(pid) { setUserNameRequired(true); } void UnixProcessInfo::readProcessInfo(int pid) { // prevent _arguments from growing longer and longer each time this // method is called. clearArguments(); if (readProcInfo(pid)) { readArguments(pid); readCurrentDir(pid); bool ok = false; const QString &processNameString = name(&ok); if (ok && processNameString == QLatin1String("sudo")) { //Append process name along with sudo const QVector &args = arguments(&ok); if (ok && args.size() > 1) { setName(processNameString + QStringLiteral(" ") + args[1]); } } } } void UnixProcessInfo::readUserName() { bool ok = false; const int uid = userId(&ok); if (!ok) { return; } struct passwd passwdStruct; struct passwd *getpwResult; char *getpwBuffer; long getpwBufferSize; int getpwStatus; getpwBufferSize = sysconf(_SC_GETPW_R_SIZE_MAX); if (getpwBufferSize == -1) { getpwBufferSize = 16384; } getpwBuffer = new char[getpwBufferSize]; if (getpwBuffer == nullptr) { return; } getpwStatus = getpwuid_r(uid, &passwdStruct, getpwBuffer, getpwBufferSize, &getpwResult); if ((getpwStatus == 0) && (getpwResult != nullptr)) { setUserName(QLatin1String(passwdStruct.pw_name)); } else { setUserName(QString()); qWarning() << "getpwuid_r returned error : " << getpwStatus; } delete [] getpwBuffer; } #endif #if defined(Q_OS_LINUX) class LinuxProcessInfo : public UnixProcessInfo { public: LinuxProcessInfo(int pid) : UnixProcessInfo(pid) { } protected: bool readCurrentDir(int pid) Q_DECL_OVERRIDE { char path_buffer[MAXPATHLEN + 1]; path_buffer[MAXPATHLEN] = 0; QByteArray procCwd = QFile::encodeName(QStringLiteral("/proc/%1/cwd").arg(pid)); const auto length = static_cast(readlink(procCwd.constData(), path_buffer, MAXPATHLEN)); if (length == -1) { setError(UnknownError); return false; } path_buffer[length] = '\0'; QString path = QFile::decodeName(path_buffer); setCurrentDir(path); return true; } private: bool readProcInfo(int pid) Q_DECL_OVERRIDE { // indicies of various fields within the process status file which // contain various information about the process const int PARENT_PID_FIELD = 3; const int PROCESS_NAME_FIELD = 1; const int GROUP_PROCESS_FIELD = 7; QString parentPidString; QString processNameString; QString foregroundPidString; QString uidLine; QString uidString; QStringList uidStrings; // For user id read process status file ( /proc//status ) // Can not use getuid() due to it does not work for 'su' QFile statusInfo(QStringLiteral("/proc/%1/status").arg(pid)); if (statusInfo.open(QIODevice::ReadOnly)) { QTextStream stream(&statusInfo); QString statusLine; do { statusLine = stream.readLine(); if (statusLine.startsWith(QLatin1String("Uid:"))) { uidLine = statusLine; } } while (!statusLine.isNull() && uidLine.isNull()); uidStrings << uidLine.split(QLatin1Char('\t'), QString::SkipEmptyParts); // Must be 5 entries: 'Uid: %d %d %d %d' and // uid string must be less than 5 chars (uint) if (uidStrings.size() == 5) { uidString = uidStrings[1]; } if (uidString.size() > 5) { uidString.clear(); } bool ok = false; const int uid = uidString.toInt(&ok); if (ok) { setUserId(uid); } // This will cause constant opening of /etc/passwd if (userNameRequired()) { readUserName(); setUserNameRequired(false); } } else { setFileError(statusInfo.error()); return false; } // read process status file ( /proc//cmdline // the expected format is a list of strings delimited by null characters, // and ending in a double null character pair. QFile argumentsFile(QStringLiteral("/proc/%1/cmdline").arg(pid)); if (argumentsFile.open(QIODevice::ReadOnly)) { QTextStream stream(&argumentsFile); const QString &data = stream.readAll(); const QStringList &argList = data.split(QLatin1Char('\0')); - foreach (const QString &entry, argList) { + for (const QString &entry : argList) { if (!entry.isEmpty()) { addArgument(entry); } } } else { setFileError(argumentsFile.error()); } return true; } }; #elif defined(Q_OS_FREEBSD) class FreeBSDProcessInfo : public UnixProcessInfo { public: FreeBSDProcessInfo(int pid) : UnixProcessInfo(pid) { } protected: bool readCurrentDir(int pid) Q_DECL_OVERRIDE { #if defined(HAVE_OS_DRAGONFLYBSD) char buf[PATH_MAX]; int managementInfoBase[4]; size_t len; managementInfoBase[0] = CTL_KERN; managementInfoBase[1] = KERN_PROC; managementInfoBase[2] = KERN_PROC_CWD; managementInfoBase[3] = pid; len = sizeof(buf); if (sysctl(managementInfoBase, 4, buf, &len, NULL, 0) == -1) { return false; } setCurrentDir(QString::fromUtf8(buf)); return true; #else int numrecords; struct kinfo_file *info = nullptr; info = kinfo_getfile(pid, &numrecords); if (!info) { return false; } for (int i = 0; i < numrecords; ++i) { if (info[i].kf_fd == KF_FD_TYPE_CWD) { setCurrentDir(QString::fromUtf8(info[i].kf_path)); free(info); return true; } } free(info); return false; #endif } private: bool readProcInfo(int pid) Q_DECL_OVERRIDE { int managementInfoBase[4]; size_t mibLength; struct kinfo_proc *kInfoProc; managementInfoBase[0] = CTL_KERN; managementInfoBase[1] = KERN_PROC; managementInfoBase[2] = KERN_PROC_PID; managementInfoBase[3] = pid; if (sysctl(managementInfoBase, 4, NULL, &mibLength, NULL, 0) == -1) { return false; } kInfoProc = new struct kinfo_proc [mibLength]; if (sysctl(managementInfoBase, 4, kInfoProc, &mibLength, NULL, 0) == -1) { delete [] kInfoProc; return false; } #if defined(HAVE_OS_DRAGONFLYBSD) setName(QString::fromUtf8(kInfoProc->kp_comm)); setPid(kInfoProc->kp_pid); setParentPid(kInfoProc->kp_ppid); setForegroundPid(kInfoProc->kp_pgid); setUserId(kInfoProc->kp_uid); #else setName(QString::fromUtf8(kInfoProc->ki_comm)); setPid(kInfoProc->ki_pid); setParentPid(kInfoProc->ki_ppid); setForegroundPid(kInfoProc->ki_pgid); setUserId(kInfoProc->ki_uid); #endif readUserName(); delete [] kInfoProc; return true; } bool readArguments(int pid) Q_DECL_OVERRIDE { char args[ARG_MAX]; int managementInfoBase[4]; size_t len; managementInfoBase[0] = CTL_KERN; managementInfoBase[1] = KERN_PROC; managementInfoBase[2] = KERN_PROC_ARGS; managementInfoBase[3] = pid; len = sizeof(args); if (sysctl(managementInfoBase, 4, args, &len, NULL, 0) == -1) { return false; } // len holds the length of the string - QString qargs = QString::fromLocal8Bit(args, len); - foreach (const QString &value, qargs.split(QLatin1Char('\u0000'))) { + const QStringList argurments = QString::fromLocal8Bit(args, len).split(QLatin1Char('\u0000')); + for (const QString &value : argurments) { if (!value.isEmpty()) { addArgument(value); } } return true; } }; #elif defined(Q_OS_OPENBSD) class OpenBSDProcessInfo : public UnixProcessInfo { public: OpenBSDProcessInfo(int pid) : UnixProcessInfo(pid) { } protected: bool readCurrentDir(int pid) Q_DECL_OVERRIDE { char buf[PATH_MAX]; int managementInfoBase[3]; size_t len; managementInfoBase[0] = CTL_KERN; managementInfoBase[1] = KERN_PROC_CWD; managementInfoBase[2] = pid; len = sizeof(buf); if (::sysctl(managementInfoBase, 3, buf, &len, NULL, 0) == -1) { qWarning() << "sysctl() call failed with code" << errno; return false; } setCurrentDir(QString::fromUtf8(buf)); return true; } private: bool readProcInfo(int pid) Q_DECL_OVERRIDE { int managementInfoBase[6]; size_t mibLength; struct kinfo_proc *kInfoProc; managementInfoBase[0] = CTL_KERN; managementInfoBase[1] = KERN_PROC; managementInfoBase[2] = KERN_PROC_PID; managementInfoBase[3] = pid; managementInfoBase[4] = sizeof(struct kinfo_proc); managementInfoBase[5] = 1; if (::sysctl(managementInfoBase, 6, NULL, &mibLength, NULL, 0) == -1) { qWarning() << "first sysctl() call failed with code" << errno; return false; } kInfoProc = (struct kinfo_proc *)malloc(mibLength); if (::sysctl(managementInfoBase, 6, kInfoProc, &mibLength, NULL, 0) == -1) { qWarning() << "second sysctl() call failed with code" << errno; free(kInfoProc); return false; } setName(kInfoProc->p_comm); setPid(kInfoProc->p_pid); setParentPid(kInfoProc->p_ppid); setForegroundPid(kInfoProc->p_tpgid); setUserId(kInfoProc->p_uid); setUserName(kInfoProc->p_login); free(kInfoProc); return true; } char **readProcArgs(int pid, int whatMib) { void *buf = NULL; void *nbuf; size_t len = 4096; int rc = -1; int managementInfoBase[4]; managementInfoBase[0] = CTL_KERN; managementInfoBase[1] = KERN_PROC_ARGS; managementInfoBase[2] = pid; managementInfoBase[3] = whatMib; do { len *= 2; nbuf = realloc(buf, len); if (nbuf == NULL) { break; } buf = nbuf; rc = ::sysctl(managementInfoBase, 4, buf, &len, NULL, 0); qWarning() << "sysctl() call failed with code" << errno; } while (rc == -1 && errno == ENOMEM); if (nbuf == NULL || rc == -1) { free(buf); return NULL; } return (char **)buf; } bool readArguments(int pid) Q_DECL_OVERRIDE { char **argv; argv = readProcArgs(pid, KERN_PROC_ARGV); if (argv == NULL) { return false; } for (char **p = argv; *p != NULL; p++) { addArgument(QString(*p)); } free(argv); return true; } }; #elif defined(Q_OS_MACOS) class MacProcessInfo : public UnixProcessInfo { public: MacProcessInfo(int pid) : UnixProcessInfo(pid) { } protected: bool readCurrentDir(int pid) Q_DECL_OVERRIDE { struct proc_vnodepathinfo vpi; const int nb = proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &vpi, sizeof(vpi)); if (nb == sizeof(vpi)) { setCurrentDir(QString::fromUtf8(vpi.pvi_cdir.vip_path)); return true; } return false; } private: bool readProcInfo(int pid) Q_DECL_OVERRIDE { int managementInfoBase[4]; size_t mibLength; struct kinfo_proc *kInfoProc; QT_STATBUF statInfo; // Find the tty device of 'pid' (Example: /dev/ttys001) managementInfoBase[0] = CTL_KERN; managementInfoBase[1] = KERN_PROC; managementInfoBase[2] = KERN_PROC_PID; managementInfoBase[3] = pid; if (sysctl(managementInfoBase, 4, nullptr, &mibLength, nullptr, 0) == -1) { return false; } else { kInfoProc = new struct kinfo_proc [mibLength]; if (sysctl(managementInfoBase, 4, kInfoProc, &mibLength, nullptr, 0) == -1) { delete [] kInfoProc; return false; } else { const QString deviceNumber = QString::fromUtf8(devname(((&kInfoProc->kp_eproc)->e_tdev), S_IFCHR)); const QString fullDeviceName = QStringLiteral("/dev/") + deviceNumber.rightJustified(3, QLatin1Char('0')); delete [] kInfoProc; const QByteArray deviceName = fullDeviceName.toLatin1(); const char *ttyName = deviceName.data(); if (QT_STAT(ttyName, &statInfo) != 0) { return false; } // Find all processes attached to ttyName managementInfoBase[0] = CTL_KERN; managementInfoBase[1] = KERN_PROC; managementInfoBase[2] = KERN_PROC_TTY; managementInfoBase[3] = statInfo.st_rdev; mibLength = 0; if (sysctl(managementInfoBase, sizeof(managementInfoBase) / sizeof(int), nullptr, &mibLength, nullptr, 0) == -1) { return false; } kInfoProc = new struct kinfo_proc [mibLength]; if (sysctl(managementInfoBase, sizeof(managementInfoBase) / sizeof(int), kInfoProc, &mibLength, nullptr, 0) == -1) { return false; } // The foreground program is the first one setName(QString::fromUtf8(kInfoProc->kp_proc.p_comm)); delete [] kInfoProc; } setPid(pid); } return true; } bool readArguments(int pid) Q_DECL_OVERRIDE { Q_UNUSED(pid); return false; } }; #elif defined(Q_OS_SOLARIS) // The procfs structure definition requires off_t to be // 32 bits, which only applies if FILE_OFFSET_BITS=32. // Futz around here to get it to compile regardless, // although some of the structure sizes might be wrong. // Fortunately, the structures we actually use don't use // off_t, and we're safe. #if defined(_FILE_OFFSET_BITS) && (_FILE_OFFSET_BITS == 64) #undef _FILE_OFFSET_BITS #endif #include class SolarisProcessInfo : public UnixProcessInfo { public: SolarisProcessInfo(int pid) : UnixProcessInfo(pid) { } protected: // FIXME: This will have the same issues as BKO 251351; the Linux // version uses readlink. bool readCurrentDir(int pid) Q_DECL_OVERRIDE { QFileInfo info(QString("/proc/%1/path/cwd").arg(pid)); const bool readable = info.isReadable(); if (readable && info.isSymLink()) { setCurrentDir(info.symLinkTarget()); return true; } else { if (!readable) { setError(PermissionsError); } else { setError(UnknownError); } return false; } } private: bool readProcInfo(int pid) Q_DECL_OVERRIDE { QFile psinfo(QString("/proc/%1/psinfo").arg(pid)); if (psinfo.open(QIODevice::ReadOnly)) { struct psinfo info; if (psinfo.read((char *)&info, sizeof(info)) != sizeof(info)) { return false; } setParentPid(info.pr_ppid); setForegroundPid(info.pr_pgid); setName(info.pr_fname); setPid(pid); // Bogus, because we're treating the arguments as one single string info.pr_psargs[PRARGSZ - 1] = 0; addArgument(info.pr_psargs); } return true; } bool readArguments(int /*pid*/) Q_DECL_OVERRIDE { // Handled in readProcInfo() return false; } }; #endif SSHProcessInfo::SSHProcessInfo(const ProcessInfo &process) : _process(process), _user(QString()), _host(QString()), _port(QString()), _command(QString()) { bool ok = false; // check that this is a SSH process const QString &name = _process.name(&ok); if (!ok || name != QLatin1String("ssh")) { if (!ok) { qWarning() << "Could not read process info"; } else { qWarning() << "Process is not a SSH process"; } return; } // read arguments const QVector &args = _process.arguments(&ok); // SSH options // these are taken from the SSH manual ( accessed via 'man ssh' ) // options which take no arguments static const QString noArgumentOptions(QStringLiteral("1246AaCfgKkMNnqsTtVvXxYy")); // options which take one argument static const QString singleArgumentOptions(QStringLiteral("bcDeFIiLlmOopRSWw")); if (ok) { // find the username, host and command arguments // // the username/host is assumed to be the first argument // which is not an option // ( ie. does not start with a dash '-' character ) // or an argument to a previous option. // // the command, if specified, is assumed to be the argument following // the username and host // // note that we skip the argument at index 0 because that is the // program name ( expected to be 'ssh' in this case ) for (int i = 1; i < args.count(); i++) { // If this one is an option ... // Most options together with its argument will be skipped. if (args[i].startsWith(QLatin1Char('-'))) { const QChar optionChar = (args[i].length() > 1) ? args[i][1] : QLatin1Char('\0'); // for example: -p2222 or -p 2222 ? const bool optionArgumentCombined = args[i].length() > 2; if (noArgumentOptions.contains(optionChar)) { continue; } else if (singleArgumentOptions.contains(optionChar)) { QString argument; if (optionArgumentCombined) { argument = args[i].mid(2); } else { // Verify correct # arguments are given if ((i + 1) < args.count()) { argument = args[i + 1]; } i++; } // support using `-l user` to specify username. if (optionChar == QLatin1Char('l')) { _user = argument; } // support using `-p port` to specify port. else if (optionChar == QLatin1Char('p')) { _port = argument; } continue; } } // check whether the host has been found yet // if not, this must be the username/host argument if (_host.isEmpty()) { // check to see if only a hostname is specified, or whether // both a username and host are specified ( in which case they // are separated by an '@' character: username@host ) const int separatorPosition = args[i].indexOf(QLatin1Char('@')); if (separatorPosition != -1) { // username and host specified _user = args[i].left(separatorPosition); _host = args[i].mid(separatorPosition + 1); } else { // just the host specified _host = args[i]; } } else { // host has already been found, this must be part of the // command arguments. // Note this is not 100% correct. If any of the above // noArgumentOptions or singleArgumentOptions are found, this // will not be correct (example ssh server top -i 50) // Suggest putting ssh command in quotes if (_command.isEmpty()) { _command = args[i]; } else { _command = _command + QLatin1Char(' ') + args[i]; } } } } else { qWarning() << "Could not read arguments"; return; } } QString SSHProcessInfo::userName() const { return _user; } QString SSHProcessInfo::host() const { return _host; } QString SSHProcessInfo::port() const { return _port; } QString SSHProcessInfo::command() const { return _command; } QString SSHProcessInfo::format(const QString &input) const { QString output(input); // search for and replace known markers output.replace(QLatin1String("%u"), _user); // provide 'user@' if user is defined -- this makes nicer // remote tabs possible: "%U%h %c" => User@Host Command // => Host Command // Depending on whether -l was passed to ssh (which is mostly not the // case due to ~/.ssh/config). if (_user.isEmpty()) { output.remove(QLatin1String("%U")); } else { output.replace(QLatin1String("%U"), _user + QLatin1Char('@')); } // test whether host is an ip address // in which case 'short host' and 'full host' // markers in the input string are replaced with // the full address struct in_addr address; const bool isIpAddress = inet_aton(_host.toLocal8Bit().constData(), &address) != 0; if (isIpAddress) { output.replace(QLatin1String("%h"), _host); } else { output.replace(QLatin1String("%h"), _host.left(_host.indexOf(QLatin1Char('.')))); } output.replace(QLatin1String("%H"), _host); output.replace(QLatin1String("%c"), _command); return output; } ProcessInfo *ProcessInfo::newInstance(int pid) { ProcessInfo *info; #if defined(Q_OS_LINUX) info = new LinuxProcessInfo(pid); #elif defined(Q_OS_SOLARIS) info = new SolarisProcessInfo(pid); #elif defined(Q_OS_MACOS) info = new MacProcessInfo(pid); #elif defined(Q_OS_FREEBSD) info = new FreeBSDProcessInfo(pid); #elif defined(Q_OS_OPENBSD) info = new OpenBSDProcessInfo(pid); #else info = new NullProcessInfo(pid); #endif info->readProcessInfo(pid); return info; } diff --git a/src/Profile.cpp b/src/Profile.cpp index 8bfef31f..63a7376b 100644 --- a/src/Profile.cpp +++ b/src/Profile.cpp @@ -1,395 +1,395 @@ /* This source file is part of Konsole, a terminal emulator. Copyright 2006-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "Profile.h" // Qt #include #include // KDE #include #include // Konsole #include "Enumeration.h" using namespace Konsole; // mappings between property enum values and names // // multiple names are defined for some property values, // in these cases, the "proper" string name comes first, // as that is used when reading/writing profiles from/to disk // // the other names are usually shorter versions for convenience // when parsing konsoleprofile commands static const char GENERAL_GROUP[] = "General"; static const char KEYBOARD_GROUP[] = "Keyboard"; static const char APPEARANCE_GROUP[] = "Appearance"; static const char SCROLLING_GROUP[] = "Scrolling"; static const char TERMINAL_GROUP[] = "Terminal Features"; static const char CURSOR_GROUP[] = "Cursor Options"; static const char INTERACTION_GROUP[] = "Interaction Options"; static const char ENCODING_GROUP[] = "Encoding Options"; const Profile::PropertyInfo Profile::DefaultPropertyNames[] = { // General { Path , "Path" , nullptr , QVariant::String } , { Name , "Name" , GENERAL_GROUP , QVariant::String } , { UntranslatedName, "UntranslatedName" , nullptr , QVariant::String } , { Icon , "Icon" , GENERAL_GROUP , QVariant::String } , { Command , "Command" , nullptr , QVariant::String } , { Arguments , "Arguments" , nullptr , QVariant::StringList } , { MenuIndex, "MenuIndex" , nullptr, QVariant::String } , { Environment , "Environment" , GENERAL_GROUP , QVariant::StringList } , { Directory , "Directory" , GENERAL_GROUP , QVariant::String } , { LocalTabTitleFormat , "LocalTabTitleFormat" , GENERAL_GROUP , QVariant::String } , { LocalTabTitleFormat , "tabtitle" , nullptr , QVariant::String } , { RemoteTabTitleFormat , "RemoteTabTitleFormat" , GENERAL_GROUP , QVariant::String } , { ShowTerminalSizeHint , "ShowTerminalSizeHint" , GENERAL_GROUP , QVariant::Bool } , { DimWhenInactive , "DimWhenInactive" , GENERAL_GROUP , QVariant::Bool } , { StartInCurrentSessionDir , "StartInCurrentSessionDir" , GENERAL_GROUP , QVariant::Bool } , { SilenceSeconds, "SilenceSeconds" , GENERAL_GROUP , QVariant::Int } , { TerminalColumns, "TerminalColumns" , GENERAL_GROUP , QVariant::Int } , { TerminalRows, "TerminalRows" , GENERAL_GROUP , QVariant::Int } , { TerminalMargin, "TerminalMargin" , GENERAL_GROUP , QVariant::Int } , { TerminalCenter, "TerminalCenter" , GENERAL_GROUP , QVariant::Bool } // Appearance , { Font , "Font" , APPEARANCE_GROUP , QVariant::Font } , { ColorScheme , "ColorScheme" , APPEARANCE_GROUP , QVariant::String } , { ColorScheme , "colors" , nullptr , QVariant::String } , { AntiAliasFonts, "AntiAliasFonts" , APPEARANCE_GROUP , QVariant::Bool } , { BoldIntense, "BoldIntense", APPEARANCE_GROUP, QVariant::Bool } , { UseFontLineCharacters, "UseFontLineChararacters", APPEARANCE_GROUP, QVariant::Bool } , { LineSpacing , "LineSpacing" , APPEARANCE_GROUP , QVariant::Int } // Keyboard , { KeyBindings , "KeyBindings" , KEYBOARD_GROUP , QVariant::String } // Scrolling , { HistoryMode , "HistoryMode" , SCROLLING_GROUP , QVariant::Int } , { HistorySize , "HistorySize" , SCROLLING_GROUP , QVariant::Int } , { ScrollBarPosition , "ScrollBarPosition" , SCROLLING_GROUP , QVariant::Int } , { ScrollFullPage , "ScrollFullPage" , SCROLLING_GROUP , QVariant::Bool } // Terminal Features , { UrlHintsModifiers , "UrlHintsModifiers" , TERMINAL_GROUP , QVariant::Int } , { ReverseUrlHints , "ReverseUrlHints" , TERMINAL_GROUP , QVariant::Bool } , { BlinkingTextEnabled , "BlinkingTextEnabled" , TERMINAL_GROUP , QVariant::Bool } , { FlowControlEnabled , "FlowControlEnabled" , TERMINAL_GROUP , QVariant::Bool } , { BidiRenderingEnabled , "BidiRenderingEnabled" , TERMINAL_GROUP , QVariant::Bool } , { BlinkingCursorEnabled , "BlinkingCursorEnabled" , TERMINAL_GROUP , QVariant::Bool } , { BellMode , "BellMode" , TERMINAL_GROUP , QVariant::Int } // Cursor , { UseCustomCursorColor , "UseCustomCursorColor" , CURSOR_GROUP , QVariant::Bool} , { CursorShape , "CursorShape" , CURSOR_GROUP , QVariant::Int} , { CustomCursorColor , "CustomCursorColor" , CURSOR_GROUP , QVariant::Color } // Interaction , { WordCharacters , "WordCharacters" , INTERACTION_GROUP , QVariant::String } , { TripleClickMode , "TripleClickMode" , INTERACTION_GROUP , QVariant::Int } , { UnderlineLinksEnabled , "UnderlineLinksEnabled" , INTERACTION_GROUP , QVariant::Bool } , { UnderlineFilesEnabled , "UnderlineFilesEnabled" , INTERACTION_GROUP , QVariant::Bool } , { OpenLinksByDirectClickEnabled , "OpenLinksByDirectClickEnabled" , INTERACTION_GROUP , QVariant::Bool } , { CtrlRequiredForDrag, "CtrlRequiredForDrag" , INTERACTION_GROUP , QVariant::Bool } , { DropUrlsAsText , "DropUrlsAsText" , INTERACTION_GROUP , QVariant::Bool } , { AutoCopySelectedText , "AutoCopySelectedText" , INTERACTION_GROUP , QVariant::Bool } , { CopyTextAsHTML , "CopyTextAsHTML" , INTERACTION_GROUP , QVariant::Bool } , { TrimLeadingSpacesInSelectedText , "TrimLeadingSpacesInSelectedText" , INTERACTION_GROUP , QVariant::Bool } , { TrimTrailingSpacesInSelectedText , "TrimTrailingSpacesInSelectedText" , INTERACTION_GROUP , QVariant::Bool } , { PasteFromSelectionEnabled , "PasteFromSelectionEnabled" , INTERACTION_GROUP , QVariant::Bool } , { PasteFromClipboardEnabled , "PasteFromClipboardEnabled" , INTERACTION_GROUP , QVariant::Bool } , { MiddleClickPasteMode, "MiddleClickPasteMode" , INTERACTION_GROUP , QVariant::Int } , { MouseWheelZoomEnabled, "MouseWheelZoomEnabled", INTERACTION_GROUP, QVariant::Bool } , { AlternateScrolling, "AlternateScrolling", INTERACTION_GROUP, QVariant::Bool } // Encoding , { DefaultEncoding , "DefaultEncoding" , ENCODING_GROUP , QVariant::String } , { static_cast(0) , nullptr , nullptr, QVariant::Invalid } }; QHash Profile::PropertyInfoByName; QHash Profile::PropertyInfoByProperty; void Profile::fillTableWithDefaultNames() { static bool filledDefaults = false; if (filledDefaults) { return; } const PropertyInfo* iter = DefaultPropertyNames; while (iter->name != nullptr) { registerProperty(*iter); iter++; } filledDefaults = true; } void Profile::useFallback() { // Fallback settings setProperty(Name, i18nc("Name of the default/builtin profile", "Default")); setProperty(UntranslatedName, QStringLiteral("Default")); // magic path for the fallback profile which is not a valid // non-directory file name setProperty(Path, QStringLiteral("FALLBACK/")); setProperty(Command, QString::fromUtf8(qgetenv("SHELL"))); // See Pty.cpp on why Arguments is populated setProperty(Arguments, QStringList() << QString::fromUtf8(qgetenv("SHELL"))); setProperty(Icon, QStringLiteral("utilities-terminal")); setProperty(Environment, QStringList() << QStringLiteral("TERM=xterm-256color") << QStringLiteral("COLORTERM=truecolor")); setProperty(LocalTabTitleFormat, QStringLiteral("%d : %n")); setProperty(RemoteTabTitleFormat, QStringLiteral("(%u) %H")); setProperty(ShowTerminalSizeHint, true); setProperty(DimWhenInactive, false); setProperty(StartInCurrentSessionDir, true); setProperty(MenuIndex, QStringLiteral("0")); setProperty(SilenceSeconds, 10); setProperty(TerminalColumns, 80); setProperty(TerminalRows, 24); setProperty(TerminalMargin, 1); setProperty(TerminalCenter, false); setProperty(MouseWheelZoomEnabled, true); setProperty(AlternateScrolling, true); setProperty(KeyBindings, QStringLiteral("default")); setProperty(ColorScheme, QStringLiteral("Breeze")); setProperty(Font, QFontDatabase::systemFont(QFontDatabase::FixedFont)); setProperty(HistoryMode, Enum::FixedSizeHistory); setProperty(HistorySize, 1000); setProperty(ScrollBarPosition, Enum::ScrollBarRight); setProperty(ScrollFullPage, false); setProperty(FlowControlEnabled, true); setProperty(UrlHintsModifiers, 0); setProperty(ReverseUrlHints, false); setProperty(BlinkingTextEnabled, true); setProperty(UnderlineLinksEnabled, true); setProperty(UnderlineFilesEnabled, false); setProperty(OpenLinksByDirectClickEnabled, false); setProperty(CtrlRequiredForDrag, true); setProperty(AutoCopySelectedText, false); setProperty(CopyTextAsHTML, true); setProperty(TrimLeadingSpacesInSelectedText, false); setProperty(TrimTrailingSpacesInSelectedText, false); setProperty(DropUrlsAsText, true); setProperty(PasteFromSelectionEnabled, true); setProperty(PasteFromClipboardEnabled, false); setProperty(MiddleClickPasteMode, Enum::PasteFromX11Selection); setProperty(TripleClickMode, Enum::SelectWholeLine); setProperty(BlinkingCursorEnabled, false); setProperty(BidiRenderingEnabled, true); setProperty(LineSpacing, 0); setProperty(CursorShape, Enum::BlockCursor); setProperty(UseCustomCursorColor, false); setProperty(CustomCursorColor, QColor(Qt::black)); setProperty(BellMode, Enum::NotifyBell); setProperty(DefaultEncoding, QLatin1String(QTextCodec::codecForLocale()->name())); setProperty(AntiAliasFonts, true); setProperty(BoldIntense, true); setProperty(UseFontLineCharacters, false); setProperty(WordCharacters, QStringLiteral(":@-./_~?&=%+#")); // Fallback should not be shown in menus setHidden(true); } Profile::Profile(const Profile::Ptr &parent) : _propertyValues(QHash()) , _parent(parent) , _hidden(false) { } void Profile::clone(Profile::Ptr profile, bool differentOnly) { const PropertyInfo* properties = DefaultPropertyNames; while (properties->name != nullptr) { Property current = properties->property; QVariant otherValue = profile->property(current); switch (current) { case Name: case Path: break; default: if (!differentOnly || property(current) != otherValue) { setProperty(current, otherValue); } } properties++; } } Profile::~Profile() = default; bool Profile::isHidden() const { return _hidden; } void Profile::setHidden(bool hidden) { _hidden = hidden; } void Profile::setParent(const Profile::Ptr &parent) { _parent = parent; } const Profile::Ptr Profile::parent() const { return _parent; } bool Profile::isEmpty() const { return _propertyValues.isEmpty(); } QHash Profile::setProperties() const { return _propertyValues; } void Profile::setProperty(Property p, const QVariant& value) { _propertyValues.insert(p, value); } bool Profile::isPropertySet(Property p) const { return _propertyValues.contains(p); } Profile::Property Profile::lookupByName(const QString& name) { // insert default names into table the first time this is called fillTableWithDefaultNames(); return PropertyInfoByName[name.toLower()].property; } void Profile::registerProperty(const PropertyInfo& info) { QString name = QLatin1String(info.name); PropertyInfoByName.insert(name.toLower(), info); // only allow one property -> name map // (multiple name -> property mappings are allowed though) if (!PropertyInfoByProperty.contains(info.property)) { PropertyInfoByProperty.insert(info.property, info); } } int Profile::menuIndexAsInt() const { bool ok; int index = menuIndex().toInt(&ok, 10); if (ok) { return index; } return 0; } const QStringList Profile::propertiesInfoList() const { QStringList info; const PropertyInfo* iter = DefaultPropertyNames; while (iter->name != nullptr) { info << QLatin1String(iter->name) + QStringLiteral(" : ") + QLatin1String(QVariant(iter->type).typeName()); iter++; } return info; } QHash ProfileCommandParser::parse(const QString& input) { QHash changes; // regular expression to parse profile change requests. // // format: property=value;property=value ... // // where 'property' is a word consisting only of characters from A-Z // where 'value' is any sequence of characters other than a semi-colon // static const QRegularExpression regExp(QStringLiteral("([a-zA-Z]+)=([^;]+)")); QRegularExpressionMatchIterator iterator(regExp.globalMatch(input)); while (iterator.hasNext()) { QRegularExpressionMatch match(iterator.next()); Profile::Property property = Profile::lookupByName(match.captured(1)); const QString value = match.captured(2); changes.insert(property, value); } return changes; } void ProfileGroup::updateValues() { const PropertyInfo* properties = Profile::DefaultPropertyNames; while (properties->name != nullptr) { // the profile group does not store a value for some properties // (eg. name, path) if even they are equal between profiles - // // the exception is when the group has only one profile in which // case it behaves like a standard Profile if (_profiles.count() > 1 && !canInheritProperty(properties->property)) { properties++; continue; } QVariant value; for (int i = 0; i < _profiles.count(); i++) { QVariant profileValue = _profiles[i]->property(properties->property); if (value.isNull()) { value = profileValue; } else if (value != profileValue) { value = QVariant(); break; } } Profile::setProperty(properties->property, value); properties++; } } void ProfileGroup::setProperty(Property p, const QVariant& value) { if (_profiles.count() > 1 && !canInheritProperty(p)) { return; } Profile::setProperty(p, value); - foreach(Profile::Ptr profile, _profiles) { + for (const Profile::Ptr &profile : qAsConst(_profiles)) { profile->setProperty(p, value); } } diff --git a/src/ProfileList.cpp b/src/ProfileList.cpp index c3c36563..377ac483 100644 --- a/src/ProfileList.cpp +++ b/src/ProfileList.cpp @@ -1,185 +1,186 @@ /* Copyright 2006-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "ProfileList.h" // Qt #include #include // KDE #include // Konsole #include "ProfileManager.h" using Konsole::Profile; using Konsole::ProfileList; ProfileList::ProfileList(bool addShortcuts , QObject* parent) : QObject(parent) , _group(nullptr) , _addShortcuts(addShortcuts) , _emptyListAction(nullptr) , _registeredWidgets(QSet()) { // construct the list of favorite profiles _group = new QActionGroup(this); // Even when there are no favorite profiles, allow user to // create new tabs using the default profile from the menu _emptyListAction = new QAction(i18n("Default profile"), _group); // TODO - Handle re-sorts when user changes profile names ProfileManager* manager = ProfileManager::instance(); - QList favoriteProfiles = manager->sortedFavorites(); + const QList favoriteProfiles = manager->sortedFavorites(); - foreach(const Profile::Ptr& profile, favoriteProfiles) { + for (const Profile::Ptr &profile : favoriteProfiles) { favoriteChanged(profile, true); } connect(_group, &QActionGroup::triggered, this, &Konsole::ProfileList::triggered); // listen for future changes to the profiles connect(manager, &Konsole::ProfileManager::favoriteStatusChanged, this, &Konsole::ProfileList::favoriteChanged); connect(manager, &Konsole::ProfileManager::shortcutChanged, this, &Konsole::ProfileList::shortcutChanged); connect(manager, &Konsole::ProfileManager::profileChanged, this, &Konsole::ProfileList::profileChanged); } void ProfileList::updateEmptyAction() { Q_ASSERT(_group); Q_ASSERT(_emptyListAction); // show this action only when it is the only action in the group const bool showEmptyAction = (_group->actions().count() == 1); if (showEmptyAction != _emptyListAction->isVisible()) { _emptyListAction->setVisible(showEmptyAction); } } QAction* ProfileList::actionForProfile(const Profile::Ptr &profile) const { - foreach(QAction* action, _group->actions()) { + const QList actionsList = _group->actions(); + for (QAction *action : actionsList) { if (action->data().value() == profile) { return action; } } return nullptr; // not found } void ProfileList::profileChanged(const Profile::Ptr &profile) { QAction* action = actionForProfile(profile); if (action != nullptr) { updateAction(action, profile); } } void ProfileList::updateAction(QAction* action , Profile::Ptr profile) { Q_ASSERT(action); Q_ASSERT(profile); action->setText(profile->name()); action->setIcon(QIcon::fromTheme(profile->icon())); } void ProfileList::shortcutChanged(const Profile::Ptr &profile, const QKeySequence& sequence) { if (!_addShortcuts) { return; } QAction* action = actionForProfile(profile); if (action != nullptr) { action->setShortcut(sequence); } } void ProfileList::syncWidgetActions(QWidget* widget, bool sync) { if (!sync) { _registeredWidgets.remove(widget); return; } _registeredWidgets.insert(widget); const QList currentActions = widget->actions(); - foreach(QAction * currentAction, currentActions) { + for (QAction *currentAction : currentActions) { widget->removeAction(currentAction); } widget->addActions(_group->actions()); } void ProfileList::addShortcutAction(const Profile::Ptr &profile) { ProfileManager* manager = ProfileManager::instance(); auto action = new QAction(_group); action->setData(QVariant::fromValue(profile)); if (_addShortcuts) { action->setShortcut(manager->shortcut(profile)); } updateAction(action, profile); - foreach(QWidget * widget, _registeredWidgets) { + for (QWidget *widget : qAsConst(_registeredWidgets)) { widget->addAction(action); } emit actionsChanged(_group->actions()); updateEmptyAction(); } void ProfileList::removeShortcutAction(const Profile::Ptr &profile) { QAction* action = actionForProfile(profile); if (action != nullptr) { _group->removeAction(action); - foreach(QWidget * widget, _registeredWidgets) { + for (QWidget *widget : qAsConst(_registeredWidgets)) { widget->removeAction(action); } emit actionsChanged(_group->actions()); } updateEmptyAction(); } void ProfileList::favoriteChanged(const Profile::Ptr &profile, bool isFavorite) { if (isFavorite) { addShortcutAction(profile); } else { removeShortcutAction(profile); } } void ProfileList::triggered(QAction* action) { emit profileSelected(action->data().value()); } QList ProfileList::actions() { return _group->actions(); } diff --git a/src/ProfileManager.cpp b/src/ProfileManager.cpp index fa6f3822..1e98675d 100644 --- a/src/ProfileManager.cpp +++ b/src/ProfileManager.cpp @@ -1,699 +1,701 @@ /* This source file is part of Konsole, a terminal emulator. Copyright 2006-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "ProfileManager.h" #include "konsoledebug.h" // Qt #include #include #include #include // KDE #include #include #include #include #include // Konsole #include "ProfileReader.h" #include "ProfileWriter.h" using namespace Konsole; static bool profileIndexLessThan(const Profile::Ptr& p1, const Profile::Ptr& p2) { return p1->menuIndexAsInt() < p2->menuIndexAsInt(); } static bool profileNameLessThan(const Profile::Ptr& p1, const Profile::Ptr& p2) { return QString::localeAwareCompare(p1->name(), p2->name()) < 0; } static bool stringLessThan(const QString& p1, const QString& p2) { return QString::localeAwareCompare(p1, p2) < 0; } static void sortByIndexProfileList(QList& list) { std::stable_sort(list.begin(), list.end(), profileIndexLessThan); } static void sortByNameProfileList(QList& list) { std::stable_sort(list.begin(), list.end(), profileNameLessThan); } ProfileManager::ProfileManager() : _profiles(QSet()) , _favorites(QSet()) , _defaultProfile(nullptr) , _fallbackProfile(nullptr) , _loadedAllProfiles(false) , _loadedFavorites(false) , _shortcuts(QMap()) { //load fallback profile _fallbackProfile = Profile::Ptr(new Profile()); _fallbackProfile->useFallback(); addProfile(_fallbackProfile); // lookup the default profile specified in rc // for stand-alone Konsole, appConfig is just konsolerc // for konsolepart, appConfig might be yakuakerc, dolphinrc, katerc... KSharedConfigPtr appConfig = KSharedConfig::openConfig(); KConfigGroup group = appConfig->group("Desktop Entry"); QString defaultProfileFileName = group.readEntry("DefaultProfile", ""); // if the hosting application of konsolepart does not specify its own // default profile, use the default profile of stand-alone Konsole. if (defaultProfileFileName.isEmpty()) { KSharedConfigPtr konsoleConfig = KSharedConfig::openConfig(QStringLiteral("konsolerc")); group = konsoleConfig->group("Desktop Entry"); defaultProfileFileName = group.readEntry("DefaultProfile", ""); } _defaultProfile = _fallbackProfile; if (!defaultProfileFileName.isEmpty()) { // load the default profile const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("konsole/") + defaultProfileFileName); if (!path.isEmpty()) { Profile::Ptr profile = loadProfile(path); if (profile) { _defaultProfile = profile; } } } Q_ASSERT(_profiles.count() > 0); Q_ASSERT(_defaultProfile); // get shortcuts and paths of profiles associated with // them - this doesn't load the shortcuts themselves, // that is done on-demand. loadShortcuts(); } ProfileManager::~ProfileManager() = default; Q_GLOBAL_STATIC(ProfileManager, theProfileManager) ProfileManager* ProfileManager::instance() { return theProfileManager; } Profile::Ptr ProfileManager::loadProfile(const QString& shortPath) { // the fallback profile has a 'special' path name, "FALLBACK/" if (shortPath == _fallbackProfile->path()) { return _fallbackProfile; } QString path = shortPath; // add a suggested suffix and relative prefix if missing QFileInfo fileInfo(path); if (fileInfo.isDir()) { return Profile::Ptr(); } if (fileInfo.suffix() != QLatin1String("profile")) { path.append(QLatin1String(".profile")); } if (fileInfo.path().isEmpty() || fileInfo.path() == QLatin1Char('.')) { path.prepend(QLatin1String("konsole") + QDir::separator()); } // if the file is not an absolute path, look it up if (!fileInfo.isAbsolute()) { path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, path); } // if the file is not found, return immediately if (path.isEmpty()) { return Profile::Ptr(); } // check that we have not already loaded this profile - foreach(const Profile::Ptr& profile, _profiles) { + for (const Profile::Ptr &profile : qAsConst(_profiles)) { if (profile->path() == path) { return profile; } } // guard to prevent problems if a profile specifies itself as its parent // or if there is recursion in the "inheritance" chain // (eg. two profiles, A and B, specifying each other as their parents) static QStack recursionGuard; PopStackOnExit popGuardOnExit(recursionGuard); if (recursionGuard.contains(path)) { qCDebug(KonsoleDebug) << "Ignoring attempt to load profile recursively from" << path; return _fallbackProfile; } else { recursionGuard.push(path); } // load the profile ProfileReader reader; Profile::Ptr newProfile = Profile::Ptr(new Profile(fallbackProfile())); newProfile->setProperty(Profile::Path, path); QString parentProfilePath; bool result = reader.readProfile(path, newProfile, parentProfilePath); if (!parentProfilePath.isEmpty()) { Profile::Ptr parentProfile = loadProfile(parentProfilePath); newProfile->setParent(parentProfile); } if (!result) { qCDebug(KonsoleDebug) << "Could not load profile from " << path; return Profile::Ptr(); } else if (newProfile->name().isEmpty()) { qCWarning(KonsoleDebug) << path << " does not have a valid name, ignoring."; return Profile::Ptr(); } else { addProfile(newProfile); return newProfile; } } QStringList ProfileManager::availableProfilePaths() const { ProfileReader reader; QStringList paths; paths += reader.findProfiles(); std::stable_sort(paths.begin(), paths.end(), stringLessThan); return paths; } QStringList ProfileManager::availableProfileNames() const { QStringList names; - foreach(Profile::Ptr profile, ProfileManager::instance()->allProfiles()) { + const QList allProfiles = ProfileManager::instance()->allProfiles(); + for (const Profile::Ptr &profile : allProfiles) { if (!profile->isHidden()) { names.push_back(profile->name()); } } std::stable_sort(names.begin(), names.end(), stringLessThan); return names; } void ProfileManager::loadAllProfiles() { if (_loadedAllProfiles) { return; } const QStringList& paths = availableProfilePaths(); - foreach(const QString& path, paths) { + for (const QString &path : paths) { loadProfile(path); } _loadedAllProfiles = true; } void ProfileManager::sortProfiles(QList& list) { QList lackingIndices; QList havingIndices; for (const auto & i : list) { // dis-regard the fallback profile if (i->path() == _fallbackProfile->path()) { continue; } if (i->menuIndexAsInt() == 0) { lackingIndices.append(i); } else { havingIndices.append(i); } } // sort by index sortByIndexProfileList(havingIndices); // sort alphabetically those w/o an index sortByNameProfileList(lackingIndices); // Put those with indices in sequential order w/o any gaps int i = 0; for (i = 0; i < havingIndices.size(); ++i) { Profile::Ptr tempProfile = havingIndices.at(i); tempProfile->setProperty(Profile::MenuIndex, QString::number(i + 1)); havingIndices.replace(i, tempProfile); } // Put those w/o indices in sequential order for (int j = 0; j < lackingIndices.size(); ++j) { Profile::Ptr tempProfile = lackingIndices.at(j); tempProfile->setProperty(Profile::MenuIndex, QString::number(j + 1 + i)); lackingIndices.replace(j, tempProfile); } // combine the 2 list: first those who had indices list.clear(); list.append(havingIndices); list.append(lackingIndices); } void ProfileManager::saveSettings() { // save default profile saveDefaultProfile(); // save shortcuts saveShortcuts(); // save favorites saveFavorites(); // ensure default/favorites/shortcuts settings are synced into disk KSharedConfigPtr appConfig = KSharedConfig::openConfig(); appConfig->sync(); } QList ProfileManager::sortedFavorites() { QList favorites = findFavorites().toList(); sortProfiles(favorites); return favorites; } QList ProfileManager::allProfiles() { loadAllProfiles(); return _profiles.toList(); } QList ProfileManager::loadedProfiles() const { return _profiles.toList(); } Profile::Ptr ProfileManager::defaultProfile() const { return _defaultProfile; } Profile::Ptr ProfileManager::fallbackProfile() const { return _fallbackProfile; } QString ProfileManager::saveProfile(const Profile::Ptr &profile) { ProfileWriter writer; QString newPath = writer.getPath(profile); if (!writer.writeProfile(newPath, profile)) { KMessageBox::sorry(nullptr, i18n("Konsole does not have permission to save this profile to %1", newPath)); } return newPath; } void ProfileManager::changeProfile(Profile::Ptr profile, QHash propertyMap, bool persistent) { Q_ASSERT(profile); const QString origPath = profile->path(); // never save a profile with empty name into disk! persistent = persistent && !profile->name().isEmpty(); Profile::Ptr newProfile; // If we are asked to store the fallback profile (which has an // invalid path by design), we reset the path to an empty string // which will make the profile writer automatically generate a // proper path. if (persistent && profile->path() == _fallbackProfile->path()) { // Generate a new name, so it is obvious what is actually built-in // in the profile manager - QList existingProfiles = allProfiles(); QStringList existingProfileNames; - foreach(Profile::Ptr existingProfile, existingProfiles) { + const QList profiles = allProfiles(); + for (const Profile::Ptr &existingProfile : profiles) { existingProfileNames.append(existingProfile->name()); } int nameSuffix = 1; QString newName; QString newTranslatedName; do { newName = QLatin1String("Profile ") + QString::number(nameSuffix); newTranslatedName = i18nc("The default name of a profile", "Profile #%1", nameSuffix); // TODO: remove the # above and below - too many issues newTranslatedName.remove(QLatin1Char('#')); nameSuffix++; } while (existingProfileNames.contains(newName)); newProfile = Profile::Ptr(new Profile(ProfileManager::instance()->fallbackProfile())); newProfile->clone(profile, true); newProfile->setProperty(Profile::UntranslatedName, newName); newProfile->setProperty(Profile::Name, newTranslatedName); newProfile->setProperty(Profile::MenuIndex, QStringLiteral("0")); newProfile->setHidden(false); addProfile(newProfile); setDefaultProfile(newProfile); } else { newProfile = profile; }; // insert the changes into the existing Profile instance QListIterator iter(propertyMap.keys()); while (iter.hasNext()) { const Profile::Property property = iter.next(); newProfile->setProperty(property, propertyMap[property]); } // when changing a group, iterate through the profiles // in the group and call changeProfile() on each of them // // this is so that each profile in the group, the profile is // applied, a change notification is emitted and the profile // is saved to disk ProfileGroup::Ptr group = newProfile->asGroup(); if (group) { - foreach(const Profile::Ptr & groupProfile, group->profiles()) { + const QList profiles = group->profiles(); + for (const Profile::Ptr &groupProfile : profiles) { changeProfile(groupProfile, propertyMap, persistent); } return; } // save changes to disk, unless the profile is hidden, in which case // it has no file on disk if (persistent && !newProfile->isHidden()) { newProfile->setProperty(Profile::Path, saveProfile(newProfile)); // if the profile was renamed, after saving the new profile // delete the old/redundant profile. // only do this if origPath is not empty, because it's empty // when creating a new profile, this works around a bug where // the newly created profile appears twice in the ProfileSettings // dialog if (!origPath.isEmpty() && (newProfile->path() != origPath)) { // this is needed to include the old profile too _loadedAllProfiles = false; const QList availableProfiles = ProfileManager::instance()->allProfiles(); - foreach(auto oldProfile, availableProfiles) { + for (const Profile::Ptr &oldProfile : availableProfiles) { if (oldProfile->path() == origPath) { // assign the same shortcut of the old profile to // the newly renamed profile const auto oldShortcut = shortcut(oldProfile); if (deleteProfile(oldProfile)) { setShortcut(newProfile, oldShortcut); } } } } } // notify the world about the change emit profileChanged(newProfile); } void ProfileManager::addProfile(const Profile::Ptr &profile) { if (_profiles.isEmpty()) { _defaultProfile = profile; } _profiles.insert(profile); emit profileAdded(profile); } bool ProfileManager::deleteProfile(Profile::Ptr profile) { bool wasDefault = (profile == defaultProfile()); if (profile) { // try to delete the config file if (profile->isPropertySet(Profile::Path) && QFile::exists(profile->path())) { if (!QFile::remove(profile->path())) { qCDebug(KonsoleDebug) << "Could not delete profile: " << profile->path() << "The file is most likely in a directory which is read-only."; return false; } } // remove from favorites, profile list, shortcut list etc. setFavorite(profile, false); setShortcut(profile, QKeySequence()); _profiles.remove(profile); // mark the profile as hidden so that it does not show up in the // Manage Profiles dialog and is not saved to disk profile->setHidden(true); } // If we just deleted the default profile, replace it with the first // profile in the list. if (wasDefault) { const QList existingProfiles = allProfiles(); setDefaultProfile(existingProfiles.at(0)); } emit profileRemoved(profile); return true; } void ProfileManager::setDefaultProfile(const Profile::Ptr &profile) { Q_ASSERT(_profiles.contains(profile)); _defaultProfile = profile; } void ProfileManager::saveDefaultProfile() { QString path = _defaultProfile->path(); ProfileWriter writer; if (path.isEmpty()) { path = writer.getPath(_defaultProfile); } QFileInfo fileInfo(path); KSharedConfigPtr appConfig = KSharedConfig::openConfig(); KConfigGroup group = appConfig->group("Desktop Entry"); group.writeEntry("DefaultProfile", fileInfo.fileName()); } QSet ProfileManager::findFavorites() { loadFavorites(); return _favorites; } void ProfileManager::setFavorite(const Profile::Ptr &profile , bool favorite) { if (!_profiles.contains(profile)) { addProfile(profile); } if (favorite && !_favorites.contains(profile)) { _favorites.insert(profile); emit favoriteStatusChanged(profile, favorite); } else if (!favorite && _favorites.contains(profile)) { _favorites.remove(profile); emit favoriteStatusChanged(profile, favorite); } } void ProfileManager::loadShortcuts() { KSharedConfigPtr appConfig = KSharedConfig::openConfig(); KConfigGroup shortcutGroup = appConfig->group("Profile Shortcuts"); QMap entries = shortcutGroup.entryMap(); QMapIterator iter(entries); while (iter.hasNext()) { iter.next(); QKeySequence shortcut = QKeySequence::fromString(iter.key()); QString profilePath = iter.value(); ShortcutData data; // if the file is not an absolute path, look it up QFileInfo fileInfo(profilePath); if (!fileInfo.isAbsolute()) { profilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("konsole/") + profilePath); } data.profilePath = profilePath; _shortcuts.insert(shortcut, data); } } QString ProfileManager::normalizePath(const QString& path) const { QFileInfo fileInfo(path); const QString location = QStandardPaths::locate( QStandardPaths::GenericDataLocation, QStringLiteral("konsole/") + fileInfo.fileName()); return (!fileInfo.isAbsolute()) || location.isEmpty() ? path : fileInfo.fileName(); } void ProfileManager::saveShortcuts() { KSharedConfigPtr appConfig = KSharedConfig::openConfig(); KConfigGroup shortcutGroup = appConfig->group("Profile Shortcuts"); shortcutGroup.deleteGroup(); QMapIterator iter(_shortcuts); while (iter.hasNext()) { iter.next(); QString shortcutString = iter.key().toString(); QString profileName = normalizePath(iter.value().profilePath); shortcutGroup.writeEntry(shortcutString, profileName); } } void ProfileManager::saveFavorites() { KSharedConfigPtr appConfig = KSharedConfig::openConfig(); KConfigGroup favoriteGroup = appConfig->group("Favorite Profiles"); QStringList paths; - foreach(const Profile::Ptr& profile, _favorites) { + for (const Profile::Ptr &profile : qAsConst(_favorites)) { Q_ASSERT(_profiles.contains(profile) && profile); paths << normalizePath(profile->path()); } favoriteGroup.writeEntry("Favorites", paths); } void ProfileManager::setShortcut(Profile::Ptr profile , const QKeySequence& keySequence) { QKeySequence existingShortcut = shortcut(profile); _shortcuts.remove(existingShortcut); if (keySequence.isEmpty()) { return; } ShortcutData data; data.profileKey = profile; data.profilePath = profile->path(); // TODO - This won't work if the profile doesn't // have a path yet _shortcuts.insert(keySequence, data); emit shortcutChanged(profile, keySequence); } void ProfileManager::loadFavorites() { if (_loadedFavorites) { return; } KSharedConfigPtr appConfig = KSharedConfig::openConfig(); KConfigGroup favoriteGroup = appConfig->group("Favorite Profiles"); QSet favoriteSet; if (favoriteGroup.hasKey("Favorites")) { QStringList list = favoriteGroup.readEntry("Favorites", QStringList()); favoriteSet = QSet::fromList(list); } // look for favorites among those already loaded - foreach(const Profile::Ptr& profile, _profiles) { + for (const Profile::Ptr &profile : qAsConst(_profiles)) { const QString& path = profile->path(); if (favoriteSet.contains(path)) { _favorites.insert(profile); favoriteSet.remove(path); } } // load any remaining favorites - foreach(const QString& favorite, favoriteSet) { + for (const QString &favorite : qAsConst(favoriteSet)) { Profile::Ptr profile = loadProfile(favorite); if (profile) { _favorites.insert(profile); } } _loadedFavorites = true; } QList ProfileManager::shortcuts() { return _shortcuts.keys(); } Profile::Ptr ProfileManager::findByShortcut(const QKeySequence& shortcut) { Q_ASSERT(_shortcuts.contains(shortcut)); if (!_shortcuts[shortcut].profileKey) { Profile::Ptr key = loadProfile(_shortcuts[shortcut].profilePath); if (!key) { _shortcuts.remove(shortcut); return Profile::Ptr(); } _shortcuts[shortcut].profileKey = key; } return _shortcuts[shortcut].profileKey; } QKeySequence ProfileManager::shortcut(Profile::Ptr profile) const { QMapIterator iter(_shortcuts); while (iter.hasNext()) { iter.next(); if (iter.value().profileKey == profile || iter.value().profilePath == profile->path()) { return iter.key(); } } return QKeySequence(); } diff --git a/src/Pty.cpp b/src/Pty.cpp index 7398dd66..6ce3a824 100644 --- a/src/Pty.cpp +++ b/src/Pty.cpp @@ -1,341 +1,341 @@ /* This file is part of Konsole, an X terminal. 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 "Pty.h" #include "konsoledebug.h" // System #include #include // Qt #include #include // KDE #include using Konsole::Pty; Pty::Pty(int masterFd, QObject *aParent) : KPtyProcess(masterFd, aParent) { init(); } Pty::Pty(QObject *aParent) : KPtyProcess(aParent) { init(); } void Pty::init() { _windowColumns = 0; _windowLines = 0; _eraseChar = 0; _xonXoff = true; _utf8 = true; setEraseChar(_eraseChar); setFlowControlEnabled(_xonXoff); setUtf8Mode(_utf8); setWindowSize(_windowColumns, _windowLines); setUseUtmp(true); setPtyChannels(KPtyProcess::AllChannels); connect(pty(), &KPtyDevice::readyRead, this, &Konsole::Pty::dataReceived); } Pty::~Pty() = default; void Pty::sendData(const QByteArray &data) { if (data.isEmpty()) { return; } if (pty()->write(data) == -1) { qCDebug(KonsoleDebug) << "Could not send input data to terminal process."; return; } } void Pty::dataReceived() { QByteArray data = pty()->readAll(); if (data.isEmpty()) { return; } emit receivedData(data.constData(), data.count()); } void Pty::setWindowSize(int columns, int lines) { _windowColumns = columns; _windowLines = lines; if (pty()->masterFd() >= 0) { pty()->setWinSize(lines, columns); } } QSize Pty::windowSize() const { return {_windowColumns, _windowLines}; } void Pty::setFlowControlEnabled(bool enable) { _xonXoff = enable; if (pty()->masterFd() >= 0) { struct ::termios ttmode; pty()->tcGetAttr(&ttmode); if (enable) { ttmode.c_iflag |= (IXOFF | IXON); } else { ttmode.c_iflag &= ~(IXOFF | IXON); } if (!pty()->tcSetAttr(&ttmode)) { qCDebug(KonsoleDebug) << "Unable to set terminal attributes."; } } } bool Pty::flowControlEnabled() const { if (pty()->masterFd() >= 0) { struct ::termios ttmode; pty()->tcGetAttr(&ttmode); return ((ttmode.c_iflag & IXOFF) != 0u) && ((ttmode.c_iflag & IXON) != 0u); } else { qCDebug(KonsoleDebug) << "Unable to get flow control status, terminal not connected."; return _xonXoff; } } void Pty::setUtf8Mode(bool enable) { #if defined(IUTF8) // XXX not a reasonable place to check it. _utf8 = enable; if (pty()->masterFd() >= 0) { struct ::termios ttmode; pty()->tcGetAttr(&ttmode); if (enable) { ttmode.c_iflag |= IUTF8; } else { ttmode.c_iflag &= ~IUTF8; } if (!pty()->tcSetAttr(&ttmode)) { qCDebug(KonsoleDebug) << "Unable to set terminal attributes."; } } #else Q_UNUSED(enable); #endif } void Pty::setEraseChar(char eChar) { _eraseChar = eChar; if (pty()->masterFd() >= 0) { struct ::termios ttmode; pty()->tcGetAttr(&ttmode); ttmode.c_cc[VERASE] = eChar; if (!pty()->tcSetAttr(&ttmode)) { qCDebug(KonsoleDebug) << "Unable to set terminal attributes."; } } } char Pty::eraseChar() const { if (pty()->masterFd() >= 0) { struct ::termios ttyAttributes; pty()->tcGetAttr(&ttyAttributes); return ttyAttributes.c_cc[VERASE]; } else { qCDebug(KonsoleDebug) << "Unable to get erase char attribute, terminal not connected."; return _eraseChar; } } void Pty::setInitialWorkingDirectory(const QString &dir) { QString pwd = dir; // remove trailing slash in the path when appropriate // example: /usr/share/icons/ ==> /usr/share/icons if (pwd.length() > 1 && pwd.endsWith(QLatin1Char('/'))) { pwd.chop(1); } setWorkingDirectory(pwd); // setting PWD to "." will cause problem for bash & zsh if (pwd != QLatin1String(".")) { setEnv(QStringLiteral("PWD"), pwd); } } void Pty::addEnvironmentVariables(const QStringList &environmentVariables) { bool isTermEnvAdded = false; - foreach (const QString &pair, environmentVariables) { + for (const QString &pair : environmentVariables) { // split on the first '=' character const int separator = pair.indexOf(QLatin1Char('=')); if (separator >= 0) { QString variable = pair.left(separator); QString value = pair.mid(separator + 1); setEnv(variable, value); if (variable == QLatin1String("TERM")) { isTermEnvAdded = true; } } } // extra safeguard to make sure $TERM is always set if (!isTermEnvAdded) { setEnv(QStringLiteral("TERM"), QStringLiteral("xterm-256color")); } } int Pty::start(const QString &programName, const QStringList &programArguments, const QStringList &environmentList) { clearProgram(); // For historical reasons, the first argument in programArguments is the // name of the program to execute, so create a list consisting of all // but the first argument to pass to setProgram() Q_ASSERT(programArguments.count() >= 1); setProgram(programName, programArguments.mid(1)); addEnvironmentVariables(environmentList); // unless the LANGUAGE environment variable has been set explicitly // set it to a null string // this fixes the problem where KCatalog sets the LANGUAGE environment // variable during the application's startup to something which // differs from LANG,LC_* etc. and causes programs run from // the terminal to display messages in the wrong language // // this can happen if LANG contains a language which KDE // does not have a translation for // // BR:149300 setEnv(QStringLiteral("LANGUAGE"), QString(), false /* do not overwrite existing value if any */); KProcess::start(); if (waitForStarted()) { return 0; } else { return -1; } } void Pty::setWriteable(bool writeable) { QT_STATBUF sbuf; if (QT_STAT(pty()->ttyName(), &sbuf) == 0) { if (writeable) { if (::chmod(pty()->ttyName(), sbuf.st_mode | S_IWGRP) < 0) { qCDebug(KonsoleDebug) << "Could not set writeable on "<ttyName(); } } else { if (::chmod(pty()->ttyName(), sbuf.st_mode & ~(S_IWGRP | S_IWOTH)) < 0) { qCDebug(KonsoleDebug) << "Could not unset writeable on "<ttyName(); } } } else { qCDebug(KonsoleDebug) << "Could not stat "<ttyName(); } } void Pty::closePty() { pty()->close(); } void Pty::sendEof() { if (pty()->masterFd() < 0) { qCDebug(KonsoleDebug) << "Unable to get eof char attribute, terminal not connected."; return; } struct ::termios ttyAttributes; pty()->tcGetAttr(&ttyAttributes); char eofChar = ttyAttributes.c_cc[VEOF]; if (pty()->write(QByteArray(1, eofChar)) == -1) { qCDebug(KonsoleDebug) << "Unable to send EOF"; } pty()->waitForBytesWritten(); } int Pty::foregroundProcessGroup() const { const int master_fd = pty()->masterFd(); if (master_fd >= 0) { int foregroundPid = tcgetpgrp(master_fd); if (foregroundPid != -1) { return foregroundPid; } } return 0; } void Pty::setupChildProcess() { KPtyProcess::setupChildProcess(); // reset all signal handlers // this ensures that terminal applications respond to // signals generated via key sequences such as Ctrl+C // (which sends SIGINT) struct sigaction action; sigemptyset(&action.sa_mask); action.sa_handler = SIG_DFL; action.sa_flags = 0; for (int signal = 1; signal < NSIG; signal++) { sigaction(signal, &action, nullptr); } } diff --git a/src/SaveHistoryTask.cpp b/src/SaveHistoryTask.cpp index 16ab661c..ecb3dd98 100644 --- a/src/SaveHistoryTask.cpp +++ b/src/SaveHistoryTask.cpp @@ -1,191 +1,192 @@ /* 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. */ #include "SaveHistoryTask.h" #include #include #include #include #include #include #include #include #include "SessionManager.h" #include "Emulation.h" namespace Konsole { QString SaveHistoryTask::_saveDialogRecentURL; 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()); dialog->setAcceptMode(QFileDialog::AcceptSave); QStringList mimeTypes { QStringLiteral("text/plain"), QStringLiteral("text/html") }; dialog->setMimeTypeFilters(mimeTypes); KSharedConfigPtr konsoleConfig = KSharedConfig::openConfig(); auto group = konsoleConfig->group("SaveHistory Settings"); if (_saveDialogRecentURL.isEmpty()) { const auto list = group.readPathEntry("Recent URLs", QStringList()); if (list.isEmpty()) { dialog->setDirectory(QDir::homePath()); } else { dialog->setDirectoryUrl(QUrl(list.at(0))); } } else { dialog->setDirectoryUrl(QUrl(_saveDialogRecentURL)); } // 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 auto& session, sessions()) { + const QList> sessionsList = sessions(); + for (const auto &session : sessionsList) { 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; } // Save selected URL for next time _saveDialogRecentURL = url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).toString(); group.writePathEntry("Recent URLs", _saveDialogRecentURL); 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))) { Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); jobInfo.decoder = new HTMLDecoder(profile); } 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.isNull()) { // 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(); } } } diff --git a/src/SessionController.cpp b/src/SessionController.cpp index 19f29fd5..a0ddc702 100644 --- a/src/SessionController.cpp +++ b/src/SessionController.cpp @@ -1,1808 +1,1812 @@ /* 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 #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" #include "SaveHistoryTask.h" #include "SearchHistoryTask.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, _bellIcon, (QIcon::fromTheme(QLatin1String("preferences-desktop-notification-bell")))) 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) , _sessionIcon(QIcon()) , _sessionIconName(QString()) , _previousState(-1) , _searchFilter(nullptr) , _urlFilter(nullptr) , _fileFilter(nullptr) , _copyInputToAllTabsAction(nullptr) , _findAction(nullptr) , _findNextAction(nullptr) , _findPreviousAction(nullptr) , _interactionTimer(nullptr) , _searchStartLine(0) , _prevSearchResultLine(0) , _codecAction(nullptr) , _switchProfileMenu(nullptr) , _webSearchMenu(nullptr) , _listenForScreenWindowUpdates(false) , _preventClose(false) , _keepIconUntilInteraction(false) , _selectedText(QString()) , _showMenuAction(nullptr) , _bookmarkValidProgramsToClear(QStringList()) , _isSearchBarEnabled(false) , _editProfileDialog(nullptr) , _searchBar(view->searchBar()) { 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()) { + + const QList actionsList = actionCollection()->actions(); + for (QAction *action : actionsList) { action->setShortcutContext(Qt::WidgetWithChildrenShortcut); } setIdentifier(++_lastControllerId); sessionAttributeChanged(); 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::sessionAttributeChanged, this, &Konsole::SessionController::sessionAttributeChanged); connect(_session.data(), &Konsole::Session::readOnlyChanged, this, &Konsole::SessionController::sessionReadOnlyChanged); connect(this, &Konsole::SessionController::tabRenamedByUser, _session, &Konsole::Session::tabTitleSetByUser); 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::focusGained, this, &Konsole::SessionController::interactionHandler); 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"); setupSearchBar(); _searchBar->setVisible(_isSearchBarEnabled); } SessionController::~SessionController() { _allControllers.remove(this); if (!_editProfileDialog.isNull()) { delete _editProfileDialog.data(); } if(factory()) { factory()->removeClient(this); } } void SessionController::trackOutput(QKeyEvent* event) { Q_ASSERT(_view->screenWindow()); // Qt has broken something, so we can't rely on just checking if certain // keys are passed as modifiers anymore. const int key = event->key(); const bool shouldNotTriggerScroll = key == Qt::Key_Super_L || key == Qt::Key_Super_R || key == Qt::Key_Hyper_L || key == Qt::Key_Hyper_R || key == Qt::Key_Shift || key == Qt::Key_Control || key == Qt::Key_Meta || key == Qt::Key_Alt || key == Qt::Key_AltGr || key == Qt::Key_CapsLock || key == Qt::Key_NumLock || key == Qt::Key_ScrollLock; // 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) || shouldNotTriggerScroll)) { 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.isNull()); 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) { + for (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() { auto * 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 auto signal = action->data().toInt(); _session->sendSignal(signal); } void SessionController::sendBackgroundColor() { const QColor c = _view->getBackgroundColor(); _session->reportBackgroundColor(c); } void SessionController::toggleReadOnly() { auto *action = qobject_cast(sender()); if (action != nullptr) { bool readonly = !isReadOnly(); _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::setupSearchBar() { 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); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::matchRegExpToggled, this, &Konsole::SessionController::changeSearchMatch); } 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())); action->setText(i18n("&Close Session")); 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); // History _findAction = KStandardAction::find(this, SLOT(searchBarEvent()), collection); _findNextAction = KStandardAction::findNext(this, SLOT(findNextInHistory()), collection); _findNextAction->setEnabled(false); _findPreviousAction = KStandardAction::findPrev(this, SLOT(findPreviousInHistory()), collection); _findPreviousAction->setEnabled(false); #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 // 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, QOverload::of(&KCodecAction::triggered), this, &Konsole::SessionController::changeCodec); // Read-only action = collection->addAction(QStringLiteral("view-readonly"), this, SLOT(toggleReadOnly())); action->setText(i18nc("@item:inmenu A read only (locked) session", "Read-only")); action->setCheckable(true); updateReadOnlyActionStates(); } 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 auto* 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 auto* 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 auto* 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 auto* copyInputActions = collection->add(QStringLiteral("copy-input-to")); copyInputActions->setText(i18n("Copy Input To")); copyInputActions->addAction(copyInputToAllTabsAction); copyInputActions->addAction(copyInputToSelectedTabsAction); copyInputActions->addAction(copyInputToNoneAction); connect(copyInputActions, QOverload::of(&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); action = collection->addAction(QStringLiteral("reset-font-size"), this, SLOT(resetFontSize())); action->setText(i18n("Reset Font Size")); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::ALT + Qt::Key_0); // Send signal auto* sendSignalActions = collection->add(QStringLiteral("send-signal")); sendSignalActions->setText(i18n("Send Signal")); connect(sendSignalActions, QOverload::of(&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); } void SessionController::switchProfile(const 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(); + for (SessionController *controller : qAsConst(_allControllers)) { + if ( (controller->profileDialogPointer() != nullptr) + && controller->profileDialogPointer()->isVisible() + && (controller->profileDialogPointer()->lookupProfile() + == SessionManager::instance()->sessionProfile(_session)) ) { + controller->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() { const QString &sessionLocalTabTitleFormat = _session->tabTitleFormat(Session::LocalTabTitle); const QString &sessionRemoteTabTitleFormat = _session->tabTitleFormat(Session::RemoteTabTitle); QScopedPointer dialog(new RenameTabDialog(QApplication::activeWindow())); dialog->setTabTitleText(sessionLocalTabTitleFormat); dialog->setRemoteTabTitleText(sessionRemoteTabTitleFormat); if (_session->isRemote()) { dialog->focusRemoteTabTitleText(); } else { dialog->focusTabTitleText(); } QPointer guard(_session); int result = dialog->exec(); if (guard.isNull()) { return; } if (result != 0) { const QString &tabTitle = dialog->tabTitleText(); const QString &remoteTabTitle = dialog->remoteTabTitleText(); if (tabTitle != sessionLocalTabTitleFormat) { _session->setTabTitleFormat(Session::LocalTabTitle, tabTitle); emit tabRenamedByUser(true); // trigger an update of the tab text snapshot(); } if(remoteTabTitle != sessionRemoteTabTitleFormat) { _session->setTabTitleFormat(Session::RemoteTabTitle, remoteTabTitle); emit tabRenamedByUser(true); snapshot(); } } } // This is called upon Menu->Close Sesssion and right-click on tab->Close Tab 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"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("CloseSingleTab")); 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() { _view->selectAll(); } 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 auto* 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()) { + const QList views = session->views(); + for (const TerminalDisplay *terminalDisplay : 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 auto mode = action->data().toInt(); 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.isNull()) { return; } if (result == QDialog::Accepted) { QSet newGroup = dialog->chosenSessions(); newGroup.remove(_session); - QSet completeGroup = newGroup | currentGroup; - foreach(Session * session, completeGroup) { + const QSet completeGroup = newGroup | currentGroup; + for (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(), QOverload<>::of(&Konsole::TerminalDisplay::update)); _listenForScreenWindowUpdates = true; } void SessionController::updateSearchFilter() { if ((_searchFilter != nullptr) && (!_searchBar.isNull())) { _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.isNull()) { 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.isNull()) && (_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.isNull()) { 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.isNull()) { _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 < 0 || _searchStartLine > _view->screenWindow()->lineCount()) { 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.isNull()) { 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(), QOverload<>::of(&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 _view->repaint(); } 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::resetFontSize() { _view->resetFontSize(); } void SessionController::monitorActivity(bool monitor) { _session->setMonitorActivity(monitor); } void SessionController::monitorSilence(bool monitor) { _session->setMonitorSilence(monitor); } void SessionController::updateSessionIcon() { // If the default profile icon is being used, don't put it on the tab // Only show the icon if the user specifically chose one if (_session->iconName() == QLatin1String("utilities-terminal")) { _sessionIconName = QString(); } else { _sessionIconName = _session->iconName(); } _sessionIcon = QIcon::fromTheme(_sessionIconName); // 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::updateReadOnlyActionStates() { bool readonly = isReadOnly(); QAction *readonlyAction = actionCollection()->action(QStringLiteral("view-readonly")); Q_ASSERT(readonlyAction != nullptr); readonlyAction->setIcon(QIcon::fromTheme(readonly ? QStringLiteral("object-locked") : QStringLiteral("object-unlocked"))); readonlyAction->setChecked(readonly); auto updateActionState = [this, readonly](const QString &name) { QAction *action = actionCollection()->action(name); if (action != nullptr) { action->setEnabled(!readonly); } }; updateActionState(QStringLiteral("edit_paste")); updateActionState(QStringLiteral("clear-history")); updateActionState(QStringLiteral("clear-history-and-reset")); updateActionState(QStringLiteral("edit-current-profile")); updateActionState(QStringLiteral("switch-profile")); updateActionState(QStringLiteral("adjust-history")); updateActionState(QStringLiteral("send-signal")); updateActionState(QStringLiteral("zmodem-upload")); _codecAction->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 { if (!_session.isNull()) { return _session->isReadOnly(); } else { return false; } } void SessionController::sessionAttributeChanged() { if (_sessionIconName != _session->iconName()) { 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::sessionReadOnlyChanged() { // Trigger icon update sessionAttributeChanged(); updateReadOnlyActionStates(); // Update all views - foreach (TerminalDisplay* view, session()->views()) { - if (view != _view.data()) { - view->updateReadOnlyState(isReadOnly()); + const QList viewsList = session()->views(); + for (TerminalDisplay *terminalDisplay : viewsList) { + if (terminalDisplay != _view.data()) { + terminalDisplay->updateReadOnlyState(isReadOnly()); } } } 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.isNull()) { updateReadOnlyActionStates(); // 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.isNull()) { 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 == NOTIFYBELL) { setIcon(*_bellIcon); _keepIconUntilInteraction = true; } else if (state == NOTIFYNORMAL) { 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(); } void SessionController::zmodemUpload() { if (_session->isZModemBusy()) { KMessageBox::sorry(_view, i18n("

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

")); return; } QString zmodem = QStandardPaths::findExecutable(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")); } QString SessionController::userTitle() const { if (!_session.isNull()) { return _session->userTitle(); } else { return QString(); } } diff --git a/src/SessionListModel.cpp b/src/SessionListModel.cpp index 75d55ca0..76cb221c 100644 --- a/src/SessionListModel.cpp +++ b/src/SessionListModel.cpp @@ -1,148 +1,148 @@ /* This source file is part of Konsole, a terminal emulator. Copyright 2006-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "SessionListModel.h" // KDE #include #include // Konsole #include "Session.h" using Konsole::Session; using Konsole::SessionListModel; SessionListModel::SessionListModel(QObject *parent) : QAbstractListModel(parent), _sessions(QList()) { } void SessionListModel::setSessions(const QList &sessions) { beginResetModel(); _sessions = sessions; - foreach (Session *session, sessions) { + for (Session *session : sessions) { connect(session, &Konsole::Session::finished, this, &Konsole::SessionListModel::sessionFinished); } endResetModel(); } QVariant SessionListModel::data(const QModelIndex &index, int role) const { Q_ASSERT(index.isValid()); int row = index.row(); int column = index.column(); Q_ASSERT(row >= 0 && row < _sessions.count()); Q_ASSERT(column >= 0 && column < 2); switch (role) { case Qt::DisplayRole: if (column == 1) { // This code is duplicated from SessionController.cpp QString title = _sessions[row]->title(Session::DisplayedTitleRole); // special handling for the "%w" marker which is replaced with the // window title set by the shell title.replace(QLatin1String("%w"), _sessions[row]->userTitle()); // special handling for the "%#" marker which is replaced with the // number of the shell title.replace(QLatin1String("%#"), QString::number(_sessions[row]->sessionId())); return title; } else if (column == 0) { return _sessions[row]->sessionId(); } break; // Due to the above 'column' constraints, this is never reached. case Qt::DecorationRole: if (column == 1) { return QIcon::fromTheme(_sessions[row]->iconName()); } else { return QVariant(); } } return QVariant(); } QVariant SessionListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) { return QVariant(); } if (orientation == Qt::Vertical) { return QVariant(); } else { switch (section) { case 0: return i18nc("@item:intable The session index", "Number"); case 1: return i18nc("@item:intable The session title", "Title"); default: return QVariant(); } } } int SessionListModel::columnCount(const QModelIndex &) const { return 2; } int SessionListModel::rowCount(const QModelIndex &) const { return _sessions.count(); } QModelIndex SessionListModel::parent(const QModelIndex &) const { return {}; } void SessionListModel::sessionFinished() { auto *session = qobject_cast(sender()); int row = _sessions.indexOf(session); if (row != -1) { beginRemoveRows(QModelIndex(), row, row); sessionRemoved(session); _sessions.removeAt(row); endRemoveRows(); } } QModelIndex SessionListModel::index(int row, int column, const QModelIndex &parent) const { if (hasIndex(row, column, parent)) { return createIndex(row, column, _sessions[row]); } else { return {}; } } diff --git a/src/SessionManager.cpp b/src/SessionManager.cpp index db89ceb6..310dcfb2 100644 --- a/src/SessionManager.cpp +++ b/src/SessionManager.cpp @@ -1,390 +1,390 @@ /* This source file is part of Konsole, a terminal emulator. Copyright 2006-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "SessionManager.h" #include "konsoledebug.h" // Qt #include #include // KDE #include #include // Konsole #include "Session.h" #include "ProfileManager.h" #include "History.h" #include "Enumeration.h" #include "TerminalDisplay.h" using namespace Konsole; SessionManager::SessionManager() : _sessions(QList()), _sessionProfiles(QHash()), _sessionRuntimeProfiles(QHash()), _restoreMapping(QHash()), _isClosingAllSessions(false) { ProfileManager *profileMananger = ProfileManager::instance(); connect(profileMananger, &Konsole::ProfileManager::profileChanged, this, &Konsole::SessionManager::profileChanged); } SessionManager::~SessionManager() { if (!_sessions.isEmpty()) { qCDebug(KonsoleDebug) << "Konsole SessionManager destroyed with" << _sessions.count() <<"session(s) still alive"; // ensure that the Session doesn't later try to call back and do things to the // SessionManager for (Session *session : qAsConst(_sessions)) { disconnect(session, nullptr, this, nullptr); } } } Q_GLOBAL_STATIC(SessionManager, theSessionManager) SessionManager* SessionManager::instance() { return theSessionManager; } bool SessionManager::isClosingAllSessions() const { return _isClosingAllSessions; } void SessionManager::closeAllSessions() { _isClosingAllSessions = true; for (Session *session : qAsConst(_sessions)) { session->close(); } _sessions.clear(); } const QList SessionManager::sessions() const { return _sessions; } Session *SessionManager::createSession(Profile::Ptr profile) { if (!profile) { profile = ProfileManager::instance()->defaultProfile(); } // TODO: check whether this is really needed if (!ProfileManager::instance()->loadedProfiles().contains(profile)) { ProfileManager::instance()->addProfile(profile); } //configuration information found, create a new session based on this auto session = new Session(); Q_ASSERT(session); applyProfile(session, profile, false); connect(session, &Konsole::Session::profileChangeCommandReceived, this, &Konsole::SessionManager::sessionProfileCommandReceived); //ask for notification when session dies connect(session, &Konsole::Session::finished, this, [this, session]() { sessionTerminated(session); }); //add session to active list _sessions << session; _sessionProfiles.insert(session, profile); return session; } void SessionManager::profileChanged(const Profile::Ptr &profile) { applyProfile(profile, true); } void SessionManager::sessionTerminated(Session *session) { Q_ASSERT(session); _sessions.removeAll(session); _sessionProfiles.remove(session); _sessionRuntimeProfiles.remove(session); session->deleteLater(); } void SessionManager::applyProfile(const Profile::Ptr &profile, bool modifiedPropertiesOnly) { for (Session *session : qAsConst(_sessions)) { if (_sessionProfiles[session] == profile) { applyProfile(session, profile, modifiedPropertiesOnly); } } } Profile::Ptr SessionManager::sessionProfile(Session *session) const { return _sessionProfiles[session]; } void SessionManager::setSessionProfile(Session *session, Profile::Ptr profile) { if (!profile) { profile = ProfileManager::instance()->defaultProfile(); } Q_ASSERT(profile); _sessionProfiles[session] = profile; applyProfile(session, profile, false); emit sessionUpdated(session); } void SessionManager::applyProfile(Session *session, const Profile::Ptr &profile, bool modifiedPropertiesOnly) { Q_ASSERT(profile); _sessionProfiles[session] = profile; ShouldApplyProperty apply(profile, modifiedPropertiesOnly); // Basic session settings if (apply.shouldApply(Profile::Name)) { session->setTitle(Session::NameRole, profile->name()); } if (apply.shouldApply(Profile::Command)) { session->setProgram(profile->command()); } if (apply.shouldApply(Profile::Arguments)) { session->setArguments(profile->arguments()); } if (apply.shouldApply(Profile::Directory)) { session->setInitialWorkingDirectory(profile->defaultWorkingDirectory()); } if (apply.shouldApply(Profile::Environment)) { // add environment variable containing home directory of current profile // (if specified) // prepend a 0 to the VERSION_MICRO part to make the version string // length consistent, so that conditions that depend on the exported // env var actually work // e.g. the second version should be higher than the first one: // 18.04.12 -> 180412 // 18.08.0 -> 180800 QStringList list = QStringLiteral(KONSOLE_VERSION).split(QLatin1Char('.')); if (list[2].length() < 2) { list[2].prepend(QLatin1String("0")); } const QString &numericVersion = list.join(QString()); QStringList environment = profile->environment(); environment << QStringLiteral("PROFILEHOME=%1").arg(profile->defaultWorkingDirectory()); environment << QStringLiteral("KONSOLE_VERSION=%1").arg(numericVersion); session->setEnvironment(environment); } if (apply.shouldApply(Profile::TerminalColumns) || apply.shouldApply(Profile::TerminalRows)) { const auto columns = profile->property(Profile::TerminalColumns); const auto rows = profile->property(Profile::TerminalRows); session->setPreferredSize(QSize(columns, rows)); } if (apply.shouldApply(Profile::Icon)) { session->setIconName(profile->icon()); } // Key bindings if (apply.shouldApply(Profile::KeyBindings)) { session->setKeyBindings(profile->keyBindings()); } // Tab formats // Preserve tab title changes, made by the user, when applying profile // changes or previewing color schemes if (apply.shouldApply(Profile::LocalTabTitleFormat) && !session->isTabTitleSetByUser()) { session->setTabTitleFormat(Session::LocalTabTitle, profile->localTabTitleFormat()); } if (apply.shouldApply(Profile::RemoteTabTitleFormat) && !session->isTabTitleSetByUser()) { session->setTabTitleFormat(Session::RemoteTabTitle, profile->remoteTabTitleFormat()); } // History if (apply.shouldApply(Profile::HistoryMode) || apply.shouldApply(Profile::HistorySize)) { const auto mode = profile->property(Profile::HistoryMode); switch (mode) { case Enum::NoHistory: session->setHistoryType(HistoryTypeNone()); break; case Enum::FixedSizeHistory: { int lines = profile->historySize(); session->setHistoryType(CompactHistoryType(lines)); break; } case Enum::UnlimitedHistory: session->setHistoryType(HistoryTypeFile()); break; } } // Terminal features if (apply.shouldApply(Profile::FlowControlEnabled)) { session->setFlowControlEnabled(profile->flowControlEnabled()); } // Encoding if (apply.shouldApply(Profile::DefaultEncoding)) { QByteArray name = profile->defaultEncoding().toUtf8(); session->setCodec(QTextCodec::codecForName(name)); } // Monitor Silence if (apply.shouldApply(Profile::SilenceSeconds)) { session->setMonitorSilenceSeconds(profile->silenceSeconds()); } } void SessionManager::sessionProfileCommandReceived(const QString &text) { auto *session = qobject_cast(sender()); Q_ASSERT(session); // store the font for each view if zoom was applied so that they can // be restored after applying the new profile QHash zoomFontSizes; - const QList views = session->views(); - for (TerminalDisplay *view : views) { + const QList viewsList = session->views(); + for (TerminalDisplay *view : viewsList) { const QFont &viewCurFont = view->getVTFont(); if (viewCurFont != _sessionProfiles[session]->font()) { zoomFontSizes.insert(view, viewCurFont); } } ProfileCommandParser parser; QHash changes = parser.parse(text); Profile::Ptr newProfile; if (!_sessionRuntimeProfiles.contains(session)) { newProfile = new Profile(_sessionProfiles[session]); _sessionRuntimeProfiles.insert(session, newProfile); } else { newProfile = _sessionRuntimeProfiles[session]; } QHashIterator iter(changes); while (iter.hasNext()) { iter.next(); newProfile->setProperty(iter.key(), iter.value()); } _sessionProfiles[session] = newProfile; applyProfile(newProfile, true); emit sessionUpdated(session); if (!zoomFontSizes.isEmpty()) { QHashIterator it(zoomFontSizes); while (it.hasNext()) { it.next(); it.key()->setVTFont(it.value()); } } } void SessionManager::saveSessions(KConfig *config) { // The session IDs can't be restored. // So we need to map the old ID to the future new ID. int n = 1; _restoreMapping.clear(); for (Session *session : qAsConst(_sessions)) { QString name = QLatin1String("Session") + QString::number(n); KConfigGroup group(config, name); group.writePathEntry("Profile", _sessionProfiles.value(session)->path()); session->saveSession(group); _restoreMapping.insert(session, n); n++; } KConfigGroup group(config, "Number"); group.writeEntry("NumberOfSessions", _sessions.count()); } int SessionManager::getRestoreId(Session *session) { return _restoreMapping.value(session); } void SessionManager::restoreSessions(KConfig *config) { KConfigGroup group(config, "Number"); const int sessions = group.readEntry("NumberOfSessions", 0); // Any sessions saved? for (int n = 1; n <= sessions; n++) { const QString name = QLatin1String("Session") + QString::number(n); KConfigGroup sessionGroup(config, name); const QString profile = sessionGroup.readPathEntry("Profile", QString()); Profile::Ptr ptr = ProfileManager::instance()->defaultProfile(); if (!profile.isEmpty()) { ptr = ProfileManager::instance()->loadProfile(profile); } Session *session = createSession(ptr); session->restoreSession(sessionGroup); } } Session *SessionManager::idToSession(int id) { for (Session *session : qAsConst(_sessions)) { if (session->sessionId() == id) { return session; } } // this should not happen qCDebug(KonsoleDebug) << "Failed to find session for ID" << id; return nullptr; } diff --git a/src/ShellCommand.cpp b/src/ShellCommand.cpp index e6a1b472..27d06f07 100644 --- a/src/ShellCommand.cpp +++ b/src/ShellCommand.cpp @@ -1,158 +1,158 @@ /* Copyright 2007-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "ShellCommand.h" // KDE #include using Konsole::ShellCommand; ShellCommand::ShellCommand(const QString &aCommand) { _arguments = KShell::splitArgs(aCommand); } ShellCommand::ShellCommand(const QString &aCommand, const QStringList &aArguments) { _arguments = aArguments; if (!_arguments.isEmpty()) { _arguments[0] = aCommand; } } QString ShellCommand::fullCommand() const { QStringList quotedArgs(_arguments); for (int i = 0; i < quotedArgs.count(); i++) { QString arg = quotedArgs.at(i); bool hasSpace = false; for (int j = 0; j < arg.count(); j++) { if (arg[j].isSpace()) { hasSpace = true; } } if (hasSpace) { quotedArgs[i] = QLatin1Char('\"') + arg + QLatin1Char('\"'); } } return quotedArgs.join(QLatin1Char(' ')); } QString ShellCommand::command() const { if (!_arguments.isEmpty()) { return _arguments[0]; } else { return QString(); } } QStringList ShellCommand::arguments() const { return _arguments; } QStringList ShellCommand::expand(const QStringList &items) { QStringList result; result.reserve(items.size()); - foreach (const QString &item, items) { + for (const QString &item : items) { result << expand(item); } return result; } QString ShellCommand::expand(const QString &text) { QString result = text; expandEnv(result); return result; } bool ShellCommand::isValidEnvCharacter(const QChar &ch) { const ushort code = ch.unicode(); return isValidLeadingEnvCharacter(ch) || ('0' <= code && code <= '9'); } bool ShellCommand::isValidLeadingEnvCharacter(const QChar &ch) { const ushort code = ch.unicode(); return (code == '_') || ('A' <= code && code <= 'Z'); } /* * expandEnv * * Expand environment variables in text. Escaped '$' characters are ignored. * Return true if any variables were expanded */ bool ShellCommand::expandEnv(QString &text) { const QLatin1Char dollarChar('$'); const QLatin1Char backslashChar('\\'); int dollarPos = 0; bool expanded = false; // find and expand all environment variables beginning with '$' while ((dollarPos = text.indexOf(dollarChar, dollarPos)) != -1) { // if '$' is the last character, there is no way of expanding if (dollarPos == text.length() - 1) { break; } // skip escaped '$' if (dollarPos > 0 && text.at(dollarPos - 1) == backslashChar) { dollarPos++; continue; } // if '$' is followed by an invalid leading character, skip this '$' if (!isValidLeadingEnvCharacter(text.at(dollarPos + 1))) { dollarPos++; continue; } int endPos = dollarPos + 1; Q_ASSERT(endPos < text.length()); while (endPos < text.length() && isValidEnvCharacter(text.at(endPos))) { endPos++; } const int len = endPos - dollarPos; const QString key = text.mid(dollarPos + 1, len - 1); const QString value = QString::fromLocal8Bit(qgetenv(key.toLocal8Bit().constData())); if (!value.isEmpty()) { text.replace(dollarPos, len, value); expanded = true; dollarPos = dollarPos + value.length(); } else { dollarPos = endPos; } } return expanded; } diff --git a/src/TerminalDisplay.cpp b/src/TerminalDisplay.cpp index 2f001370..cf9e0c0e 100644 --- a/src/TerminalDisplay.cpp +++ b/src/TerminalDisplay.cpp @@ -1,4024 +1,4024 @@ /* 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 "config-konsole.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include // Konsole #include "Filter.h" #include "konsoledebug.h" #include "TerminalCharacterDecoder.h" #include "Screen.h" #include "SessionController.h" #include "ExtendedCharTable.h" #include "TerminalDisplayAccessible.h" #include "SessionManager.h" #include "Session.h" #include "WindowSystemInfo.h" #include "IncrementalSearchBar.h" #include "Profile.h" #include "ViewManager.h" // for colorSchemeForProfile. // TODO: Rewrite this. #include "LineBlockCharacters.h" using namespace Konsole; #define REPCHAR "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "abcdefgjijklmnopqrstuvwxyz" \ "0123456789./+@" // we use this to force QPainter to display text in LTR mode // more information can be found in: https://unicode.org/reports/tr9/ const QChar LTR_OVERRIDE_CHAR(0x202D); inline int TerminalDisplay::loc(int x, int y) const { Q_ASSERT(y >= 0 && y < _lines); Q_ASSERT(x >= 0 && x < _columns); x = qBound(0, x, _columns - 1); y = qBound(0, y, _lines - 1); return y * _columns + x; } /* ------------------------------------------------------------------------- */ /* */ /* Colors */ /* */ /* ------------------------------------------------------------------------- */ /* Note that we use ANSI color order (bgr), while IBMPC color order is (rgb) Code 0 1 2 3 4 5 6 7 ----------- ------- ------- ------- ------- ------- ------- ------- ------- ANSI (bgr) Black Red Green Yellow Blue Magenta Cyan White IBMPC (rgb) Black Blue Green Cyan Red Magenta Yellow White */ ScreenWindow* TerminalDisplay::screenWindow() const { return _screenWindow; } void TerminalDisplay::setScreenWindow(ScreenWindow* window) { // disconnect existing screen window if any if (!_screenWindow.isNull()) { disconnect(_screenWindow , nullptr , this , nullptr); } _screenWindow = window; if (!_screenWindow.isNull()) { connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateLineProperties); connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateImage); connect(_screenWindow.data() , &Konsole::ScreenWindow::currentResultLineChanged , this , &Konsole::TerminalDisplay::updateImage); connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, [this]() { _filterUpdateRequired = true; }); connect(_screenWindow.data(), &Konsole::ScreenWindow::scrolled, this, [this]() { _filterUpdateRequired = true; }); _screenWindow->setWindowLines(_lines); } } const ColorEntry* TerminalDisplay::colorTable() const { return _colorTable; } void TerminalDisplay::onColorsChanged() { // Mostly just fix the scrollbar // this is a workaround to add some readability to old themes like Fusion // changing the light value for button a bit makes themes like fusion, windows and oxygen way more readable and pleasing QPalette p = QApplication::palette(); QColor buttonTextColor = _colorTable[DEFAULT_FORE_COLOR]; QColor backgroundColor = _colorTable[DEFAULT_BACK_COLOR]; backgroundColor.setAlphaF(_opacity); QColor buttonColor = backgroundColor.toHsv(); if (buttonColor.valueF() < 0.5) { buttonColor = buttonColor.lighter(); } else { buttonColor = buttonColor.darker(); } p.setColor(QPalette::Button, buttonColor); p.setColor(QPalette::Window, backgroundColor); p.setColor(QPalette::Base, backgroundColor); p.setColor(QPalette::WindowText, buttonTextColor); p.setColor(QPalette::ButtonText, buttonTextColor); setPalette(p); _scrollBar->setPalette(p); update(); } void TerminalDisplay::setBackgroundColor(const QColor& color) { _colorTable[DEFAULT_BACK_COLOR] = color; onColorsChanged(); } QColor TerminalDisplay::getBackgroundColor() const { return _colorTable[DEFAULT_BACK_COLOR]; } void TerminalDisplay::setForegroundColor(const QColor& color) { _colorTable[DEFAULT_FORE_COLOR] = color; onColorsChanged(); } void TerminalDisplay::setColorTable(const ColorEntry table[]) { for (int i = 0; i < TABLE_COLORS; i++) { _colorTable[i] = table[i]; } setBackgroundColor(_colorTable[DEFAULT_BACK_COLOR]); onColorsChanged(); } /* ------------------------------------------------------------------------- */ /* */ /* Font */ /* */ /* ------------------------------------------------------------------------- */ static inline bool isLineCharString(const QString& string) { if (string.length() == 0) { return false; } return LineBlockCharacters::canDraw(string.at(0).unicode()); } void TerminalDisplay::fontChange(const QFont&) { QFontMetrics fm(font()); _fontHeight = fm.height() + _lineSpacing; Q_ASSERT(_fontHeight > 0); /* TODO: When changing the three deprecated width() below * consider the info in * https://phabricator.kde.org/D23144 comments * horizontalAdvance() was added in Qt 5.11 (which should be the * minimum for 20.04 or 20.08 KDE Applications release) */ // waba TerminalDisplay 1.123: // "Base character width on widest ASCII character. This prevents too wide // characters in the presence of double wide (e.g. Japanese) characters." // Get the width from representative normal width characters _fontWidth = qRound((static_cast(fm.width(QStringLiteral(REPCHAR))) / static_cast(qstrlen(REPCHAR)))); _fixedFont = true; const int fw = fm.width(QLatin1Char(REPCHAR[0])); for (unsigned int i = 1; i < qstrlen(REPCHAR); i++) { if (fw != fm.width(QLatin1Char(REPCHAR[i]))) { _fixedFont = false; break; } } if (_fontWidth < 1) { _fontWidth = 1; } _fontAscent = fm.ascent(); emit changedFontMetricSignal(_fontHeight, _fontWidth); propagateSize(); update(); } void TerminalDisplay::setVTFont(const QFont& f) { QFont newFont(f); int strategy = 0; // hint that text should be drawn with- or without anti-aliasing. // depending on the user's font configuration, this may not be respected strategy |= _antialiasText ? QFont::PreferAntialias : QFont::NoAntialias; // Konsole cannot handle non-integer font metrics strategy |= QFont::ForceIntegerMetrics; // In case the provided font doesn't have some specific characters it should // fall back to a Monospace fonts. newFont.setStyleHint(QFont::TypeWriter, QFont::StyleStrategy(strategy)); // Try to check that a good font has been loaded. // For some fonts, ForceIntegerMetrics causes height() == 0 which // will cause Konsole to crash later. QFontMetrics fontMetrics2(newFont); if (fontMetrics2.height() < 1) { qCDebug(KonsoleDebug)<<"The font "<(fontInfo.styleHint()), fontInfo.weight(), static_cast(fontInfo.style()), static_cast(fontInfo.underline()), static_cast(fontInfo.strikeOut()), // Intentional newFont use - fixedPitch is bugged, see comment above static_cast(newFont.fixedPitch()), static_cast(fontInfo.rawMode())); qCDebug(KonsoleDebug) << "The font to use in the terminal can not be matched exactly on your system."; qCDebug(KonsoleDebug) << " Selected: " << newFont.toString(); qCDebug(KonsoleDebug) << " System : " << nonMatching; } QWidget::setFont(newFont); fontChange(newFont); } void TerminalDisplay::increaseFontSize() { QFont font = getVTFont(); font.setPointSizeF(font.pointSizeF() + 1); setVTFont(font); } void TerminalDisplay::decreaseFontSize() { const qreal MinimumFontSize = 6; QFont font = getVTFont(); font.setPointSizeF(qMax(font.pointSizeF() - 1, MinimumFontSize)); setVTFont(font); } void TerminalDisplay::resetFontSize() { const qreal MinimumFontSize = 6; QFont font = getVTFont(); Profile::Ptr currentProfile = SessionManager::instance()->sessionProfile(_sessionController->session()); const qreal defaultFontSize = currentProfile->font().pointSizeF(); font.setPointSizeF(qMax(defaultFontSize, MinimumFontSize)); setVTFont(font); } uint TerminalDisplay::lineSpacing() const { return _lineSpacing; } void TerminalDisplay::setLineSpacing(uint i) { _lineSpacing = i; fontChange(font()); // Trigger an update. } /* ------------------------------------------------------------------------- */ /* */ /* Accessibility */ /* */ /* ------------------------------------------------------------------------- */ namespace Konsole { #ifndef QT_NO_ACCESSIBILITY /** * This function installs the factory function which lets Qt instantiate the QAccessibleInterface * for the TerminalDisplay. */ QAccessibleInterface* accessibleInterfaceFactory(const QString &key, QObject *object) { Q_UNUSED(key) if (auto *display = qobject_cast(object)) { return new TerminalDisplayAccessible(display); } return nullptr; } #endif } /* ------------------------------------------------------------------------- */ /* */ /* Constructor / Destructor */ /* */ /* ------------------------------------------------------------------------- */ TerminalDisplay::TerminalDisplay(QWidget* parent) : QWidget(parent) , _screenWindow(nullptr) , _bellMasked(false) , _verticalLayout(new QVBoxLayout(this)) , _fixedFont(true) , _fontHeight(1) , _fontWidth(1) , _fontAscent(1) , _boldIntense(true) , _lines(1) , _columns(1) , _usedLines(1) , _usedColumns(1) , _contentRect(QRect()) , _image(nullptr) , _imageSize(0) , _lineProperties(QVector()) , _randomSeed(0) , _resizing(false) , _showTerminalSizeHint(true) , _bidiEnabled(false) , _usesMouseTracking(false) , _alternateScrolling(true) , _bracketedPasteMode(false) , _iPntSel(QPoint()) , _pntSel(QPoint()) , _tripleSelBegin(QPoint()) , _actSel(0) , _wordSelectionMode(false) , _lineSelectionMode(false) , _preserveLineBreaks(true) , _columnSelectionMode(false) , _autoCopySelectedText(false) , _copyTextAsHTML(true) , _middleClickPasteMode(Enum::PasteFromX11Selection) , _scrollBar(nullptr) , _scrollbarLocation(Enum::ScrollBarRight) , _scrollFullPage(false) , _wordCharacters(QStringLiteral(":@-./_~")) , _bellMode(Enum::NotifyBell) , _allowBlinkingText(true) , _allowBlinkingCursor(false) , _textBlinking(false) , _cursorBlinking(false) , _hasTextBlinker(false) , _urlHintsModifiers(Qt::NoModifier) , _showUrlHint(false) , _reverseUrlHints(false) , _openLinksByDirectClick(false) , _ctrlRequiredForDrag(true) , _dropUrlsAsText(false) , _tripleClickMode(Enum::SelectWholeLine) , _possibleTripleClick(false) , _resizeWidget(nullptr) , _resizeTimer(nullptr) , _flowControlWarningEnabled(false) , _outputSuspendedMessageWidget(nullptr) , _lineSpacing(0) , _size(QSize()) , _blendColor(qRgba(0, 0, 0, 0xff)) , _wallpaper(nullptr) , _filterChain(new TerminalImageFilterChain()) , _mouseOverHotspotArea(QRegion()) , _filterUpdateRequired(true) , _cursorShape(Enum::BlockCursor) , _cursorColor(QColor()) , _antialiasText(true) , _useFontLineCharacters(false) , _printerFriendly(false) , _sessionController(nullptr) , _trimLeadingSpaces(false) , _trimTrailingSpaces(false) , _mouseWheelZoom(false) , _margin(1) , _centerContents(false) , _readOnlyMessageWidget(nullptr) , _readOnly(false) , _opacity(1.0) , _dimWhenInactive(false) , _scrollWheelState(ScrollState()) , _searchBar(new IncrementalSearchBar(this)) , _headerBar(new TerminalHeaderBar(this)) , _searchResultRect(QRect()) , _drawOverlay(false) { // terminal applications are not designed with Right-To-Left in mind, // so the layout is forced to Left-To-Right setLayoutDirection(Qt::LeftToRight); _contentRect = QRect(_margin, _margin, 1, 1); // create scroll bar for scrolling output up and down _scrollBar = new QScrollBar(this); _scrollBar->setAutoFillBackground(false); // set the scroll bar's slider to occupy the whole area of the scroll bar initially setScroll(0, 0); _scrollBar->setCursor(Qt::ArrowCursor); _headerBar->setCursor(Qt::ArrowCursor); connect(_headerBar, &TerminalHeaderBar::requestToggleExpansion, this, &Konsole::TerminalDisplay::requestToggleExpansion); connect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged); connect(_scrollBar, &QScrollBar::sliderMoved, this, &Konsole::TerminalDisplay::viewScrolledByUser); // setup timers for blinking text _blinkTextTimer = new QTimer(this); _blinkTextTimer->setInterval(TEXT_BLINK_DELAY); connect(_blinkTextTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkTextEvent); // setup timers for blinking cursor _blinkCursorTimer = new QTimer(this); _blinkCursorTimer->setInterval(QApplication::cursorFlashTime() / 2); connect(_blinkCursorTimer, &QTimer::timeout, this, &Konsole::TerminalDisplay::blinkCursorEvent); // hide mouse cursor on keystroke or idle KCursor::setAutoHideCursor(this, true); setMouseTracking(true); setUsesMouseTracking(false); setBracketedPasteMode(false); setColorTable(ColorScheme::defaultTable); // Enable drag and drop support setAcceptDrops(true); _dragInfo.state = diNone; setFocusPolicy(Qt::WheelFocus); // enable input method support setAttribute(Qt::WA_InputMethodEnabled, true); // this is an important optimization, it tells Qt // that TerminalDisplay will handle repainting its entire area. setAttribute(Qt::WA_OpaquePaintEvent); // Add the stretch item once, the KMessageWidgets are inserted at index 0. _verticalLayout->addWidget(_headerBar); _verticalLayout->addStretch(); _verticalLayout->setSpacing(0); _verticalLayout->setContentsMargins(0, 0, 0, 0); setLayout(_verticalLayout); new AutoScrollHandler(this); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(Konsole::accessibleInterfaceFactory); #endif } TerminalDisplay::~TerminalDisplay() { disconnect(_blinkTextTimer); disconnect(_blinkCursorTimer); delete _readOnlyMessageWidget; delete _outputSuspendedMessageWidget; delete[] _image; delete _filterChain; _readOnlyMessageWidget = nullptr; _outputSuspendedMessageWidget = nullptr; } void TerminalDisplay::hideDragTarget() { _drawOverlay = false; update(); } void TerminalDisplay::showDragTarget(const QPoint& cursorPos) { using EdgeDistance = std::pair; auto closerToEdge = std::min( { {cursorPos.x(), Qt::LeftEdge}, {cursorPos.y(), Qt::TopEdge}, {width() - cursorPos.x(), Qt::RightEdge}, {height() - cursorPos.y(), Qt::BottomEdge} }, [](const EdgeDistance& left, const EdgeDistance& right) -> bool { return left.first < right.first; } ); if (_overlayEdge == closerToEdge.second) { return; } _overlayEdge = closerToEdge.second; _drawOverlay = true; update(); } /* ------------------------------------------------------------------------- */ /* */ /* Display Operations */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::drawLineCharString(QPainter& painter, int x, int y, const QString& str, const Character* attributes) { // only turn on anti-aliasing during this short time for the "text" // for the normal text we have TextAntialiasing on demand on // otherwise we have rendering artifacts // set https://bugreports.qt.io/browse/QTBUG-66036 painter.setRenderHint(QPainter::Antialiasing, _antialiasText); const bool useBoldPen = (attributes->rendition & RE_BOLD) != 0 && _boldIntense; QRect cellRect = {x, y, _fontWidth, _fontHeight}; for (int i = 0 ; i < str.length(); i++) { LineBlockCharacters::draw(painter, cellRect.translated(i * _fontWidth, 0), str[i], useBoldPen); } painter.setRenderHint(QPainter::Antialiasing, false); } void TerminalDisplay::setKeyboardCursorShape(Enum::CursorShapeEnum shape) { _cursorShape = shape; } Enum::CursorShapeEnum TerminalDisplay::keyboardCursorShape() const { return _cursorShape; } void TerminalDisplay::setCursorStyle(Enum::CursorShapeEnum shape, bool isBlinking) { setKeyboardCursorShape(shape); setBlinkingCursorEnabled(isBlinking); // when the cursor shape and blinking state are changed via the // Set Cursor Style (DECSCUSR) escape sequences in vim, and if the // cursor isn't set to blink, the cursor shape doesn't actually // change until the cursor is moved by the user; calling update() // makes the cursor shape get updated sooner. if (!isBlinking) { update(); } } void TerminalDisplay::resetCursorStyle() { Q_ASSERT(_sessionController != nullptr); Q_ASSERT(!_sessionController->session().isNull()); Profile::Ptr currentProfile = SessionManager::instance()->sessionProfile(_sessionController->session()); if (currentProfile != nullptr) { auto shape = static_cast(currentProfile->property(Profile::CursorShape)); setKeyboardCursorShape(shape); setBlinkingCursorEnabled(currentProfile->blinkingCursorEnabled()); } } void TerminalDisplay::setKeyboardCursorColor(const QColor& color) { _cursorColor = color; } QColor TerminalDisplay::keyboardCursorColor() const { return _cursorColor; } void TerminalDisplay::setOpacity(qreal opacity) { QColor color(_blendColor); color.setAlphaF(opacity); _opacity = opacity; _blendColor = color.rgba(); onColorsChanged(); } void TerminalDisplay::setWallpaper(const ColorSchemeWallpaper::Ptr &p) { _wallpaper = p; } void TerminalDisplay::drawBackground(QPainter& painter, const QRect& rect, const QColor& backgroundColor, bool useOpacitySetting) { // the area of the widget showing the contents of the terminal display is drawn // using the background color from the color scheme set with setColorTable() // // the area of the widget behind the scroll-bar is drawn using the background // brush from the scroll-bar's palette, to give the effect of the scroll-bar // being outside of the terminal display and visual consistency with other KDE // applications. if (useOpacitySetting && !_wallpaper->isNull() && _wallpaper->draw(painter, rect, _opacity)) { } else if (qAlpha(_blendColor) < 0xff && useOpacitySetting) { #if defined(Q_OS_MACOS) // TODO - On MacOS, using CompositionMode doesn't work. Altering the // transparency in the color scheme alters the brightness. painter.fillRect(rect, backgroundColor); #else QColor color(backgroundColor); color.setAlpha(qAlpha(_blendColor)); const QPainter::CompositionMode originalMode = painter.compositionMode(); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(rect, color); painter.setCompositionMode(originalMode); #endif } else { painter.fillRect(rect, backgroundColor); } } void TerminalDisplay::drawCursor(QPainter& painter, const QRect& rect, const QColor& foregroundColor, const QColor& /*backgroundColor*/, bool& invertCharacterColor) { // don't draw cursor which is currently blinking if (_cursorBlinking) { return; } // shift rectangle top down one pixel to leave some space // between top and bottom QRectF cursorRect = rect.adjusted(0, 1, 0, 0); QColor cursorColor = _cursorColor.isValid() ? _cursorColor : foregroundColor; painter.setPen(cursorColor); if (_cursorShape == Enum::BlockCursor) { // draw the cursor outline, adjusting the area so that // it is draw entirely inside 'rect' int penWidth = qMax(1, painter.pen().width()); painter.drawRect(cursorRect.adjusted(int(penWidth / 2) + 0.5, int(penWidth / 2) + 0.5, - int(penWidth / 2) - penWidth % 2 + 0.5, - int(penWidth / 2) - penWidth % 2 + 0.5)); // draw the cursor body only when the widget has focus if (hasFocus()) { painter.fillRect(cursorRect, cursorColor); if (!_cursorColor.isValid()) { // invert the color used to draw the text to ensure that the character at // the cursor position is readable invertCharacterColor = true; } } } else if (_cursorShape == Enum::UnderlineCursor) { QLineF line(cursorRect.left() + 0.5, cursorRect.bottom() - 0.5, cursorRect.right() - 0.5, cursorRect.bottom() - 0.5); painter.drawLine(line); } else if (_cursorShape == Enum::IBeamCursor) { QLineF line(cursorRect.left() + 0.5, cursorRect.top() + 0.5, cursorRect.left() + 0.5, cursorRect.bottom() - 0.5); painter.drawLine(line); } } void TerminalDisplay::drawCharacters(QPainter& painter, const QRect& rect, const QString& text, const Character* style, bool invertCharacterColor) { // don't draw text which is currently blinking if (_textBlinking && ((style->rendition & RE_BLINK) != 0)) { return; } // don't draw concealed characters if ((style->rendition & RE_CONCEAL) != 0) { return; } static constexpr int MaxFontWeight = 99; // https://doc.qt.io/qt-5/qfont.html#Weight-enum const int normalWeight = font().weight(); // +26 makes "bold" from "normal", "normal" from "light", etc. It is 26 instead of not 25 to prefer // bolder weight when 25 falls in the middle between two weights. See QFont::Weight const int boldWeight = qMin(normalWeight + 26, MaxFontWeight); const auto isBold = [boldWeight](const QFont &font) { return font.weight() >= boldWeight; }; const bool useBold = (((style->rendition & RE_BOLD) != 0) && _boldIntense); const bool useUnderline = ((style->rendition & RE_UNDERLINE) != 0) || font().underline(); const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || font().italic(); const bool useStrikeOut = ((style->rendition & RE_STRIKEOUT) != 0) || font().strikeOut(); const bool useOverline = ((style->rendition & RE_OVERLINE) != 0) || font().overline(); QFont currentFont = painter.font(); if (isBold(currentFont) != useBold || currentFont.underline() != useUnderline || currentFont.italic() != useItalic || currentFont.strikeOut() != useStrikeOut || currentFont.overline() != useOverline) { currentFont.setWeight(useBold ? boldWeight : normalWeight); currentFont.setUnderline(useUnderline); currentFont.setItalic(useItalic); currentFont.setStrikeOut(useStrikeOut); currentFont.setOverline(useOverline); painter.setFont(currentFont); } // setup pen const CharacterColor& textColor = (invertCharacterColor ? style->backgroundColor : style->foregroundColor); const QColor color = textColor.color(_colorTable); QPen pen = painter.pen(); if (pen.color() != color) { pen.setColor(color); painter.setPen(color); } const bool origClipping = painter.hasClipping(); const auto origClipRegion = painter.clipRegion(); painter.setClipRect(rect); // draw text if (isLineCharString(text) && !_useFontLineCharacters) { drawLineCharString(painter, rect.x(), rect.y(), text, style); } else { // Force using LTR as the document layout for the terminal area, because // there is no use cases for RTL emulator and RTL terminal application. // // This still allows RTL characters to be rendered in the RTL way. painter.setLayoutDirection(Qt::LeftToRight); if (_bidiEnabled) { painter.drawText(rect.x(), rect.y() + _fontAscent + _lineSpacing, text); } else { painter.drawText(rect.x(), rect.y() + _fontAscent + _lineSpacing, LTR_OVERRIDE_CHAR + text); } } painter.setClipRegion(origClipRegion); painter.setClipping(origClipping); } void TerminalDisplay::drawTextFragment(QPainter& painter , const QRect& rect, const QString& text, const Character* style) { // setup painter const QColor foregroundColor = style->foregroundColor.color(_colorTable); const QColor backgroundColor = style->backgroundColor.color(_colorTable); // draw background if different from the display's background color if (backgroundColor != getBackgroundColor()) { drawBackground(painter, rect, backgroundColor, false /* do not use transparency */); } // draw cursor shape if the current character is the cursor // this may alter the foreground and background colors bool invertCharacterColor = false; if ((style->rendition & RE_CURSOR) != 0) { drawCursor(painter, rect, foregroundColor, backgroundColor, invertCharacterColor); } // draw text drawCharacters(painter, rect, text, style, invertCharacterColor); } void TerminalDisplay::drawPrinterFriendlyTextFragment(QPainter& painter, const QRect& rect, const QString& text, const Character* style) { // Set the colors used to draw to black foreground and white // background for printer friendly output when printing Character print_style = *style; print_style.foregroundColor = CharacterColor(COLOR_SPACE_RGB, 0x00000000); print_style.backgroundColor = CharacterColor(COLOR_SPACE_RGB, 0xFFFFFFFF); // draw text drawCharacters(painter, rect, text, &print_style, false); } void TerminalDisplay::setRandomSeed(uint randomSeed) { _randomSeed = randomSeed; } uint TerminalDisplay::randomSeed() const { return _randomSeed; } // scrolls the image by 'lines', down if lines > 0 or up otherwise. // // the terminal emulation keeps track of the scrolling of the character // image as it receives input, and when the view is updated, it calls scrollImage() // with the final scroll amount. this improves performance because scrolling the // display is much cheaper than re-rendering all the text for the // part of the image which has moved up or down. // Instead only new lines have to be drawn void TerminalDisplay::scrollImage(int lines , const QRect& screenWindowRegion) { // return if there is nothing to do if ((lines == 0) || (_image == nullptr)) { return; } // if the flow control warning is enabled this will interfere with the // scrolling optimizations and cause artifacts. the simple solution here // is to just disable the optimization whilst it is visible if ((_outputSuspendedMessageWidget != nullptr) && _outputSuspendedMessageWidget->isVisible()) { return; } if ((_readOnlyMessageWidget != nullptr) && _readOnlyMessageWidget->isVisible()) { return; } // constrain the region to the display // the bottom of the region is capped to the number of lines in the display's // internal image - 2, so that the height of 'region' is strictly less // than the height of the internal image. QRect region = screenWindowRegion; region.setBottom(qMin(region.bottom(), _lines - 2)); // return if there is nothing to do if (!region.isValid() || (region.top() + abs(lines)) >= region.bottom() || _lines <= region.height()) { return; } // hide terminal size label to prevent it being scrolled if ((_resizeWidget != nullptr) && _resizeWidget->isVisible()) { _resizeWidget->hide(); } // Note: With Qt 4.4 the left edge of the scrolled area must be at 0 // to get the correct (newly exposed) part of the widget repainted. // // The right edge must be before the left edge of the scroll bar to // avoid triggering a repaint of the entire widget, the distance is // given by SCROLLBAR_CONTENT_GAP // // Set the QT_FLUSH_PAINT environment variable to '1' before starting the // application to monitor repainting. // const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->width(); const int SCROLLBAR_CONTENT_GAP = 1; QRect scrollRect; if (_scrollbarLocation == Enum::ScrollBarLeft) { scrollRect.setLeft(scrollBarWidth + SCROLLBAR_CONTENT_GAP); scrollRect.setRight(width()); } else { scrollRect.setLeft(0); scrollRect.setRight(width() - scrollBarWidth - SCROLLBAR_CONTENT_GAP); } void* firstCharPos = &_image[ region.top() * _columns ]; void* lastCharPos = &_image[(region.top() + abs(lines)) * _columns ]; const int top = _contentRect.top() + (region.top() * _fontHeight); const int linesToMove = region.height() - abs(lines); const int bytesToMove = linesToMove * _columns * sizeof(Character); Q_ASSERT(linesToMove > 0); Q_ASSERT(bytesToMove > 0); scrollRect.setTop( lines > 0 ? top : top + abs(lines) * _fontHeight); scrollRect.setHeight(linesToMove * _fontHeight); if (!scrollRect.isValid() || scrollRect.isEmpty()) { return; } //scroll internal image if (lines > 0) { // check that the memory areas that we are going to move are valid Q_ASSERT((char*)lastCharPos + bytesToMove < (char*)(_image + (_lines * _columns))); Q_ASSERT((lines * _columns) < _imageSize); //scroll internal image down memmove(firstCharPos , lastCharPos , bytesToMove); } else { // check that the memory areas that we are going to move are valid Q_ASSERT((char*)firstCharPos + bytesToMove < (char*)(_image + (_lines * _columns))); //scroll internal image up memmove(lastCharPos , firstCharPos , bytesToMove); } //scroll the display vertically to match internal _image scroll(0 , _fontHeight * (-lines) , scrollRect); } QRegion TerminalDisplay::hotSpotRegion() const { QRegion region; for (const Filter::HotSpot *hotSpot : _filterChain->hotSpots()) { QRect r; r.setLeft(hotSpot->startColumn()); r.setTop(hotSpot->startLine()); if (hotSpot->startLine() == hotSpot->endLine()) { r.setRight(hotSpot->endColumn()); r.setBottom(hotSpot->endLine()); region |= imageToWidget(r); } else { r.setRight(_columns); r.setBottom(hotSpot->startLine()); region |= imageToWidget(r); r.setLeft(0); for (int line = hotSpot->startLine() + 1 ; line < hotSpot->endLine(); line++) { r.moveTop(line); region |= imageToWidget(r); } r.moveTop(hotSpot->endLine()); r.setRight(hotSpot->endColumn()); region |= imageToWidget(r); } } return region; } void TerminalDisplay::processFilters() { if (_screenWindow.isNull()) { return; } if (!_filterUpdateRequired) { return; } QRegion preUpdateHotSpots = hotSpotRegion(); // use _screenWindow->getImage() here rather than _image because // other classes may call processFilters() when this display's // ScreenWindow emits a scrolled() signal - which will happen before // updateImage() is called on the display and therefore _image is // out of date at this point _filterChain->setImage(_screenWindow->getImage(), _screenWindow->windowLines(), _screenWindow->windowColumns(), _screenWindow->getLineProperties()); _filterChain->process(); QRegion postUpdateHotSpots = hotSpotRegion(); update(preUpdateHotSpots | postUpdateHotSpots); _filterUpdateRequired = false; } void TerminalDisplay::updateImage() { if (_screenWindow.isNull()) { return; } // optimization - scroll the existing image where possible and // avoid expensive text drawing for parts of the image that // can simply be moved up or down // disable this shortcut for transparent konsole with scaled pixels, otherwise we get rendering artifacts, see BUG 350651 if (!(WindowSystemInfo::HAVE_TRANSPARENCY && (qApp->devicePixelRatio() > 1.0)) && _wallpaper->isNull() && !_searchBar->isVisible()) { scrollImage(_screenWindow->scrollCount() , _screenWindow->scrollRegion()); } if (_image == nullptr) { // Create _image. // The emitted changedContentSizeSignal also leads to getImage being recreated, so do this first. updateImageSize(); } Character* const newimg = _screenWindow->getImage(); const int lines = _screenWindow->windowLines(); const int columns = _screenWindow->windowColumns(); setScroll(_screenWindow->currentLine() , _screenWindow->lineCount()); Q_ASSERT(_usedLines <= _lines); Q_ASSERT(_usedColumns <= _columns); int y, x, len; const QPoint tL = contentsRect().topLeft(); const int tLx = tL.x(); const int tLy = tL.y(); _hasTextBlinker = false; CharacterColor cf; // undefined const int linesToUpdate = qMin(_lines, qMax(0, lines)); const int columnsToUpdate = qMin(_columns, qMax(0, columns)); auto dirtyMask = new char[columnsToUpdate + 2]; QRegion dirtyRegion; // debugging variable, this records the number of lines that are found to // be 'dirty' ( ie. have changed from the old _image to the new _image ) and // which therefore need to be repainted int dirtyLineCount = 0; for (y = 0; y < linesToUpdate; ++y) { const Character* currentLine = &_image[y * _columns]; const Character* const newLine = &newimg[y * columns]; bool updateLine = false; // The dirty mask indicates which characters need repainting. We also // mark surrounding neighbors dirty, in case the character exceeds // its cell boundaries memset(dirtyMask, 0, columnsToUpdate + 2); for (x = 0 ; x < columnsToUpdate ; ++x) { if (newLine[x] != currentLine[x]) { dirtyMask[x] = 1; } } if (!_resizing) { // not while _resizing, we're expecting a paintEvent for (x = 0; x < columnsToUpdate; ++x) { _hasTextBlinker |= (newLine[x].rendition & RE_BLINK); // Start drawing if this character or the next one differs. // We also take the next one into account to handle the situation // where characters exceed their cell width. if (dirtyMask[x] != 0) { if (newLine[x + 0].character == 0u) { continue; } const bool lineDraw = LineBlockCharacters::canDraw(newLine[x + 0].character); const bool doubleWidth = (x + 1 == columnsToUpdate) ? false : (newLine[x + 1].character == 0); const RenditionFlags cr = newLine[x].rendition; const CharacterColor clipboard = newLine[x].backgroundColor; if (newLine[x].foregroundColor != cf) { cf = newLine[x].foregroundColor; } const int lln = columnsToUpdate - x; for (len = 1; len < lln; ++len) { const Character& ch = newLine[x + len]; if (ch.character == 0u) { continue; // Skip trailing part of multi-col chars. } const bool nextIsDoubleWidth = (x + len + 1 == columnsToUpdate) ? false : (newLine[x + len + 1].character == 0); if (ch.foregroundColor != cf || ch.backgroundColor != clipboard || (ch.rendition & ~RE_EXTENDED_CHAR) != (cr & ~RE_EXTENDED_CHAR) || (dirtyMask[x + len] == 0) || LineBlockCharacters::canDraw(ch.character) != lineDraw || nextIsDoubleWidth != doubleWidth) { break; } } const bool saveFixedFont = _fixedFont; if (lineDraw) { _fixedFont = false; } if (doubleWidth) { _fixedFont = false; } updateLine = true; _fixedFont = saveFixedFont; x += len - 1; } } } //both the top and bottom halves of double height _lines must always be redrawn //although both top and bottom halves contain the same characters, only //the top one is actually //drawn. if (_lineProperties.count() > y) { updateLine |= (_lineProperties[y] & LINE_DOUBLEHEIGHT); } // if the characters on the line are different in the old and the new _image // then this line must be repainted. if (updateLine) { dirtyLineCount++; // add the area occupied by this line to the region which needs to be // repainted QRect dirtyRect = QRect(_contentRect.left() + tLx , _contentRect.top() + tLy + _fontHeight * y , _fontWidth * columnsToUpdate , _fontHeight); dirtyRegion |= dirtyRect; } // replace the line of characters in the old _image with the // current line of the new _image memcpy((void*)currentLine, (const void*)newLine, columnsToUpdate * sizeof(Character)); } // if the new _image is smaller than the previous _image, then ensure that the area // outside the new _image is cleared if (linesToUpdate < _usedLines) { dirtyRegion |= QRect(_contentRect.left() + tLx , _contentRect.top() + tLy + _fontHeight * linesToUpdate , _fontWidth * _columns , _fontHeight * (_usedLines - linesToUpdate)); } _usedLines = linesToUpdate; if (columnsToUpdate < _usedColumns) { dirtyRegion |= QRect(_contentRect.left() + tLx + columnsToUpdate * _fontWidth , _contentRect.top() + tLy , _fontWidth * (_usedColumns - columnsToUpdate) , _fontHeight * _lines); } _usedColumns = columnsToUpdate; dirtyRegion |= _inputMethodData.previousPreeditRect; if ((_screenWindow->currentResultLine() != -1) && (_screenWindow->scrollCount() != 0)) { // De-highlight previous result region dirtyRegion |= _searchResultRect; // Highlight new result region dirtyRegion |= QRect(0, _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _fontHeight, _columns * _fontWidth, _fontHeight); } _screenWindow->resetScrollCount(); // update the parts of the display which have changed update(dirtyRegion); if (_allowBlinkingText && _hasTextBlinker && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!_hasTextBlinker && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } delete[] dirtyMask; #ifndef QT_NO_ACCESSIBILITY QAccessibleEvent dataChangeEvent(this, QAccessible::VisibleDataChanged); QAccessible::updateAccessibility(&dataChangeEvent); QAccessibleTextCursorEvent cursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX()); QAccessible::updateAccessibility(&cursorEvent); #endif } void TerminalDisplay::showResizeNotification() { if (_showTerminalSizeHint && isVisible()) { if (_resizeWidget == nullptr) { _resizeWidget = new QLabel(i18n("Size: XXX x XXX"), this); _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().boundingRect(i18n("Size: XXX x XXX")).width()); _resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height()); _resizeWidget->setAlignment(Qt::AlignCenter); _resizeWidget->setStyleSheet(QStringLiteral("background-color:palette(window);border-style:solid;border-width:1px;border-color:palette(dark)")); _resizeTimer = new QTimer(this); _resizeTimer->setInterval(SIZE_HINT_DURATION); _resizeTimer->setSingleShot(true); connect(_resizeTimer, &QTimer::timeout, _resizeWidget, &QLabel::hide); } QString sizeStr = i18n("Size: %1 x %2", _columns, _lines); _resizeWidget->setText(sizeStr); _resizeWidget->move((width() - _resizeWidget->width()) / 2, (height() - _resizeWidget->height()) / 2 + 20); _resizeWidget->show(); _resizeTimer->start(); } } void TerminalDisplay::paintEvent(QPaintEvent* pe) { QPainter paint(this); // Determine which characters should be repainted (1 region unit = 1 character) QRegion dirtyImageRegion; const QRegion region = pe->region() & contentsRect(); for (const QRect &rect : region) { dirtyImageRegion += widgetToImage(rect); drawBackground(paint, rect, getBackgroundColor(), true /* use opacity setting */); } // only turn on text anti-aliasing, never turn on normal antialiasing // set https://bugreports.qt.io/browse/QTBUG-66036 paint.setRenderHint(QPainter::TextAntialiasing, _antialiasText); for (const QRect &rect : qAsConst(dirtyImageRegion)) { drawContents(paint, rect); } drawCurrentResultRect(paint); drawInputMethodPreeditString(paint, preeditRect()); paintFilters(paint); const bool drawDimmed = _dimWhenInactive && !hasFocus(); const QColor dimColor(0, 0, 0, 128); for (const QRect &rect : region) { if (drawDimmed) { paint.fillRect(rect, dimColor); } } if (_drawOverlay) { const auto y = _headerBar->isVisible() ? _headerBar->height() : 0; const auto rect = _overlayEdge == Qt::LeftEdge ? QRect(0, y, width() / 2, height()) : _overlayEdge == Qt::TopEdge ? QRect(0, y, width(), height() / 2) : _overlayEdge == Qt::RightEdge ? QRect(width() - width() / 2, y, width() / 2, height()) : QRect(0, height() - height() / 2, width(), height() / 2); paint.setRenderHint(QPainter::Antialiasing); paint.setPen(Qt::NoPen); paint.setBrush(QColor(100,100,100, 127)); paint.drawRect(rect); } } void TerminalDisplay::printContent(QPainter& painter, bool friendly) { // Reinitialize the font with the printers paint device so the font // measurement calculations will be done correctly QFont savedFont = getVTFont(); QFont font(savedFont, painter.device()); painter.setFont(font); setVTFont(font); QRect rect(0, 0, _usedColumns, _usedLines); _printerFriendly = friendly; if (!friendly) { drawBackground(painter, rect, getBackgroundColor(), true /* use opacity setting */); } drawContents(painter, rect); _printerFriendly = false; setVTFont(savedFont); } QPoint TerminalDisplay::cursorPosition() const { if (!_screenWindow.isNull()) { return _screenWindow->cursorPosition(); } else { return {0, 0}; } } inline bool TerminalDisplay::isCursorOnDisplay() const { return cursorPosition().x() < _columns && cursorPosition().y() < _lines; } FilterChain* TerminalDisplay::filterChain() const { return _filterChain; } void TerminalDisplay::paintFilters(QPainter& painter) { if (_filterUpdateRequired) { return; } // get color of character under mouse and use it to draw // lines for filters QPoint cursorPos = mapFromGlobal(QCursor::pos()); int cursorLine; int cursorColumn; getCharacterPosition(cursorPos, cursorLine, cursorColumn, false); Character cursorCharacter = _image[loc(qMin(cursorColumn, _columns - 1), cursorLine)]; painter.setPen(QPen(cursorCharacter.foregroundColor.color(_colorTable))); // iterate over hotspots identified by the display's currently active filters // and draw appropriate visuals to indicate the presence of the hotspot - QList spots = _filterChain->hotSpots(); + const QList spots = _filterChain->hotSpots(); int urlNumber, urlNumInc; if (_reverseUrlHints) { urlNumber = spots.size() + 1; urlNumInc = -1; } else { urlNumber = 0; urlNumInc = 1; } - foreach(Filter::HotSpot* spot, spots) { + for (const Filter::HotSpot *spot : spots) { urlNumber += urlNumInc; QRegion region; if (spot->type() == Filter::HotSpot::Link) { QRect r; if (spot->startLine() == spot->endLine()) { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } else { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) { r.setCoords(0 * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } r.setCoords(0 * _fontWidth + _contentRect.left(), spot->endLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } if (_showUrlHint && urlNumber < 10) { // Position at the beginning of the URL QRect hintRect(*region.begin()); hintRect.setWidth(r.height()); painter.fillRect(hintRect, QColor(0, 0, 0, 128)); painter.setPen(Qt::white); painter.drawRect(hintRect.adjusted(0, 0, -1, -1)); painter.drawText(hintRect, Qt::AlignCenter, QString::number(urlNumber)); } } for (int line = spot->startLine() ; line <= spot->endLine() ; line++) { int startColumn = 0; int endColumn = _columns - 1; // TODO use number of _columns which are actually // occupied on this line rather than the width of the // display in _columns // Check image size so _image[] is valid (see makeImage) if (endColumn >= _columns || line >= _lines) { break; } // ignore whitespace at the end of the lines while (_image[loc(endColumn, line)].isSpace() && endColumn > 0) { endColumn--; } // increment here because the column which we want to set 'endColumn' to // is the first whitespace character at the end of the line endColumn++; if (line == spot->startLine()) { startColumn = spot->startColumn(); } if (line == spot->endLine()) { endColumn = spot->endColumn(); } // TODO: resolve this comment with the new margin/center code // subtract one pixel from // the right and bottom so that // we do not overdraw adjacent // hotspots // // subtracting one pixel from all sides also prevents an edge case where // moving the mouse outside a link could still leave it underlined // because the check below for the position of the cursor // finds it on the border of the target area QRect r; r.setCoords(startColumn * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), endColumn * _fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); // Underline link hotspots if (spot->type() == Filter::HotSpot::Link) { QFontMetrics metrics(font()); // find the baseline (which is the invisible line that the characters in the font sit on, // with some having tails dangling below) const int baseline = r.bottom() - metrics.descent(); // find the position of the underline below that const int underlinePos = baseline + metrics.underlinePos(); if (_showUrlHint || region.contains(mapFromGlobal(QCursor::pos()))) { painter.drawLine(r.left() , underlinePos , r.right() , underlinePos); } // Marker hotspots simply have a transparent rectangular shape // drawn on top of them } else if (spot->type() == Filter::HotSpot::Marker) { //TODO - Do not use a hardcoded color for this const bool isCurrentResultLine = (_screenWindow->currentResultLine() == (spot->startLine() + _screenWindow->currentLine())); QColor color = isCurrentResultLine ? QColor(255, 255, 0, 120) : QColor(255, 0, 0, 120); painter.fillRect(r, color); } } } } inline static bool isRtl(const Character &chr) { uint c = 0; if ((chr.rendition & RE_EXTENDED_CHAR) == 0) { c = chr.character; } else { ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(chr.character, extendedCharLength); if (chars != nullptr) { c = chars[0]; } } switch(QChar::direction(c)) { case QChar::DirR: case QChar::DirAL: case QChar::DirRLE: case QChar::DirRLI: case QChar::DirRLO: return true; default: return false; } } void TerminalDisplay::drawContents(QPainter& paint, const QRect& rect) { const int numberOfColumns = _usedColumns; QVector univec; univec.reserve(numberOfColumns); for (int y = rect.y(); y <= rect.bottom(); y++) { int x = rect.x(); if ((_image[loc(rect.x(), y)].character == 0u) && (x != 0)) { x--; // Search for start of multi-column character } for (; x <= rect.right(); x++) { int len = 1; int p = 0; // reset our buffer to the number of columns int bufferSize = numberOfColumns; univec.resize(bufferSize); uint *disstrU = univec.data(); // is this a single character or a sequence of characters ? if ((_image[loc(x, y)].rendition & RE_EXTENDED_CHAR) != 0) { // sequence of characters ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(_image[loc(x, y)].character, extendedCharLength); if (chars != nullptr) { Q_ASSERT(extendedCharLength > 1); bufferSize += extendedCharLength - 1; univec.resize(bufferSize); disstrU = univec.data(); for (int index = 0 ; index < extendedCharLength ; index++) { Q_ASSERT(p < bufferSize); disstrU[p++] = chars[index]; } } } else { // single character const uint c = _image[loc(x, y)].character; if (c != 0u) { Q_ASSERT(p < bufferSize); disstrU[p++] = c; } } const bool lineDraw = LineBlockCharacters::canDraw(_image[loc(x, y)].character); const bool doubleWidth = (_image[qMin(loc(x, y) + 1, _imageSize - 1)].character == 0); const CharacterColor currentForeground = _image[loc(x, y)].foregroundColor; const CharacterColor currentBackground = _image[loc(x, y)].backgroundColor; const RenditionFlags currentRendition = _image[loc(x, y)].rendition; const bool rtl = isRtl(_image[loc(x, y)]); const auto isInsideDrawArea = [&](int column) { return column <= rect.right(); }; const auto hasSameColors = [&](int column) { return _image[loc(column, y)].foregroundColor == currentForeground && _image[loc(column, y)].backgroundColor == currentBackground; }; const auto hasSameRendition = [&](int column) { return (_image[loc(column, y)].rendition & ~RE_EXTENDED_CHAR) == (currentRendition & ~RE_EXTENDED_CHAR); }; const auto hasSameWidth = [&](int column) { const int characterLoc = qMin(loc(column, y) + 1, _imageSize - 1); return (_image[characterLoc].character == 0) == doubleWidth; }; const auto canBeGrouped = [&](int column) { return _image[loc(column, y)].character <= 0x7e || rtl; }; if (canBeGrouped(x)) { while (isInsideDrawArea(x + len) && hasSameColors(x + len) && hasSameRendition(x + len) && hasSameWidth(x + len) && canBeGrouped(x + len)) { const uint c = _image[loc(x + len, y)].character; if ((_image[loc(x + len, y)].rendition & RE_EXTENDED_CHAR) != 0) { // sequence of characters ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(c, extendedCharLength); if (chars != nullptr) { Q_ASSERT(extendedCharLength > 1); bufferSize += extendedCharLength - 1; univec.resize(bufferSize); disstrU = univec.data(); for (int index = 0 ; index < extendedCharLength ; index++) { Q_ASSERT(p < bufferSize); disstrU[p++] = chars[index]; } } } else { // single character if (c != 0u) { Q_ASSERT(p < bufferSize); disstrU[p++] = c; } } if (doubleWidth) { // assert((_image[loc(x+len,y)+1].character == 0)), see above if condition len++; // Skip trailing part of multi-column character } len++; } } else { // Group spaces following any non-wide character with the character. This allows for // rendering ambiguous characters with wide glyphs without clipping them. while (!doubleWidth && isInsideDrawArea(x + len) && _image[loc(x + len, y)].character == ' ' && hasSameColors(x + len) && hasSameRendition(x + len)) { // disstrU intentionally not modified - trailing spaces are meaningless len++; } } if ((x + len < _usedColumns) && (_image[loc(x + len, y)].character == 0u)) { len++; // Adjust for trailing part of multi-column character } const bool save__fixedFont = _fixedFont; if (lineDraw) { _fixedFont = false; } if (doubleWidth) { _fixedFont = false; } univec.resize(p); // Create a text scaling matrix for double width and double height lines. QMatrix textScale; if (y < _lineProperties.size()) { if ((_lineProperties[y] & LINE_DOUBLEWIDTH) != 0) { textScale.scale(2, 1); } if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) { textScale.scale(1, 2); } } //Apply text scaling matrix. // TODO: setWorldMatrix is obsolete, change to setWorldTransform paint.setWorldMatrix(textScale, true); //calculate the area in which the text will be drawn QRect textArea = QRect(_contentRect.left() + contentsRect().left() + _fontWidth * x, _contentRect.top() + contentsRect().top() + _fontHeight * y, _fontWidth * len, _fontHeight); //move the calculated area to take account of scaling applied to the painter. //the position of the area from the origin (0,0) is scaled //by the opposite of whatever //transformation has been applied to the painter. this ensures that //painting does actually start from textArea.topLeft() //(instead of textArea.topLeft() * painter-scale) textArea.moveTopLeft(textScale.inverted().map(textArea.topLeft())); QString unistr = QString::fromUcs4(univec.data(), univec.length()); //paint text fragment if (_printerFriendly) { drawPrinterFriendlyTextFragment(paint, textArea, unistr, &_image[loc(x, y)]); } else { drawTextFragment(paint, textArea, unistr, &_image[loc(x, y)]); } _fixedFont = save__fixedFont; //reset back to single-width, single-height _lines // TODO: setWorldMatrix is obsolete, change to setWorldTransform paint.setWorldMatrix(textScale.inverted(), true); if (y < _lineProperties.size() - 1) { //double-height _lines are represented by two adjacent _lines //containing the same characters //both _lines will have the LINE_DOUBLEHEIGHT attribute. //If the current line has the LINE_DOUBLEHEIGHT attribute, //we can therefore skip the next line if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) { y++; } } x += len - 1; } } } void TerminalDisplay::drawCurrentResultRect(QPainter& painter) { if(_screenWindow->currentResultLine() == -1) { return; } _searchResultRect.setRect(0, _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _fontHeight, _columns * _fontWidth, _fontHeight); painter.fillRect(_searchResultRect, QColor(0, 0, 255, 80)); } QRect TerminalDisplay::imageToWidget(const QRect& imageArea) const { QRect result; result.setLeft(_contentRect.left() + _fontWidth * imageArea.left()); result.setTop(_contentRect.top() + _fontHeight * imageArea.top()); result.setWidth(_fontWidth * imageArea.width()); result.setHeight(_fontHeight * imageArea.height()); return result; } QRect TerminalDisplay::widgetToImage(const QRect &widgetArea) const { QRect result; result.setLeft( qMin(_usedColumns - 1, qMax(0, (widgetArea.left() - contentsRect().left() - _contentRect.left()) / _fontWidth ))); result.setTop( qMin(_usedLines - 1, qMax(0, (widgetArea.top() - contentsRect().top() - _contentRect.top() ) / _fontHeight))); result.setRight( qMin(_usedColumns - 1, qMax(0, (widgetArea.right() - contentsRect().left() - _contentRect.left()) / _fontWidth ))); result.setBottom(qMin(_usedLines - 1, qMax(0, (widgetArea.bottom() - contentsRect().top() - _contentRect.top() ) / _fontHeight))); return result; } /* ------------------------------------------------------------------------- */ /* */ /* Blinking Text & Cursor */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setBlinkingCursorEnabled(bool blink) { _allowBlinkingCursor = blink; if (blink && !_blinkCursorTimer->isActive()) { _blinkCursorTimer->start(); } if (!blink && _blinkCursorTimer->isActive()) { _blinkCursorTimer->stop(); if (_cursorBlinking) { // if cursor is blinking(hidden), blink it again to make it show _cursorBlinking = false; updateCursor(); } Q_ASSERT(!_cursorBlinking); } } void TerminalDisplay::setBlinkingTextEnabled(bool blink) { _allowBlinkingText = blink; if (blink && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!blink && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } } void TerminalDisplay::focusOutEvent(QFocusEvent*) { // trigger a repaint of the cursor so that it is both: // // * visible (in case it was hidden during blinking) // * drawn in a focused out state _cursorBlinking = false; updateCursor(); // suppress further cursor blinking _blinkCursorTimer->stop(); Q_ASSERT(!_cursorBlinking); // if text is blinking (hidden), blink it again to make it shown if (_textBlinking) { blinkTextEvent(); } // suppress further text blinking _blinkTextTimer->stop(); Q_ASSERT(!_textBlinking); _showUrlHint = false; _headerBar->terminalFocusOut(); emit focusLost(); } void TerminalDisplay::focusInEvent(QFocusEvent*) { if (_allowBlinkingCursor) { _blinkCursorTimer->start(); } updateCursor(); if (_allowBlinkingText && _hasTextBlinker) { _blinkTextTimer->start(); } _headerBar->terminalFocusIn(); emit focusGained(); } void TerminalDisplay::blinkTextEvent() { Q_ASSERT(_allowBlinkingText); _textBlinking = !_textBlinking; // TODO: Optimize to only repaint the areas of the widget where there is // blinking text rather than repainting the whole widget. _headerBar->terminalFocusOut(); update(); } void TerminalDisplay::blinkCursorEvent() { Q_ASSERT(_allowBlinkingCursor); _cursorBlinking = !_cursorBlinking; updateCursor(); } void TerminalDisplay::updateCursor() { if (!isCursorOnDisplay()){ return; } const int cursorLocation = loc(cursorPosition().x(), cursorPosition().y()); Q_ASSERT(cursorLocation < _imageSize); int charWidth = _image[cursorLocation].width(); QRect cursorRect = imageToWidget(QRect(cursorPosition(), QSize(charWidth, 1))); update(cursorRect); } /* ------------------------------------------------------------------------- */ /* */ /* Geometry & Resizing */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) if (contentsRect().isValid()) { // NOTE: This calls setTabText() in TabbedViewContainer::updateTitle(), // which might update the widget size again. New resizeEvent // won't be called, do not rely on new sizes before this call. updateImageSize(); updateImage(); } const auto scrollBarWidth = _scrollbarLocation != Enum::ScrollBarHidden ? _scrollBar->width() : 0; const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0; const auto x = width() - scrollBarWidth - _searchBar->width(); const auto y = headerHeight; _searchBar->move(x, y); } void TerminalDisplay::propagateSize() { if (_image != nullptr) { updateImageSize(); } } void TerminalDisplay::updateImageSize() { Character* oldImage = _image; const int oldLines = _lines; const int oldColumns = _columns; makeImage(); if (oldImage != nullptr) { // copy the old image to reduce flicker int lines = qMin(oldLines, _lines); int columns = qMin(oldColumns, _columns); for (int line = 0; line < lines; line++) { memcpy((void*)&_image[_columns * line], (void*)&oldImage[oldColumns * line], columns * sizeof(Character)); } delete[] oldImage; } if (!_screenWindow.isNull()) { _screenWindow->setWindowLines(_lines); } _resizing = (oldLines != _lines) || (oldColumns != _columns); if (_resizing) { showResizeNotification(); emit changedContentSizeSignal(_contentRect.height(), _contentRect.width()); // expose resizeEvent } _resizing = false; } void TerminalDisplay::makeImage() { _wallpaper->load(); calcGeometry(); // confirm that array will be of non-zero size, since the painting code // assumes a non-zero array length Q_ASSERT(_lines > 0 && _columns > 0); Q_ASSERT(_usedLines <= _lines && _usedColumns <= _columns); _imageSize = _lines * _columns; _image = new Character[_imageSize]; clearImage(); } void TerminalDisplay::clearImage() { for (int i = 0; i < _imageSize; ++i) { _image[i] = Screen::DefaultChar; } } void TerminalDisplay::calcGeometry() { const auto headerHeight = _headerBar->isVisible() ? _headerBar->height() : 0; _scrollBar->resize( _scrollBar->sizeHint().width(), // width contentsRect().height() - headerHeight // height ); _contentRect = contentsRect().adjusted(_margin, _margin, -_margin, -_margin); switch (_scrollbarLocation) { case Enum::ScrollBarHidden : break; case Enum::ScrollBarLeft : _contentRect.setLeft(_contentRect.left() + _scrollBar->width()); _scrollBar->move(contentsRect().left(), contentsRect().top() + headerHeight); break; case Enum::ScrollBarRight: _contentRect.setRight(_contentRect.right() - _scrollBar->width()); _scrollBar->move(contentsRect().left() + contentsRect().width() - _scrollBar->width(), contentsRect().top() + headerHeight); break; } _contentRect.setTop(_contentRect.top() + headerHeight); // ensure that display is always at least one column wide _columns = qMax(1, _contentRect.width() / _fontWidth); _usedColumns = qMin(_usedColumns, _columns); // ensure that display is always at least one line high _lines = qMax(1, _contentRect.height() / _fontHeight); _usedLines = qMin(_usedLines, _lines); if(_centerContents) { QSize unusedPixels = _contentRect.size() - QSize(_columns * _fontWidth, _lines * _fontHeight); _contentRect.adjust(unusedPixels.width() / 2, unusedPixels.height() / 2, 0, 0); } } // calculate the needed size, this must be synced with calcGeometry() void TerminalDisplay::setSize(int columns, int lines) { const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->sizeHint().width(); const int horizontalMargin = _margin * 2; const int verticalMargin = _margin * 2; QSize newSize = QSize(horizontalMargin + scrollBarWidth + (columns * _fontWidth) , verticalMargin + (lines * _fontHeight)); if (newSize != size()) { _size = newSize; updateGeometry(); } } QSize TerminalDisplay::sizeHint() const { return _size; } //showEvent and hideEvent are reimplemented here so that it appears to other classes that the //display has been resized when the display is hidden or shown. // //TODO: Perhaps it would be better to have separate signals for show and hide instead of using //the same signal as the one for a content size change void TerminalDisplay::showEvent(QShowEvent*) { propagateSize(); emit changedContentSizeSignal(_contentRect.height(), _contentRect.width()); } void TerminalDisplay::hideEvent(QHideEvent*) { emit changedContentSizeSignal(_contentRect.height(), _contentRect.width()); } void TerminalDisplay::setMargin(int margin) { if (margin < 0) { margin = 0; } _margin = margin; updateImageSize(); } void TerminalDisplay::setCenterContents(bool enable) { _centerContents = enable; calcGeometry(); update(); } /* ------------------------------------------------------------------------- */ /* */ /* Scrollbar */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setScrollBarPosition(Enum::ScrollBarPositionEnum position) { if (_scrollbarLocation == position) { return; } _scrollBar->setHidden(position == Enum::ScrollBarHidden); _scrollbarLocation = position; propagateSize(); update(); } void TerminalDisplay::scrollBarPositionChanged(int) { if (_screenWindow.isNull()) { return; } _screenWindow->scrollTo(_scrollBar->value()); // if the thumb has been moved to the bottom of the _scrollBar then set // the display to automatically track new output, // that is, scroll down automatically // to how new _lines as they are added const bool atEndOfOutput = (_scrollBar->value() == _scrollBar->maximum()); _screenWindow->setTrackOutput(atEndOfOutput); updateImage(); } void TerminalDisplay::setScroll(int cursor, int slines) { // update _scrollBar if the range or value has changed, // otherwise return // // setting the range or value of a _scrollBar will always trigger // a repaint, so it should be avoided if it is not necessary if (_scrollBar->minimum() == 0 && _scrollBar->maximum() == (slines - _lines) && _scrollBar->value() == cursor) { return; } disconnect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged); _scrollBar->setRange(0, slines - _lines); _scrollBar->setSingleStep(1); _scrollBar->setPageStep(_lines); _scrollBar->setValue(cursor); connect(_scrollBar, &QScrollBar::valueChanged, this, &Konsole::TerminalDisplay::scrollBarPositionChanged); } void TerminalDisplay::setScrollFullPage(bool fullPage) { _scrollFullPage = fullPage; } bool TerminalDisplay::scrollFullPage() const { return _scrollFullPage; } /* ------------------------------------------------------------------------- */ /* */ /* Mouse */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::mousePressEvent(QMouseEvent* ev) { if (_possibleTripleClick && (ev->button() == Qt::LeftButton)) { mouseTripleClickEvent(ev); return; } if (!contentsRect().contains(ev->pos())) { return; } if (_screenWindow.isNull()) { return; } // Ignore clicks on the message widget if (_readOnlyMessageWidget != nullptr) { if (_readOnlyMessageWidget->isVisible() && _readOnlyMessageWidget->frameGeometry().contains(ev->pos())) { return; } } if (_outputSuspendedMessageWidget != nullptr) { if (_outputSuspendedMessageWidget->isVisible() && _outputSuspendedMessageWidget->frameGeometry().contains(ev->pos())) { return; } } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); QPoint pos = QPoint(charColumn, charLine); if (ev->button() == Qt::LeftButton) { // request the software keyboard, if any if (qApp->autoSipEnabled()) { auto behavior = QStyle::RequestSoftwareInputPanel( style()->styleHint(QStyle::SH_RequestSoftwareInputPanel)); if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) { QEvent event(QEvent::RequestSoftwareInputPanel); QApplication::sendEvent(this, &event); } } if (!ev->modifiers()) { _lineSelectionMode = false; _wordSelectionMode = false; } // The user clicked inside selected text bool selected = _screenWindow->isSelected(pos.x(), pos.y()); // Drag only when the Control key is held if ((!_ctrlRequiredForDrag || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && selected) { _dragInfo.state = diPending; _dragInfo.start = ev->pos(); } else { // No reason to ever start a drag event _dragInfo.state = diNone; _preserveLineBreaks = !(((ev->modifiers() & Qt::ControlModifier) != 0u) && !(ev->modifiers() & Qt::AltModifier)); _columnSelectionMode = ((ev->modifiers() & Qt::AltModifier) != 0u) && ((ev->modifiers() & Qt::ControlModifier) != 0u); // There are a couple of use cases when selecting text : // Normal buffer or Alternate buffer when not using Mouse Tracking: // select text or extendSelection or columnSelection or columnSelection + extendSelection // // Alternate buffer when using Mouse Tracking and with Shift pressed: // select text or columnSelection if (!_usesMouseTracking && ((ev->modifiers() == Qt::ShiftModifier) || (((ev->modifiers() & Qt::ShiftModifier) != 0u) && _columnSelectionMode))) { extendSelection(ev->pos()); } else if ((!_usesMouseTracking && !((ev->modifiers() & Qt::ShiftModifier))) || (_usesMouseTracking && ((ev->modifiers() & Qt::ShiftModifier) != 0u))) { _screenWindow->clearSelection(); pos.ry() += _scrollBar->value(); _iPntSel = _pntSel = pos; _actSel = 1; // left mouse button pressed but nothing selected yet. } else if (_usesMouseTracking && !_readOnly) { emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u))) { Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) { QObject action; action.setObjectName(QStringLiteral("open-action")); spot->activate(&action); } } } } else if (ev->button() == Qt::MidButton) { processMidButtonClick(ev); } else if (ev->button() == Qt::RightButton) { if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) { emit configureRequest(ev->pos()); } else { if(!_readOnly) { emit mouseSignal(2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } } QList TerminalDisplay::filterActions(const QPoint& position) { int charLine, charColumn; getCharacterPosition(position, charLine, charColumn, false); Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); return spot != nullptr ? spot->actions() : QList(); } void TerminalDisplay::mouseMoveEvent(QMouseEvent* ev) { int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); processFilters(); // handle filters // change link hot-spot appearance on mouse-over Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) { QRegion previousHotspotArea = _mouseOverHotspotArea; _mouseOverHotspotArea = QRegion(); QRect r; if (spot->startLine() == spot->endLine()) { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } else { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) { r.setCoords(0 * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } r.setCoords(0 * _fontWidth + _contentRect.left(), spot->endLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && (cursor().shape() != Qt::PointingHandCursor)) { setCursor(Qt::PointingHandCursor); } update(_mouseOverHotspotArea | previousHotspotArea); } else if (!_mouseOverHotspotArea.isEmpty()) { if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) || (cursor().shape() == Qt::PointingHandCursor)) { setCursor(_usesMouseTracking ? Qt::ArrowCursor : Qt::IBeamCursor); } update(_mouseOverHotspotArea); // set hotspot area to an invalid rectangle _mouseOverHotspotArea = QRegion(); } // for auto-hiding the cursor, we need mouseTracking if (ev->buttons() == Qt::NoButton) { return; } // if the program running in the terminal is interested in Mouse Tracking // evnets then emit a mouse movement signal, unless the shift key is // being held down, which overrides this. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { int button = 3; if ((ev->buttons() & Qt::LeftButton) != 0u) { button = 0; } if ((ev->buttons() & Qt::MidButton) != 0u) { button = 1; } if ((ev->buttons() & Qt::RightButton) != 0u) { button = 2; } emit mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 1); return; } if (_dragInfo.state == diPending) { // we had a mouse down, but haven't confirmed a drag yet // if the mouse has moved sufficiently, we will confirm const int distance = QApplication::startDragDistance(); if (ev->x() > _dragInfo.start.x() + distance || ev->x() < _dragInfo.start.x() - distance || ev->y() > _dragInfo.start.y() + distance || ev->y() < _dragInfo.start.y() - distance) { // we've left the drag square, we can start a real drag operation now _screenWindow->clearSelection(); doDrag(); } return; } else if (_dragInfo.state == diDragging) { // this isn't technically needed because mouseMoveEvent is suppressed during // Qt drag operations, replaced by dragMoveEvent return; } if (_actSel == 0) { return; } // don't extend selection while pasting if ((ev->buttons() & Qt::MidButton) != 0u) { return; } extendSelection(ev->pos()); } void TerminalDisplay::leaveEvent(QEvent *) { // remove underline from an active link when cursor leaves the widget area if(!_mouseOverHotspotArea.isEmpty()) { update(_mouseOverHotspotArea); _mouseOverHotspotArea = QRegion(); } } void TerminalDisplay::extendSelection(const QPoint& position) { if (_screenWindow.isNull()) { return; } //if ( !contentsRect().contains(ev->pos()) ) return; const QPoint tL = contentsRect().topLeft(); const int tLx = tL.x(); const int tLy = tL.y(); const int scroll = _scrollBar->value(); // we're in the process of moving the mouse with the left button pressed // the mouse cursor will kept caught within the bounds of the text in // this widget. int linesBeyondWidget = 0; QRect textBounds(tLx + _contentRect.left(), tLy + _contentRect.top(), _usedColumns * _fontWidth - 1, _usedLines * _fontHeight - 1); QPoint pos = position; // Adjust position within text area bounds. const QPoint oldpos = pos; pos.setX(qBound(textBounds.left(), pos.x(), textBounds.right())); pos.setY(qBound(textBounds.top(), pos.y(), textBounds.bottom())); if (oldpos.y() > textBounds.bottom()) { linesBeyondWidget = (oldpos.y() - textBounds.bottom()) / _fontHeight; _scrollBar->setValue(_scrollBar->value() + linesBeyondWidget + 1); // scrollforward } if (oldpos.y() < textBounds.top()) { linesBeyondWidget = (textBounds.top() - oldpos.y()) / _fontHeight; _scrollBar->setValue(_scrollBar->value() - linesBeyondWidget - 1); // history } int charColumn = 0; int charLine = 0; getCharacterPosition(pos, charLine, charColumn, true); QPoint here = QPoint(charColumn, charLine); QPoint ohere; QPoint _iPntSelCorr = _iPntSel; _iPntSelCorr.ry() -= _scrollBar->value(); QPoint _pntSelCorr = _pntSel; _pntSelCorr.ry() -= _scrollBar->value(); bool swapping = false; if (_wordSelectionMode) { // Extend to word boundaries const bool left_not_right = (here.y() < _iPntSelCorr.y() || (here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x())); const bool old_left_not_right = (_pntSelCorr.y() < _iPntSelCorr.y() || (_pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x())); swapping = left_not_right != old_left_not_right; // Find left (left_not_right ? from here : from start of word) QPoint left = left_not_right ? here : _iPntSelCorr; // Find left (left_not_right ? from end of word : from here) QPoint right = left_not_right ? _iPntSelCorr : here; if (left.y() < 0 || left.y() >= _lines || left.x() < 0 || left.x() >= _columns) { left = _pntSelCorr; } else { left = findWordStart(left); } if (right.y() < 0 || right.y() >= _lines || right.x() < 0 || right.x() >= _columns) { right = _pntSelCorr; } else { right = findWordEnd(right); } // Pick which is start (ohere) and which is extension (here) if (left_not_right) { here = left; ohere = right; } else { here = right; ohere = left; } ohere.rx()++; } if (_lineSelectionMode) { // Extend to complete line const bool above_not_below = (here.y() < _iPntSelCorr.y()); if (above_not_below) { ohere = findLineEnd(_iPntSelCorr); here = findLineStart(here); } else { ohere = findLineStart(_iPntSelCorr); here = findLineEnd(here); } swapping = !(_tripleSelBegin == ohere); _tripleSelBegin = ohere; ohere.rx()++; } int offset = 0; if (!_wordSelectionMode && !_lineSelectionMode) { QChar selClass; const bool left_not_right = (here.y() < _iPntSelCorr.y() || (here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x())); const bool old_left_not_right = (_pntSelCorr.y() < _iPntSelCorr.y() || (_pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x())); swapping = left_not_right != old_left_not_right; // Find left (left_not_right ? from here : from start) const QPoint left = left_not_right ? here : _iPntSelCorr; // Find left (left_not_right ? from start : from here) QPoint right = left_not_right ? _iPntSelCorr : here; if (right.x() > 0 && !_columnSelectionMode) { if (right.x() - 1 < _columns && right.y() < _lines) { selClass = charClass(_image[loc(right.x() - 1, right.y())]); } } // Pick which is start (ohere) and which is extension (here) if (left_not_right) { here = left; ohere = right; offset = 0; } else { here = right; ohere = left; offset = -1; } } if ((here == _pntSelCorr) && (scroll == _scrollBar->value())) { return; // not moved } if (here == ohere) { return; // It's not left, it's not right. } if (_actSel < 2 || swapping) { if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) { _screenWindow->setSelectionStart(ohere.x() , ohere.y() , true); } else { _screenWindow->setSelectionStart(ohere.x() - 1 - offset , ohere.y() , false); } } _actSel = 2; // within selection _pntSel = here; _pntSel.ry() += _scrollBar->value(); if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) { _screenWindow->setSelectionEnd(here.x() , here.y()); } else { _screenWindow->setSelectionEnd(here.x() + offset , here.y()); } } void TerminalDisplay::mouseReleaseEvent(QMouseEvent* ev) { if (_screenWindow.isNull()) { return; } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); if (ev->button() == Qt::LeftButton) { if (_dragInfo.state == diPending) { // We had a drag event pending but never confirmed. Kill selection _screenWindow->clearSelection(); } else { if (_actSel > 1) { copyToX11Selection(); } _actSel = 0; //FIXME: emits a release event even if the mouse is // outside the range. The procedure used in `mouseMoveEvent' // applies here, too. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2); } } _dragInfo.state = diNone; } if (_usesMouseTracking && (ev->button() == Qt::RightButton || ev->button() == Qt::MidButton) && !(ev->modifiers() & Qt::ShiftModifier)) { emit mouseSignal(ev->button() == Qt::MidButton ? 1 : 2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2); } } void TerminalDisplay::getCharacterPosition(const QPoint& widgetPoint, int& line, int& column, bool edge) const { // the column value returned can be equal to _usedColumns (when edge == true), // which is the position just after the last character displayed in a line. // // this is required so that the user can select characters in the right-most // column (or left-most for right-to-left input) const int columnMax = edge ? _usedColumns : _usedColumns - 1; const int xOffset = edge ? _fontWidth / 2 : 0; column = qBound(0, (widgetPoint.x() + xOffset - contentsRect().left() - _contentRect.left()) / _fontWidth, columnMax); line = qBound(0, (widgetPoint.y() - contentsRect().top() - _contentRect.top()) / _fontHeight, _usedLines - 1); } void TerminalDisplay::updateLineProperties() { if (_screenWindow.isNull()) { return; } _lineProperties = _screenWindow->getLineProperties(); } void TerminalDisplay::processMidButtonClick(QMouseEvent* ev) { if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) { const bool appendEnter = (ev->modifiers() & Qt::ControlModifier) != 0u; if (_middleClickPasteMode == Enum::PasteFromX11Selection) { pasteFromX11Selection(appendEnter); } else if (_middleClickPasteMode == Enum::PasteFromClipboard) { pasteFromClipboard(appendEnter); } else { Q_ASSERT(false); } } else { if(!_readOnly) { int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); emit mouseSignal(1, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } void TerminalDisplay::mouseDoubleClickEvent(QMouseEvent* ev) { // Yes, successive middle click can trigger this event if (ev->button() == Qt::MidButton) { processMidButtonClick(ev); return; } if (ev->button() != Qt::LeftButton) { return; } if (_screenWindow.isNull()) { return; } int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); QPoint pos(qMin(charColumn, _columns - 1), qMin(charLine, _lines - 1)); // pass on double click as two clicks. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { if(!_readOnly) { // Send just _ONE_ click event, since the first click of the double click // was already sent by the click handler emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0); // left button } return; } _screenWindow->clearSelection(); _iPntSel = pos; _iPntSel.ry() += _scrollBar->value(); _wordSelectionMode = true; _actSel = 2; // within selection // find word boundaries... { // find the start of the word const QPoint bgnSel = findWordStart(pos); const QPoint endSel = findWordEnd(pos); _actSel = 2; // within selection _screenWindow->setSelectionStart(bgnSel.x() , bgnSel.y() , false); _screenWindow->setSelectionEnd(endSel.x() , endSel.y()); copyToX11Selection(); } _possibleTripleClick = true; QTimer::singleShot(QApplication::doubleClickInterval(), this, [this]() { _possibleTripleClick = false; }); } void TerminalDisplay::wheelEvent(QWheelEvent* ev) { // Only vertical scrolling is supported if (ev->orientation() != Qt::Vertical) { return; } const int modifiers = ev->modifiers(); // ctrl+ for zooming, like in konqueror and firefox if (((modifiers & Qt::ControlModifier) != 0u) && _mouseWheelZoom) { _scrollWheelState.addWheelEvent(ev); int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE); for (;steps > 0; --steps) { // wheel-up for increasing font size increaseFontSize(); } for (;steps < 0; ++steps) { // wheel-down for decreasing font size decreaseFontSize(); } } else if (!_usesMouseTracking && (_scrollBar->maximum() > 0)) { // If the program running in the terminal is not interested in Mouse // Tracking events, send the event to the scrollbar if the slider // has room to move _scrollWheelState.addWheelEvent(ev); _scrollBar->event(ev); Q_ASSERT(_sessionController != nullptr); _sessionController->setSearchStartToWindowCurrentLine(); _scrollWheelState.clearAll(); } else if (!_readOnly) { _scrollWheelState.addWheelEvent(ev); Q_ASSERT(!_sessionController->session().isNull()); if(!_usesMouseTracking && !_sessionController->session()->isPrimaryScreen() && _alternateScrolling) { // Send simulated up / down key presses to the terminal program // for the benefit of programs such as 'less' (which use the alternate screen) // assume that each Up / Down key event will cause the terminal application // to scroll by one line. // // to get a reasonable scrolling speed, scroll by one line for every 5 degrees // of mouse wheel rotation. Mouse wheels typically move in steps of 15 degrees, // giving a scroll of 3 lines const int lines = _scrollWheelState.consumeSteps(static_cast(_fontHeight * qApp->devicePixelRatio()), ScrollState::degreesToAngle(5)); const int keyCode = lines > 0 ? Qt::Key_Up : Qt::Key_Down; QKeyEvent keyEvent(QEvent::KeyPress, keyCode, Qt::NoModifier); for (int i = 0; i < abs(lines); i++) { _screenWindow->screen()->setCurrentTerminalDisplay(this); emit keyPressedSignal(&keyEvent); } } else if (_usesMouseTracking) { // terminal program wants notification of mouse activity int charLine; int charColumn; getCharacterPosition(ev->pos() , charLine , charColumn, !_usesMouseTracking); const int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE); const int button = (steps > 0) ? 4 : 5; for (int i = 0; i < abs(steps); ++i) { emit mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } } void TerminalDisplay::viewScrolledByUser() { Q_ASSERT(_sessionController != nullptr); _sessionController->setSearchStartToWindowCurrentLine(); } /* Moving left/up from the line containing pnt, return the starting offset point which the given line is continuously wrapped (top left corner = 0,0; previous line not visible = 0,-1). */ QPoint TerminalDisplay::findLineStart(const QPoint &pnt) { const int visibleScreenLines = _lineProperties.size(); const int topVisibleLine = _screenWindow->currentLine(); Screen *screen = _screenWindow->screen(); int line = pnt.y(); int lineInHistory= line + topVisibleLine; QVector lineProperties = _lineProperties; while (lineInHistory > 0) { for (; line > 0; line--, lineInHistory--) { // Does previous line wrap around? if ((lineProperties[line - 1] & LINE_WRAPPED) == 0) { return {0, lineInHistory - topVisibleLine}; } } if (lineInHistory < 1) { break; } // _lineProperties is only for the visible screen, so grab new data int newRegionStart = qMax(0, lineInHistory - visibleScreenLines); lineProperties = screen->getLineProperties(newRegionStart, lineInHistory - 1); line = lineInHistory - newRegionStart; } return {0, lineInHistory - topVisibleLine}; } /* Moving right/down from the line containing pnt, return the ending offset point which the given line is continuously wrapped. */ QPoint TerminalDisplay::findLineEnd(const QPoint &pnt) { const int visibleScreenLines = _lineProperties.size(); const int topVisibleLine = _screenWindow->currentLine(); const int maxY = _screenWindow->lineCount() - 1; Screen *screen = _screenWindow->screen(); int line = pnt.y(); int lineInHistory= line + topVisibleLine; QVector lineProperties = _lineProperties; while (lineInHistory < maxY) { for (; line < lineProperties.count() && lineInHistory < maxY; line++, lineInHistory++) { // Does current line wrap around? if ((lineProperties[line] & LINE_WRAPPED) == 0) { return {_columns - 1, lineInHistory - topVisibleLine}; } } line = 0; lineProperties = screen->getLineProperties(lineInHistory, qMin(lineInHistory + visibleScreenLines, maxY)); } return {_columns - 1, lineInHistory - topVisibleLine}; } QPoint TerminalDisplay::findWordStart(const QPoint &pnt) { const int regSize = qMax(_screenWindow->windowLines(), 10); const int firstVisibleLine = _screenWindow->currentLine(); Screen *screen = _screenWindow->screen(); Character *image = _image; Character *tmp_image = nullptr; int imgLine = pnt.y(); int x = pnt.x(); int y = imgLine + firstVisibleLine; int imgLoc = loc(x, imgLine); QVector lineProperties = _lineProperties; const QChar selClass = charClass(image[imgLoc]); const int imageSize = regSize * _columns; while (true) { for (;;imgLoc--, x--) { if (imgLoc < 1) { // no more chars in this region break; } if (x > 0) { // has previous char on this line if (charClass(image[imgLoc - 1]) == selClass) { continue; } goto out; } else if (imgLine > 0) { // not the first line in the session if ((lineProperties[imgLine - 1] & LINE_WRAPPED) != 0) { // have continuation on prev line if (charClass(image[imgLoc - 1]) == selClass) { x = _columns; imgLine--; y--; continue; } } goto out; } else if (y > 0) { // want more data, but need to fetch new region break; } else { goto out; } } if (y <= 0) { // No more data goto out; } int newRegStart = qMax(0, y - regSize + 1); lineProperties = screen->getLineProperties(newRegStart, y - 1); imgLine = y - newRegStart; delete[] tmp_image; tmp_image = new Character[imageSize]; image = tmp_image; screen->getImage(tmp_image, imageSize, newRegStart, y - 1); imgLoc = loc(x, imgLine); if (imgLoc < 1) { // Reached the start of the session break; } } out: delete[] tmp_image; return {x, y - firstVisibleLine}; } QPoint TerminalDisplay::findWordEnd(const QPoint &pnt) { const int regSize = qMax(_screenWindow->windowLines(), 10); const int curLine = _screenWindow->currentLine(); int i = pnt.y(); int x = pnt.x(); int y = i + curLine; int j = loc(x, i); QVector lineProperties = _lineProperties; Screen *screen = _screenWindow->screen(); Character *image = _image; Character *tmp_image = nullptr; const QChar selClass = charClass(image[j]); const int imageSize = regSize * _columns; const int maxY = _screenWindow->lineCount() - 1; const int maxX = _columns - 1; while (true) { const int lineCount = lineProperties.count(); for (;;j++, x++) { if (x < maxX) { if (charClass(image[j + 1]) == selClass && // A colon right before whitespace is never part of a word ! (image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) { continue; } goto out; } else if (i < lineCount - 1) { if (((lineProperties[i] & LINE_WRAPPED) != 0) && charClass(image[j + 1]) == selClass && // A colon right before whitespace is never part of a word ! (image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) { x = -1; i++; y++; continue; } goto out; } else if (y < maxY) { if (i < lineCount && ((lineProperties[i] & LINE_WRAPPED) == 0)) { goto out; } break; } else { goto out; } } int newRegEnd = qMin(y + regSize - 1, maxY); lineProperties = screen->getLineProperties(y, newRegEnd); i = 0; if (tmp_image == nullptr) { tmp_image = new Character[imageSize]; image = tmp_image; } screen->getImage(tmp_image, imageSize, y, newRegEnd); x--; j = loc(x, i); } out: y -= curLine; // In word selection mode don't select @ (64) if at end of word. if (((image[j].rendition & RE_EXTENDED_CHAR) == 0) && (QChar(image[j].character) == QLatin1Char('@')) && (y > pnt.y() || x > pnt.x())) { if (x > 0) { x--; } else { y--; } } delete[] tmp_image; return {x, y}; } Screen::DecodingOptions TerminalDisplay::currentDecodingOptions() { Screen::DecodingOptions decodingOptions; if (_preserveLineBreaks) { decodingOptions |= Screen::PreserveLineBreaks; } if (_trimLeadingSpaces) { decodingOptions |= Screen::TrimLeadingWhitespace; } if (_trimTrailingSpaces) { decodingOptions |= Screen::TrimTrailingWhitespace; } return decodingOptions; } void TerminalDisplay::mouseTripleClickEvent(QMouseEvent* ev) { if (_screenWindow.isNull()) { return; } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, true); selectLine(QPoint(charColumn, charLine), _tripleClickMode == Enum::SelectWholeLine); } void TerminalDisplay::selectLine(QPoint pos, bool entireLine) { _iPntSel = pos; _screenWindow->clearSelection(); _lineSelectionMode = true; _wordSelectionMode = false; _actSel = 2; // within selection if (!entireLine) { // Select from cursor to end of line _tripleSelBegin = findWordStart(_iPntSel); _screenWindow->setSelectionStart(_tripleSelBegin.x(), _tripleSelBegin.y() , false); } else { _tripleSelBegin = findLineStart(_iPntSel); _screenWindow->setSelectionStart(0 , _tripleSelBegin.y() , false); } _iPntSel = findLineEnd(_iPntSel); _screenWindow->setSelectionEnd(_iPntSel.x() , _iPntSel.y()); copyToX11Selection(); _iPntSel.ry() += _scrollBar->value(); } void TerminalDisplay::selectCurrentLine() { if (_screenWindow.isNull()) { return; } selectLine(cursorPosition(), true); } void TerminalDisplay::selectAll() { if (_screenWindow.isNull()) { return; } _preserveLineBreaks = true; _screenWindow->setSelectionByLineRange(0, _screenWindow->lineCount()); copyToX11Selection(); } bool TerminalDisplay::focusNextPrevChild(bool next) { // for 'Tab', always disable focus switching among widgets // for 'Shift+Tab', leave the decision to higher level if (next) { return false; } else { return QWidget::focusNextPrevChild(next); } } QChar TerminalDisplay::charClass(const Character& ch) const { if ((ch.rendition & RE_EXTENDED_CHAR) != 0) { ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength); if ((chars != nullptr) && extendedCharLength > 0) { const QString s = QString::fromUcs4(chars, extendedCharLength); if (_wordCharacters.contains(s, Qt::CaseInsensitive)) { return QLatin1Char('a'); } bool letterOrNumber = false; for (int i = 0; !letterOrNumber && i < s.size(); ++i) { letterOrNumber = s.at(i).isLetterOrNumber(); } return letterOrNumber ? QLatin1Char('a') : s.at(0); } return 0; } else { const QChar qch(ch.character); if (qch.isSpace()) { return QLatin1Char(' '); } if (qch.isLetterOrNumber() || _wordCharacters.contains(qch, Qt::CaseInsensitive)) { return QLatin1Char('a'); } return qch; } } void TerminalDisplay::setWordCharacters(const QString& wc) { _wordCharacters = wc; } void TerminalDisplay::setUsesMouseTracking(bool on) { _usesMouseTracking = on; setCursor(_usesMouseTracking ? Qt::ArrowCursor : Qt::IBeamCursor); } bool TerminalDisplay::usesMouseTracking() const { return _usesMouseTracking; } void TerminalDisplay::setAlternateScrolling(bool enable) { _alternateScrolling = enable; } bool TerminalDisplay::alternateScrolling() const { return _alternateScrolling; } void TerminalDisplay::setBracketedPasteMode(bool on) { _bracketedPasteMode = on; } bool TerminalDisplay::bracketedPasteMode() const { return _bracketedPasteMode; } /* ------------------------------------------------------------------------- */ /* */ /* Clipboard */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::doPaste(QString text, bool appendReturn) { if (_screenWindow.isNull()) { return; } if (_readOnly) { return; } if (appendReturn) { text.append(QLatin1String("\r")); } if (text.length() > 8000) { if (KMessageBox::warningContinueCancel(window(), i18np("Are you sure you want to paste %1 character?", "Are you sure you want to paste %1 characters?", text.length()), i18n("Confirm Paste"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("ShowPasteHugeTextWarning")) == KMessageBox::Cancel) { return; } } // Most code in Konsole uses UTF-32. We're filtering // UTF-16 here, as all control characters can be represented // in this encoding as single code unit. If you ever need to // filter anything above 0xFFFF (specific code points or // categories which contain such code points), convert text to // UTF-32 using QString::toUcs4() and use QChar static // methods which take "uint ucs4". static const QVector whitelist = { u'\t', u'\r', u'\n' }; static const auto isUnsafe = [](const QChar &c) { return (c.category() == QChar::Category::Other_Control && !whitelist.contains(c.unicode())); }; // Returns control sequence string (e.g. "^C") for control character c static const auto charToSequence = [](const QChar &c) { if (c.unicode() <= 0x1F) { return QStringLiteral("^%1").arg(QChar(u'@' + c.unicode())); } else if (c.unicode() == 0x7F) { return QStringLiteral("^?"); } else if (c.unicode() >= 0x80 && c.unicode() <= 0x9F){ return QStringLiteral("^[%1").arg(QChar(u'@' + c.unicode() - 0x80)); } return QString(); }; const QMap characterDescriptions = { {0x0003, i18n("End Of Text/Interrupt: may exit the current process")}, {0x0004, i18n("End Of Transmission: may exit the current process")}, {0x0007, i18n("Bell: will try to emit an audible warning")}, {0x0008, i18n("Backspace")}, {0x0013, i18n("Device Control Three/XOFF: suspends output")}, {0x001a, i18n("Substitute/Suspend: may suspend current process")}, {0x001b, i18n("Escape: used for manipulating terminal state")}, {0x001c, i18n("File Separator/Quit: may abort the current process")}, }; QStringList unsafeCharacters; for (const QChar &c : text) { if (isUnsafe(c)) { const QString sequence = charToSequence(c); const QString description = characterDescriptions.value(c.unicode(), QString()); QString entry = QStringLiteral("U+%1").arg(c.unicode(), 4, 16, QLatin1Char('0')); if(!sequence.isEmpty()) { entry += QStringLiteral("\t%1").arg(sequence); } if(!description.isEmpty()) { entry += QStringLiteral("\t%1").arg(description); } unsafeCharacters.append(entry); } } unsafeCharacters.removeDuplicates(); if (!unsafeCharacters.isEmpty()) { int result = KMessageBox::warningYesNoCancelList(window(), i18n("The text you're trying to paste contains hidden control characters, " "do you want to filter them out?"), unsafeCharacters, i18nc("@title", "Confirm Paste"), KGuiItem(i18nc("@action:button", "Paste &without control characters"), QStringLiteral("filter-symbolic")), KGuiItem(i18nc("@action:button", "&Paste everything"), QStringLiteral("edit-paste")), KGuiItem(i18nc("@action:button", "&Cancel"), QStringLiteral("dialog-cancel")), QStringLiteral("ShowPasteUnprintableWarning") ); switch(result){ case KMessageBox::Cancel: return; case KMessageBox::Yes: { QString sanitized; for (const QChar &c : text) { if (!isUnsafe(c)) { sanitized.append(c); } } text = sanitized; } case KMessageBox::No: break; default: break; } } if (!text.isEmpty()) { text.replace(QLatin1Char('\n'), QLatin1Char('\r')); if (bracketedPasteMode()) { text.remove(QLatin1String("\033")); text.prepend(QLatin1String("\033[200~")); text.append(QLatin1String("\033[201~")); } // perform paste by simulating keypress events QKeyEvent e(QEvent::KeyPress, 0, Qt::NoModifier, text); emit keyPressedSignal(&e); } } void TerminalDisplay::setAutoCopySelectedText(bool enabled) { _autoCopySelectedText = enabled; } void TerminalDisplay::setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode) { _middleClickPasteMode = mode; } void TerminalDisplay::setCopyTextAsHTML(bool enabled) { _copyTextAsHTML = enabled; } void TerminalDisplay::copyToX11Selection() { if (_screenWindow.isNull()) { return; } const QString &text = _screenWindow->selectedText(currentDecodingOptions()); if (text.isEmpty()) { return; } auto mimeData = new QMimeData; mimeData->setText(text); if (_copyTextAsHTML) { mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml)); } if (QApplication::clipboard()->supportsSelection()) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection); } if (_autoCopySelectedText) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } } void TerminalDisplay::copyToClipboard() { if (_screenWindow.isNull()) { return; } const QString &text = _screenWindow->selectedText(currentDecodingOptions()); if (text.isEmpty()) { return; } auto mimeData = new QMimeData; mimeData->setText(text); if (_copyTextAsHTML) { mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml)); } QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void TerminalDisplay::pasteFromClipboard(bool appendEnter) { QString text; const QMimeData *mimeData = QApplication::clipboard()->mimeData(QClipboard::Clipboard); // When pasting urls of local files: // - remove the scheme part, "file://" // - paste the path(s) as a space-separated list of strings, which are quoted if needed if (!mimeData->hasUrls()) { // fast path if there are no urls text = mimeData->text(); } else { // handle local file urls const QList list = mimeData->urls(); for (const QUrl &url : list) { if (url.isLocalFile()) { text += KShell::quoteArg(url.toLocalFile()); text += QLatin1Char(' '); } else { // can users copy urls of both local and remote files at the same time? text = mimeData->text(); break; } } } doPaste(text, appendEnter); } void TerminalDisplay::pasteFromX11Selection(bool appendEnter) { if (QApplication::clipboard()->supportsSelection()) { QString text = QApplication::clipboard()->text(QClipboard::Selection); doPaste(text, appendEnter); } } /* ------------------------------------------------------------------------- */ /* */ /* Input Method */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::inputMethodEvent(QInputMethodEvent* event) { if (!event->commitString().isEmpty()) { QKeyEvent keyEvent(QEvent::KeyPress, 0, Qt::NoModifier, event->commitString()); emit keyPressedSignal(&keyEvent); } if (!_readOnly && isCursorOnDisplay()) { _inputMethodData.preeditString = event->preeditString(); update(preeditRect() | _inputMethodData.previousPreeditRect); } event->accept(); } QVariant TerminalDisplay::inputMethodQuery(Qt::InputMethodQuery query) const { const QPoint cursorPos = cursorPosition(); switch (query) { case Qt::ImMicroFocus: return imageToWidget(QRect(cursorPos.x(), cursorPos.y(), 1, 1)); case Qt::ImFont: return font(); case Qt::ImCursorPosition: // return the cursor position within the current line return cursorPos.x(); case Qt::ImSurroundingText: { // return the text from the current line QString lineText; QTextStream stream(&lineText); PlainTextDecoder decoder; decoder.begin(&stream); if (isCursorOnDisplay()) { decoder.decodeLine(&_image[loc(0, cursorPos.y())], _usedColumns, LINE_DEFAULT); } decoder.end(); return lineText; } case Qt::ImCurrentSelection: return QString(); default: break; } return QVariant(); } QRect TerminalDisplay::preeditRect() const { const int preeditLength = Character::stringWidth(_inputMethodData.preeditString); if (preeditLength == 0) { return {}; } const QRect stringRect(_contentRect.left() + _fontWidth * cursorPosition().x(), _contentRect.top() + _fontHeight * cursorPosition().y(), _fontWidth * preeditLength, _fontHeight); return stringRect.intersected(_contentRect); } void TerminalDisplay::drawInputMethodPreeditString(QPainter& painter , const QRect& rect) { if (_inputMethodData.preeditString.isEmpty() || !isCursorOnDisplay()) { return; } const QPoint cursorPos = cursorPosition(); bool invertColors = false; const QColor background = _colorTable[DEFAULT_BACK_COLOR]; const QColor foreground = _colorTable[DEFAULT_FORE_COLOR]; const Character* style = &_image[loc(cursorPos.x(), cursorPos.y())]; drawBackground(painter, rect, background, true); drawCursor(painter, rect, foreground, background, invertColors); drawCharacters(painter, rect, _inputMethodData.preeditString, style, invertColors); _inputMethodData.previousPreeditRect = rect; } /* ------------------------------------------------------------------------- */ /* */ /* Keyboard */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setFlowControlWarningEnabled(bool enable) { _flowControlWarningEnabled = enable; // if the dialog is currently visible and the flow control warning has // been disabled then hide the dialog if (!enable) { outputSuspended(false); } } void TerminalDisplay::outputSuspended(bool suspended) { //create the label when this function is first called if (_outputSuspendedMessageWidget == nullptr) { //This label includes a link to an English language website //describing the 'flow control' (Xon/Xoff) feature found in almost //all terminal emulators. //If there isn't a suitable article available in the target language the link //can simply be removed. _outputSuspendedMessageWidget = createMessageWidget(i18n("Output has been " "suspended" " by pressing Ctrl+S." " Press Ctrl+Q to resume.")); connect(_outputSuspendedMessageWidget, &KMessageWidget::linkActivated, this, [](const QString &url) { QDesktopServices::openUrl(QUrl(url)); }); _outputSuspendedMessageWidget->setMessageType(KMessageWidget::Warning); } suspended ? _outputSuspendedMessageWidget->animatedShow() : _outputSuspendedMessageWidget->animatedHide(); } void TerminalDisplay::dismissOutputSuspendedMessage() { outputSuspended(false); } KMessageWidget* TerminalDisplay::createMessageWidget(const QString &text) { auto widget = new KMessageWidget(text); widget->setWordWrap(true); widget->setFocusProxy(this); widget->setCursor(Qt::ArrowCursor); _verticalLayout->insertWidget(1, widget); return widget; } void TerminalDisplay::updateReadOnlyState(bool readonly) { if (_readOnly == readonly) { return; } if (readonly) { // Lazy create the readonly messagewidget if (_readOnlyMessageWidget == nullptr) { _readOnlyMessageWidget = createMessageWidget(i18n("This terminal is read-only.")); _readOnlyMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("object-locked"))); } } if (_readOnlyMessageWidget != nullptr) { readonly ? _readOnlyMessageWidget->animatedShow() : _readOnlyMessageWidget->animatedHide(); } _readOnly = readonly; } void TerminalDisplay::scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode, int amount) { _screenWindow->scrollBy(mode, amount, _scrollFullPage); _screenWindow->setTrackOutput(_screenWindow->atEndOfOutput()); updateLineProperties(); updateImage(); viewScrolledByUser(); } void TerminalDisplay::keyPressEvent(QKeyEvent* event) { if ((_urlHintsModifiers != 0u) && event->modifiers() == _urlHintsModifiers) { int nHotSpots = _filterChain->hotSpots().count(); int hintSelected = event->key() - 0x31; if (hintSelected >= 0 && hintSelected < 10 && hintSelected < nHotSpots) { if (_reverseUrlHints) { hintSelected = nHotSpots - hintSelected - 1; } _filterChain->hotSpots().at(hintSelected)->activate(); _showUrlHint = false; update(); return; } if (!_showUrlHint) { processFilters(); _showUrlHint = true; update(); } } _screenWindow->screen()->setCurrentTerminalDisplay(this); if (!_readOnly) { _actSel = 0; // Key stroke implies a screen update, so TerminalDisplay won't // know where the current selection is. if (_allowBlinkingCursor) { _blinkCursorTimer->start(); if (_cursorBlinking) { // if cursor is blinking(hidden), blink it again to show it blinkCursorEvent(); } Q_ASSERT(!_cursorBlinking); } } if (_searchBar->isVisible() && event->key() == Qt::Key_Escape) { _sessionController->searchClosed(); } emit keyPressedSignal(event); #ifndef QT_NO_ACCESSIBILITY if (!_readOnly) { QAccessibleTextCursorEvent textCursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX()); QAccessible::updateAccessibility(&textCursorEvent); } #endif event->accept(); } void TerminalDisplay::keyReleaseEvent(QKeyEvent *event) { if (_showUrlHint) { _showUrlHint = false; update(); } if (_readOnly) { event->accept(); return; } QWidget::keyReleaseEvent(event); } bool TerminalDisplay::handleShortcutOverrideEvent(QKeyEvent* keyEvent) { const int modifiers = keyEvent->modifiers(); // When a possible shortcut combination is pressed, // emit the overrideShortcutCheck() signal to allow the host // to decide whether the terminal should override it or not. if (modifiers != Qt::NoModifier) { int modifierCount = 0; unsigned int currentModifier = Qt::ShiftModifier; while (currentModifier <= Qt::KeypadModifier) { if ((modifiers & currentModifier) != 0u) { modifierCount++; } currentModifier <<= 1; } if (modifierCount < 2) { bool override = false; emit overrideShortcutCheck(keyEvent, override); if (override) { keyEvent->accept(); return true; } } } // Override any of the following shortcuts because // they are needed by the terminal int keyCode = keyEvent->key() | modifiers; switch (keyCode) { // list is taken from the QLineEdit::event() code case Qt::Key_Tab: case Qt::Key_Delete: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Backspace: case Qt::Key_Left: case Qt::Key_Right: case Qt::Key_Slash: case Qt::Key_Period: case Qt::Key_Space: keyEvent->accept(); return true; } return false; } bool TerminalDisplay::event(QEvent* event) { bool eventHandled = false; switch (event->type()) { case QEvent::ShortcutOverride: eventHandled = handleShortcutOverrideEvent(static_cast(event)); break; case QEvent::PaletteChange: case QEvent::ApplicationPaletteChange: onColorsChanged(); break; case QEvent::FocusOut: case QEvent::FocusIn: if(_screenWindow != nullptr) { // force a redraw on focusIn, fixes the // black screen bug when the view is focused // but doesn't redraws. _screenWindow->notifyOutputChanged(); } update(); break; default: break; } return eventHandled ? true : QWidget::event(event); } void TerminalDisplay::contextMenuEvent(QContextMenuEvent* event) { // the logic for the mouse case is within MousePressEvent() if (event->reason() != QContextMenuEvent::Mouse) { emit configureRequest(mapFromGlobal(QCursor::pos())); } } /* --------------------------------------------------------------------- */ /* */ /* Bell */ /* */ /* --------------------------------------------------------------------- */ void TerminalDisplay::setBellMode(int mode) { _bellMode = mode; } int TerminalDisplay::bellMode() const { return _bellMode; } void TerminalDisplay::bell(const QString& message) { if (_bellMasked) { return; } switch (_bellMode) { case Enum::SystemBeepBell: KNotification::beep(); break; case Enum::NotifyBell: // STABLE API: // Please note that these event names, "BellVisible" and "BellInvisible", // should not change and should be kept stable, because other applications // that use this code via KPart rely on these names for notifications. KNotification::event(hasFocus() ? QStringLiteral("BellVisible") : QStringLiteral("BellInvisible"), message, QPixmap(), this); break; case Enum::VisualBell: visualBell(); break; default: break; } // limit the rate at which bells can occur. // ...mainly for sound effects where rapid bells in sequence // produce a horrible noise. _bellMasked = true; QTimer::singleShot(500, this, [this]() { _bellMasked = false; }); } void TerminalDisplay::visualBell() { swapFGBGColors(); QTimer::singleShot(200, this, &Konsole::TerminalDisplay::swapFGBGColors); } void TerminalDisplay::swapFGBGColors() { // swap the default foreground & background color ColorEntry color = _colorTable[DEFAULT_BACK_COLOR]; _colorTable[DEFAULT_BACK_COLOR] = _colorTable[DEFAULT_FORE_COLOR]; _colorTable[DEFAULT_FORE_COLOR] = color; onColorsChanged(); } /* --------------------------------------------------------------------- */ /* */ /* Drag & Drop */ /* */ /* --------------------------------------------------------------------- */ void TerminalDisplay::dragEnterEvent(QDragEnterEvent* event) { // text/plain alone is enough for KDE-apps // text/uri-list is for supporting some non-KDE apps, such as thunar // and pcmanfm // That also applies in dropEvent() const auto mimeData = event->mimeData(); if ((!_readOnly) && (mimeData != nullptr) && (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list")))) { event->acceptProposedAction(); } } namespace { QString extractDroppedText(const QList& urls) { QString dropText; for (int i = 0 ; i < urls.count() ; i++) { KIO::StatJob* job = KIO::mostLocalUrl(urls[i], KIO::HideProgressInfo); if (!job->exec()) { continue; } const QUrl url = job->mostLocalUrl(); // in future it may be useful to be able to insert file names with drag-and-drop // without quoting them (this only affects paths with spaces in) dropText += KShell::quoteArg(url.isLocalFile() ? url.path() : url.url()); // Each filename(including the last) should be followed by one space. dropText += QLatin1Char(' '); } return dropText; } void setupCdToUrlAction(const QString& dropText, const QUrl& url, QList& additionalActions, TerminalDisplay *display) { KIO::StatJob* job = KIO::mostLocalUrl(url, KIO::HideProgressInfo); if (!job->exec()) { return; } const QUrl localUrl = job->mostLocalUrl(); if (!localUrl.isLocalFile()) { return; } const QFileInfo fileInfo(localUrl.path()); if (!fileInfo.isDir()) { return; } QAction* cdAction = new QAction(i18n("Change &Directory To"), display); const QByteArray triggerText = QString(QLatin1String(" cd ") + dropText + QLatin1Char('\n')).toLocal8Bit(); display->connect(cdAction, &QAction::triggered, display, [display, triggerText]{ emit display->sendStringToEmu(triggerText);} ); additionalActions.append(cdAction); } } void TerminalDisplay::dropEvent(QDropEvent* event) { if (_readOnly) { event->accept(); return; } const auto mimeData = event->mimeData(); if (mimeData == nullptr) { return; } auto urls = mimeData->urls(); QString dropText; if (!urls.isEmpty()) { dropText = extractDroppedText(urls); // If our target is local we will open a popup - otherwise the fallback kicks // in and the URLs will simply be pasted as text. if (!_dropUrlsAsText && (_sessionController != nullptr) && _sessionController->url().isLocalFile()) { // A standard popup with Copy, Move and Link as options - // plus an additional Paste option. QAction* pasteAction = new QAction(i18n("&Paste Location"), this); connect(pasteAction, &QAction::triggered, this, [this, dropText]{ emit sendStringToEmu(dropText.toLocal8Bit());} ); QList additionalActions; additionalActions.append(pasteAction); if (urls.count() == 1) { setupCdToUrlAction(dropText, urls.at(0), additionalActions, this); } QUrl target = QUrl::fromLocalFile(_sessionController->currentDir()); KIO::DropJob* job = KIO::drop(event, target); KJobWidgets::setWindow(job, this); job->setApplicationActions(additionalActions); return; } } else { dropText = mimeData->text(); } if (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list"))) { emit sendStringToEmu(dropText.toLocal8Bit()); } } void TerminalDisplay::doDrag() { const QMimeData *clipboardMimeData = QApplication::clipboard()->mimeData(QClipboard::Selection); if (clipboardMimeData == nullptr) { return; } auto mimeData = new QMimeData(); _dragInfo.state = diDragging; _dragInfo.dragObject = new QDrag(this); mimeData->setText(clipboardMimeData->text()); mimeData->setHtml(clipboardMimeData->html()); _dragInfo.dragObject->setMimeData(mimeData); _dragInfo.dragObject->exec(Qt::CopyAction); } void TerminalDisplay::setSessionController(SessionController* controller) { _sessionController = controller; _headerBar->finishHeaderSetup(controller); } SessionController* TerminalDisplay::sessionController() { return _sessionController; } IncrementalSearchBar *TerminalDisplay::searchBar() const { return _searchBar; } AutoScrollHandler::AutoScrollHandler(QWidget* parent) : QObject(parent) , _timerId(0) { parent->installEventFilter(this); } void AutoScrollHandler::timerEvent(QTimerEvent* event) { if (event->timerId() != _timerId) { return; } QMouseEvent mouseEvent(QEvent::MouseMove, widget()->mapFromGlobal(QCursor::pos()), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(widget(), &mouseEvent); } bool AutoScrollHandler::eventFilter(QObject* watched, QEvent* event) { Q_ASSERT(watched == parent()); Q_UNUSED(watched); switch (event->type()) { case QEvent::MouseMove: { auto* mouseEvent = static_cast(event); bool mouseInWidget = widget()->rect().contains(mouseEvent->pos()); if (mouseInWidget) { if (_timerId != 0) { killTimer(_timerId); } _timerId = 0; } else { if ((_timerId == 0) && ((mouseEvent->buttons() & Qt::LeftButton) != 0u)) { _timerId = startTimer(100); } } break; } case QEvent::MouseButtonRelease: { auto* mouseEvent = static_cast(event); if ((_timerId != 0) && ((mouseEvent->buttons() & ~Qt::LeftButton) != 0u)) { killTimer(_timerId); _timerId = 0; } break; } default: break; }; return false; } void TerminalDisplay::applyProfile(const Profile::Ptr &profile) { // load color scheme ColorEntry table[TABLE_COLORS]; _colorScheme = ViewManager::colorSchemeForProfile(profile); _colorScheme->getColorTable(table, randomSeed()); setColorTable(table); setOpacity(_colorScheme->opacity()); setWallpaper(_colorScheme->wallpaper()); // load font _antialiasText = profile->antiAliasFonts(); _boldIntense = profile->boldIntense(); _useFontLineCharacters = profile->useFontLineCharacters(); setVTFont(profile->font()); // set scroll-bar position setScrollBarPosition(Enum::ScrollBarPositionEnum(profile->property(Profile::ScrollBarPosition))); setScrollFullPage(profile->property(Profile::ScrollFullPage)); // show hint about terminal size after resizing _showTerminalSizeHint = profile->showTerminalSizeHint(); _dimWhenInactive = profile->dimWhenInactive(); // terminal features setBlinkingCursorEnabled(profile->blinkingCursorEnabled()); setBlinkingTextEnabled(profile->blinkingTextEnabled()); _tripleClickMode = Enum::TripleClickModeEnum(profile->property(Profile::TripleClickMode)); setAutoCopySelectedText(profile->autoCopySelectedText()); _ctrlRequiredForDrag = profile->property(Profile::CtrlRequiredForDrag); _dropUrlsAsText = profile->property(Profile::DropUrlsAsText); _bidiEnabled = profile->bidiRenderingEnabled(); setLineSpacing(uint(profile->lineSpacing())); _trimLeadingSpaces = profile->property(Profile::TrimLeadingSpacesInSelectedText); _trimTrailingSpaces = profile->property(Profile::TrimTrailingSpacesInSelectedText); _openLinksByDirectClick = profile->property(Profile::OpenLinksByDirectClickEnabled); _urlHintsModifiers = Qt::KeyboardModifiers(profile->property(Profile::UrlHintsModifiers)); _reverseUrlHints = profile->property(Profile::ReverseUrlHints); setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum(profile->property(Profile::MiddleClickPasteMode))); setCopyTextAsHTML(profile->property(Profile::CopyTextAsHTML)); // margin/center setMargin(profile->property(Profile::TerminalMargin)); setCenterContents(profile->property(Profile::TerminalCenter)); // cursor shape setKeyboardCursorShape(Enum::CursorShapeEnum(profile->property(Profile::CursorShape))); // cursor color // an invalid QColor is used to inform the view widget to // draw the cursor using the default color( matching the text) setKeyboardCursorColor(profile->useCustomCursorColor() ? profile->customCursorColor() : QColor()); // word characters setWordCharacters(profile->wordCharacters()); // bell mode setBellMode(profile->property(Profile::BellMode)); // mouse wheel zoom _mouseWheelZoom = profile->mouseWheelZoomEnabled(); setAlternateScrolling(profile->property(Profile::AlternateScrolling)); } diff --git a/src/ViewContainer.cpp b/src/ViewContainer.cpp index 172771bf..530cc763 100644 --- a/src/ViewContainer.cpp +++ b/src/ViewContainer.cpp @@ -1,639 +1,640 @@ /* This file is part of the Konsole Terminal. Copyright 2006-2008 Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "ViewContainer.h" #include "config-konsole.h" // Qt #include #include #include #include // KDE #include #include #include #include // Konsole #include "IncrementalSearchBar.h" #include "ViewProperties.h" #include "ProfileList.h" #include "ViewManager.h" #include "KonsoleSettings.h" #include "SessionController.h" #include "DetachableTabBar.h" #include "TerminalDisplay.h" #include "ViewSplitter.h" #include "MainWindow.h" #include "Session.h" // TODO Perhaps move everything which is Konsole-specific into different files using namespace Konsole; TabbedViewContainer::TabbedViewContainer(ViewManager *connectedViewManager, QWidget *parent) : QTabWidget(parent), _connectedViewManager(connectedViewManager), _newTabButton(new QToolButton(this)), _closeTabButton(new QToolButton(this)), _contextMenuTabIndex(-1), _navigationVisibility(ViewManager::NavigationVisibility::NavigationNotSet), _newTabBehavior(PutNewTabAtTheEnd) { setAcceptDrops(true); auto tabBarWidget = new DetachableTabBar(); setTabBar(tabBarWidget); setDocumentMode(true); setMovable(true); connect(tabBarWidget, &DetachableTabBar::moveTabToWindow, this, &TabbedViewContainer::moveTabToWindow); tabBar()->setContextMenuPolicy(Qt::CustomContextMenu); _newTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-new"))); _newTabButton->setAutoRaise(true); connect(_newTabButton, &QToolButton::clicked, this, &TabbedViewContainer::newViewRequest); _closeTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); _closeTabButton->setAutoRaise(true); connect(_closeTabButton, &QToolButton::clicked, this, [this]{ closeCurrentTab(); }); connect(tabBar(), &QTabBar::tabBarDoubleClicked, this, &Konsole::TabbedViewContainer::tabDoubleClicked); connect(tabBar(), &QTabBar::customContextMenuRequested, this, &Konsole::TabbedViewContainer::openTabContextMenu); connect(tabBarWidget, &DetachableTabBar::detachTab, this, [this](int idx) { emit detachTab(idx); }); connect(tabBarWidget, &DetachableTabBar::closeTab, this, &TabbedViewContainer::closeTerminalTab); connect(tabBarWidget, &DetachableTabBar::newTabRequest, this, [this]{ emit newViewRequest(); }); connect(this, &TabbedViewContainer::currentChanged, this, &TabbedViewContainer::currentTabChanged); // The context menu of tab bar _contextPopupMenu = new QMenu(tabBar()); connect(_contextPopupMenu, &QMenu::aboutToHide, this, [this]() { // Remove the read-only action when the popup closes for (auto &action : _contextPopupMenu->actions()) { if (action->objectName() == QLatin1String("view-readonly")) { _contextPopupMenu->removeAction(action); break; } } }); connect(tabBar(), &QTabBar::tabCloseRequested, this, &TabbedViewContainer::closeTerminalTab); auto detachAction = _contextPopupMenu->addAction( QIcon::fromTheme(QStringLiteral("tab-detach")), i18nc("@action:inmenu", "&Detach Tab"), this, [this] { emit detachTab(_contextMenuTabIndex); } ); detachAction->setObjectName(QStringLiteral("tab-detach")); auto editAction = _contextPopupMenu->addAction( QIcon::fromTheme(QStringLiteral("edit-rename")), i18nc("@action:inmenu", "&Rename Tab..."), this, [this]{ renameTab(_contextMenuTabIndex); } ); editAction->setObjectName(QStringLiteral("edit-rename")); auto closeAction = _contextPopupMenu->addAction( QIcon::fromTheme(QStringLiteral("tab-close")), i18nc("@action:inmenu", "Close Tab"), this, [this] { closeTerminalTab(_contextMenuTabIndex); } ); closeAction->setObjectName(QStringLiteral("tab-close")); auto profileMenu = new QMenu(this); auto profileList = new ProfileList(false, profileMenu); profileList->syncWidgetActions(profileMenu, true); connect(profileList, &Konsole::ProfileList::profileSelected, this, &TabbedViewContainer::newViewWithProfileRequest); _newTabButton->setMenu(profileMenu); konsoleConfigChanged(); connect(KonsoleSettings::self(), &KonsoleSettings::configChanged, this, &TabbedViewContainer::konsoleConfigChanged); } TabbedViewContainer::~TabbedViewContainer() { for(int i = 0, end = count(); i < end; i++) { auto view = widget(i); disconnect(view, &QWidget::destroyed, this, &Konsole::TabbedViewContainer::viewDestroyed); } } ViewSplitter *TabbedViewContainer::activeViewSplitter() { return viewSplitterAt(currentIndex()); } ViewSplitter *TabbedViewContainer::viewSplitterAt(int index) { return qobject_cast(widget(index)); } void TabbedViewContainer::moveTabToWindow(int index, QWidget *window) { auto splitter = viewSplitterAt(index); auto manager = window->findChild(); QHash sessionsMap = _connectedViewManager->forgetAll(splitter); - foreach(TerminalDisplay* terminal, splitter->findChildren()) { + const QList displays = splitter->findChildren(); + for (TerminalDisplay *terminal : displays) { manager->attachView(terminal, sessionsMap[terminal]); } auto container = manager->activeContainer(); container->addSplitter(splitter); auto controller = splitter->activeTerminalDisplay()->sessionController(); container->currentSessionControllerChanged(controller); forgetView(splitter); } void TabbedViewContainer::konsoleConfigChanged() { // don't show tabs if we are in KParts mode. // This is a hack, and this needs to be rewritten. // The container should not be part of the KParts, perhaps just the // TerminalDisplay should. // ASAN issue if using sessionController->isKonsolePart(), just // duplicate code for now if (qApp->applicationName() != QLatin1String("konsole")) { tabBar()->setVisible(false); } else { // if we start with --show-tabbar or --hide-tabbar we ignore the preferences. setTabBarAutoHide(KonsoleSettings::tabBarVisibility() == KonsoleSettings::EnumTabBarVisibility::ShowTabBarWhenNeeded); if (KonsoleSettings::tabBarVisibility() == KonsoleSettings::EnumTabBarVisibility::AlwaysShowTabBar) { tabBar()->setVisible(true); } else if (KonsoleSettings::tabBarVisibility() == KonsoleSettings::EnumTabBarVisibility::AlwaysHideTabBar) { tabBar()->setVisible(false); } } setTabPosition((QTabWidget::TabPosition) KonsoleSettings::tabBarPosition()); setCornerWidget(KonsoleSettings::newTabButton() ? _newTabButton : nullptr, Qt::TopLeftCorner); _newTabButton->setVisible(KonsoleSettings::newTabButton()); setCornerWidget(KonsoleSettings::closeTabButton() == 1 ? _closeTabButton : nullptr, Qt::TopRightCorner); _closeTabButton->setVisible(KonsoleSettings::closeTabButton() == 1); tabBar()->setTabsClosable(KonsoleSettings::closeTabButton() == 0); tabBar()->setExpanding(KonsoleSettings::expandTabWidth()); tabBar()->update(); if (KonsoleSettings::tabBarUseUserStyleSheet()) { setCssFromFile(KonsoleSettings::tabBarUserStyleSheetFile()); } else { setCss(); } } void TabbedViewContainer::setCss(const QString& styleSheet) { static const QString defaultCss = QStringLiteral("QTabWidget::tab-bar, QTabWidget::pane { margin: 0; }\n"); setStyleSheet(defaultCss + styleSheet); } void TabbedViewContainer::setCssFromFile(const QUrl &url) { // Let's only deal w/ local files for now if (!url.isLocalFile()) { setStyleSheet(KonsoleSettings::tabBarStyleSheet()); } QFile file(url.toLocalFile()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { setStyleSheet(KonsoleSettings::tabBarStyleSheet()); } QTextStream in(&file); setCss(in.readAll()); } void TabbedViewContainer::moveActiveView(MoveDirection direction) { if (count() < 2) { // return if only one view return; } const int currentIndex = indexOf(currentWidget()); int newIndex = direction == MoveViewLeft ? qMax(currentIndex - 1, 0) : qMin(currentIndex + 1, count() - 1); auto swappedWidget = viewSplitterAt(newIndex); auto swappedTitle = tabBar()->tabText(newIndex); auto swappedIcon = tabBar()->tabIcon(newIndex); auto currentWidget = viewSplitterAt(currentIndex); auto currentTitle = tabBar()->tabText(currentIndex); auto currentIcon = tabBar()->tabIcon(currentIndex); if (newIndex < currentIndex) { insertTab(newIndex, currentWidget, currentIcon, currentTitle); insertTab(currentIndex, swappedWidget, swappedIcon, swappedTitle); } else { insertTab(currentIndex, swappedWidget, swappedIcon, swappedTitle); insertTab(newIndex, currentWidget, currentIcon, currentTitle); } setCurrentIndex(newIndex); } void TabbedViewContainer::terminalDisplayDropped(TerminalDisplay *terminalDisplay) { if (terminalDisplay->sessionController()->parent() != connectedViewManager()) { // Terminal from another window - recreate SessionController for current ViewManager disconnectTerminalDisplay(terminalDisplay); Session* terminalSession = terminalDisplay->sessionController()->session(); terminalDisplay->sessionController()->deleteLater(); connectedViewManager()->attachView(terminalDisplay, terminalSession); connectTerminalDisplay(terminalDisplay); } } QSize TabbedViewContainer::sizeHint() const { // QTabWidget::sizeHint() contains some margins added by widgets // style, which were making the initial window size too big. const auto tabsSize = tabBar()->sizeHint(); const auto *leftWidget = cornerWidget(Qt::TopLeftCorner); const auto *rightWidget = cornerWidget(Qt::TopRightCorner); const auto leftSize = leftWidget ? leftWidget->sizeHint() : QSize(0, 0); const auto rightSize = rightWidget ? rightWidget->sizeHint() : QSize(0, 0); auto tabBarSize = QSize(0, 0); // isVisible() won't work; this is called when the window is not yet visible if (tabBar()->isVisibleTo(this)) { tabBarSize.setWidth(leftSize.width() + tabsSize.width() + rightSize.width()); tabBarSize.setHeight(qMax(tabsSize.height(), qMax(leftSize.height(), rightSize.height()))); } const auto terminalSize = currentWidget() ? currentWidget()->sizeHint() : QSize(0, 0); // width // ├──────────────────┤ // // ┌──────────────────┐ ┬ // │ │ │ // │ Terminal │ │ // │ │ │ height // ├───┬──────────┬───┤ │ ┬ // │ L │ Tabs │ R │ │ │ tab bar height // └───┴──────────┴───┘ ┴ ┴ // // L/R = left/right widget return {qMax(terminalSize.width(), tabBarSize.width()), tabBarSize.height() + terminalSize.height()}; } void TabbedViewContainer::addSplitter(ViewSplitter *viewSplitter, int index) { if (index == -1) { index = addTab(viewSplitter, QString()); } else { insertTab(index, viewSplitter, QString()); } connect(viewSplitter, &ViewSplitter::destroyed, this, &TabbedViewContainer::viewDestroyed); disconnect(viewSplitter, &ViewSplitter::terminalDisplayDropped, nullptr, nullptr); connect(viewSplitter, &ViewSplitter::terminalDisplayDropped, this, &TabbedViewContainer::terminalDisplayDropped); - auto terminalDisplays = viewSplitter->findChildren(); - foreach(TerminalDisplay* terminal, terminalDisplays) { + const auto terminalDisplays = viewSplitter->findChildren(); + for (TerminalDisplay *terminal : terminalDisplays) { connectTerminalDisplay(terminal); } if (terminalDisplays.count() > 0) { updateTitle(qobject_cast(terminalDisplays.at(0)->sessionController())); } setCurrentIndex(index); } void TabbedViewContainer::addView(TerminalDisplay *view) { auto viewSplitter = new ViewSplitter(); viewSplitter->addTerminalDisplay(view, Qt::Horizontal); auto item = view->sessionController(); int index = _newTabBehavior == PutNewTabAfterCurrentTab ? currentIndex() + 1 : -1; if (index == -1) { index = addTab(viewSplitter, item->icon(), item->title()); } else { insertTab(index, viewSplitter, item->icon(), item->title()); } connectTerminalDisplay(view); connect(viewSplitter, &ViewSplitter::destroyed, this, &TabbedViewContainer::viewDestroyed); connect(viewSplitter, &ViewSplitter::terminalDisplayDropped, this, &TabbedViewContainer::terminalDisplayDropped); setCurrentIndex(index); emit viewAdded(view); } void TabbedViewContainer::splitView(TerminalDisplay *view, Qt::Orientation orientation) { auto viewSplitter = qobject_cast(currentWidget()); viewSplitter->addTerminalDisplay(view, orientation); connectTerminalDisplay(view); } void TabbedViewContainer::connectTerminalDisplay(TerminalDisplay *display) { auto item = display->sessionController(); connect(item, &Konsole::SessionController::focused, this, &Konsole::TabbedViewContainer::currentSessionControllerChanged); connect(item, &Konsole::ViewProperties::titleChanged, this, &Konsole::TabbedViewContainer::updateTitle); connect(item, &Konsole::ViewProperties::iconChanged, this, &Konsole::TabbedViewContainer::updateIcon); connect(item, &Konsole::ViewProperties::activity, this, &Konsole::TabbedViewContainer::updateActivity); } void TabbedViewContainer::disconnectTerminalDisplay(TerminalDisplay *display) { auto item = display->sessionController(); disconnect(item, &Konsole::SessionController::focused, this, &Konsole::TabbedViewContainer::currentSessionControllerChanged); disconnect(item, &Konsole::ViewProperties::titleChanged, this, &Konsole::TabbedViewContainer::updateTitle); disconnect(item, &Konsole::ViewProperties::iconChanged, this, &Konsole::TabbedViewContainer::updateIcon); disconnect(item, &Konsole::ViewProperties::activity, this, &Konsole::TabbedViewContainer::updateActivity); } void TabbedViewContainer::viewDestroyed(QObject *view) { auto widget = static_cast(view); const auto idx = indexOf(widget); removeTab(idx); forgetView(widget); } void TabbedViewContainer::forgetView(ViewSplitter *view) { Q_UNUSED(view); if (count() == 0) { emit empty(this); } } void TabbedViewContainer::activateNextView() { QWidget *active = currentWidget(); int index = indexOf(active); setCurrentIndex(index == count() - 1 ? 0 : index + 1); } void TabbedViewContainer::activateLastView() { setCurrentIndex(count() - 1); } void TabbedViewContainer::activatePreviousView() { QWidget *active = currentWidget(); int index = indexOf(active); setCurrentIndex(index == 0 ? count() - 1 : index - 1); } void TabbedViewContainer::keyReleaseEvent(QKeyEvent* event) { if (event->modifiers() == Qt::NoModifier) { _connectedViewManager->updateTerminalDisplayHistory(); } } void TabbedViewContainer::closeCurrentTab() { if (currentIndex() != -1) { closeTerminalTab(currentIndex()); } } void TabbedViewContainer::tabDoubleClicked(int index) { if (index >= 0) { renameTab(index); } else { emit newViewRequest(); } } void TabbedViewContainer::renameTab(int index) { if (index != -1) { setCurrentIndex(index); viewSplitterAt(index) -> activeTerminalDisplay() -> sessionController() -> rename(); } } void TabbedViewContainer::openTabContextMenu(const QPoint &point) { if (point.isNull()) { return; } _contextMenuTabIndex = tabBar()->tabAt(point); if (_contextMenuTabIndex < 0) { return; } //TODO: add a countChanged signal so we can remove this for. // Detaching in mac causes crashes. for(auto action : _contextPopupMenu->actions()) { if (action->objectName() == QLatin1String("tab-detach")) { action->setEnabled(count() > 1); } } /* This needs to nove away fro the tab or to lock every thing inside of it. * for now, disable. * */ // // Add the read-only action #if 0 auto sessionController = terminalAt(_contextMenuTabIndex)->sessionController(); if (sessionController != nullptr) { auto collection = sessionController->actionCollection(); auto readonlyAction = collection->action(QStringLiteral("view-readonly")); if (readonlyAction != nullptr) { const auto readonlyActions = _contextPopupMenu->actions(); _contextPopupMenu->insertAction(readonlyActions.last(), readonlyAction); } // Disable tab rename for (auto &action : _contextPopupMenu->actions()) { if (action->objectName() == QLatin1String("edit-rename")) { action->setEnabled(!sessionController->isReadOnly()); break; } } } #endif _contextPopupMenu->exec(tabBar()->mapToGlobal(point)); } void TabbedViewContainer::currentTabChanged(int index) { if (index != -1) { auto splitview = qobject_cast(widget(index)); auto view = splitview->activeTerminalDisplay(); emit activeViewChanged(view); setTabActivity(index, false); } else { deleteLater(); } } void TabbedViewContainer::wheelScrolled(int delta) { if (delta < 0) { activateNextView(); } else { activatePreviousView(); } } void TabbedViewContainer::setTabActivity(int index, bool activity) { const QPalette &palette = tabBar()->palette(); KColorScheme colorScheme(palette.currentColorGroup()); const QColor colorSchemeActive = colorScheme.foreground(KColorScheme::ActiveText).color(); const QColor normalColor = palette.text().color(); const QColor activityColor = KColorUtils::mix(normalColor, colorSchemeActive); QColor color = activity ? activityColor : QColor(); if (color != tabBar()->tabTextColor(index)) { tabBar()->setTabTextColor(index, color); } } void TabbedViewContainer::updateActivity(ViewProperties *item) { auto controller = qobject_cast(item); auto topLevelSplitter = qobject_cast(controller->view()->parentWidget())->getToplevelSplitter(); const int index = indexOf(topLevelSplitter); if (index != currentIndex()) { setTabActivity(index, true); } } void TabbedViewContainer::currentSessionControllerChanged(SessionController *controller) { updateTitle(qobject_cast(controller)); } void TabbedViewContainer::updateTitle(ViewProperties *item) { auto controller = qobject_cast(item); auto topLevelSplitter = qobject_cast(controller->view()->parentWidget())->getToplevelSplitter(); const int index = indexOf(topLevelSplitter); QString tabText = item->title(); setTabToolTip(index, tabText); // To avoid having & replaced with _ (shortcut indicator) tabText.replace(QLatin1Char('&'), QLatin1String("&&")); setTabText(index, tabText); } void TabbedViewContainer::updateIcon(ViewProperties *item) { auto controller = qobject_cast(item); const int index = indexOf(controller->view()); setTabIcon(index, item->icon()); } void TabbedViewContainer::closeTerminalTab(int idx) { //TODO: This for should probably go to the ViewSplitter for (auto terminal : viewSplitterAt(idx)->findChildren()) { terminal->sessionController()->closeSession(); } } ViewManager *TabbedViewContainer::connectedViewManager() { return _connectedViewManager; } void TabbedViewContainer::setNavigationVisibility(ViewManager::NavigationVisibility navigationVisibility) { if (navigationVisibility == ViewManager::NavigationNotSet) { return; } setTabBarAutoHide(navigationVisibility == ViewManager::ShowNavigationAsNeeded); if (navigationVisibility == ViewManager::AlwaysShowNavigation) { tabBar()->setVisible(true); } else if (navigationVisibility == ViewManager::AlwaysHideNavigation) { tabBar()->setVisible(false); } } void TabbedViewContainer::toggleMaximizeCurrentTerminal() { if (auto *terminal = qobject_cast(sender())) { terminal->setFocus(Qt::FocusReason::OtherFocusReason); } activeViewSplitter()->toggleMaximizeCurrentTerminal(); } void TabbedViewContainer::moveTabLeft() { if (currentIndex() == 0) { return; } tabBar()->moveTab(currentIndex(), currentIndex() -1); } void TabbedViewContainer::moveTabRight() { if (currentIndex() == count() -1) { return; } tabBar()->moveTab(currentIndex(), currentIndex() + 1); } void TabbedViewContainer::setNavigationBehavior(int behavior) { _newTabBehavior = static_cast(behavior); } diff --git a/src/ViewManager.cpp b/src/ViewManager.cpp index 3efbc3ec..2f04cdad 100644 --- a/src/ViewManager.cpp +++ b/src/ViewManager.cpp @@ -1,1144 +1,1146 @@ /* Copyright 2006-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "ViewManager.h" #include "config-konsole.h" // Qt #include #include // KDE #include #include #include // Konsole #include #include "ColorScheme.h" #include "ColorSchemeManager.h" #include "Session.h" #include "TerminalDisplay.h" #include "SessionController.h" #include "SessionManager.h" #include "ProfileManager.h" #include "ViewSplitter.h" #include "Enumeration.h" #include "ViewContainer.h" using namespace Konsole; int ViewManager::lastManagerId = 0; ViewManager::ViewManager(QObject *parent, KActionCollection *collection) : QObject(parent), _viewContainer(nullptr), _pluggedController(nullptr), _sessionMap(QHash()), _actionCollection(collection), _navigationMethod(NoNavigation), _navigationVisibility(NavigationNotSet), _managerId(0), _terminalDisplayHistoryIndex(-1) { _viewContainer = createContainer(); // setup actions which are related to the views setupActions(); /* TODO: Reconnect // emit a signal when all of the views held by this view manager are destroyed */ connect(_viewContainer.data(), &Konsole::TabbedViewContainer::empty, this, &Konsole::ViewManager::empty); // listen for profile changes connect(ProfileManager::instance(), &Konsole::ProfileManager::profileChanged, this, &Konsole::ViewManager::profileChanged); connect(SessionManager::instance(), &Konsole::SessionManager::sessionUpdated, this, &Konsole::ViewManager::updateViewsForSession); //prepare DBus communication new WindowAdaptor(this); _managerId = ++lastManagerId; QDBusConnection::sessionBus().registerObject(QLatin1String("/Windows/") + QString::number(_managerId), this); } ViewManager::~ViewManager() = default; int ViewManager::managerId() const { return _managerId; } QWidget *ViewManager::activeView() const { return _viewContainer->currentWidget(); } QWidget *ViewManager::widget() const { return _viewContainer; } void ViewManager::setupActions() { Q_ASSERT(_actionCollection); if (_actionCollection == nullptr) { return; } KActionCollection *collection = _actionCollection; // Let's reuse the pointer, no need not to. auto *action = new QAction(this); action->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right"))); action->setText(i18nc("@action:inmenu", "Split View Left/Right")); connect(action, &QAction::triggered, this, &ViewManager::splitLeftRight); collection->addAction(QStringLiteral("split-view-left-right"), action); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::Key_ParenLeft); action = new QAction(this); action->setIcon(QIcon::fromTheme(QStringLiteral("view-split-top-bottom"))); action->setText(i18nc("@action:inmenu", "Split View Top/Bottom")); connect(action, &QAction::triggered, this, &ViewManager::splitTopBottom); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::Key_ParenRight); collection->addAction(QStringLiteral("split-view-top-bottom"), action); action = new QAction(this); action->setText(i18nc("@action:inmenu", "Expand View")); action->setEnabled(false); connect(action, &QAction::triggered, this, &ViewManager::expandActiveContainer); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_BracketRight); collection->addAction(QStringLiteral("expand-active-view"), action); _multiSplitterOnlyActions << action; action = new QAction(this); action->setText(i18nc("@action:inmenu", "Shrink View")); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_BracketLeft); action->setEnabled(false); collection->addAction(QStringLiteral("shrink-active-view"), action); connect(action, &QAction::triggered, this, &ViewManager::shrinkActiveContainer); _multiSplitterOnlyActions << action; action = collection->addAction(QStringLiteral("detach-view")); action->setEnabled(true); action->setIcon(QIcon::fromTheme(QStringLiteral("tab-detach"))); action->setText(i18nc("@action:inmenu", "Detach Current &View")); connect(action, &QAction::triggered, this, &ViewManager::detachActiveView); _multiSplitterOnlyActions << action; // Ctrl+Shift+D is not used as a shortcut by default because it is too close // to Ctrl+D - which will terminate the session in many cases collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_H); action = collection->addAction(QStringLiteral("detach-tab")); action->setEnabled(true); action->setIcon(QIcon::fromTheme(QStringLiteral("tab-detach"))); action->setText(i18nc("@action:inmenu", "Detach Current &Tab")); connect(action, &QAction::triggered, this, &ViewManager::detachActiveTab); _multiTabOnlyActions << action; // Ctrl+Shift+D is not used as a shortcut by default because it is too close // to Ctrl+D - which will terminate the session in many cases collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_L); // keyboard shortcut only actions action = new QAction(i18nc("@action Shortcut entry", "Next Tab"), this); const QList nextViewActionKeys{Qt::SHIFT + Qt::Key_Right, Qt::CTRL + Qt::Key_PageDown}; collection->setDefaultShortcuts(action, nextViewActionKeys); collection->addAction(QStringLiteral("next-tab"), action); connect(action, &QAction::triggered, this, &ViewManager::nextView); _multiTabOnlyActions << action; // _viewSplitter->addAction(nextViewAction); action = new QAction(i18nc("@action Shortcut entry", "Previous Tab"), this); const QList previousViewActionKeys{Qt::SHIFT + Qt::Key_Left, Qt::CTRL + Qt::Key_PageUp}; collection->setDefaultShortcuts(action, previousViewActionKeys); collection->addAction(QStringLiteral("previous-tab"), action); connect(action, &QAction::triggered, this, &ViewManager::previousView); _multiTabOnlyActions << action; // _viewSplitter->addAction(previousViewAction); action = new QAction(i18nc("@action Shortcut entry", "Focus Above Terminal"), this); connect(action, &QAction::triggered, this, &ViewManager::focusUp); collection->addAction(QStringLiteral("focus-view-above"), action); collection->setDefaultShortcut(action, Qt::SHIFT + Qt::CTRL + Qt::Key_Up); _viewContainer->addAction(action); _multiSplitterOnlyActions << action; action = new QAction(i18nc("@action Shortcut entry", "Focus Below Terminal"), this); collection->setDefaultShortcut(action, Qt::SHIFT + Qt::CTRL + Qt::Key_Down); collection->addAction(QStringLiteral("focus-view-below"), action); connect(action, &QAction::triggered, this, &ViewManager::focusDown); _multiSplitterOnlyActions << action; _viewContainer->addAction(action); action = new QAction(i18nc("@action Shortcut entry", "Focus Left Terminal"), this); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Konsole::LEFT); connect(action, &QAction::triggered, this, &ViewManager::focusLeft); collection->addAction(QStringLiteral("focus-view-left"), action); _multiSplitterOnlyActions << action; action = new QAction(i18nc("@action Shortcut entry", "Focus Right Terminal"), this); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Konsole::RIGHT); connect(action, &QAction::triggered, this, &ViewManager::focusRight); collection->addAction(QStringLiteral("focus-view-right"), action); _multiSplitterOnlyActions << action; action = new QAction(i18nc("@action Shortcut entry", "Switch to Last Tab"), this); connect(action, &QAction::triggered, this, &ViewManager::lastView); collection->addAction(QStringLiteral("last-tab"), action); _multiTabOnlyActions << action; action = new QAction(i18nc("@action Shortcut entry", "Last Used Tabs"), this); connect(action, &QAction::triggered, this, &ViewManager::lastUsedView); collection->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Tab); collection->addAction(QStringLiteral("last-used-tab"), action); action = new QAction(i18nc("@action Shortcut entry", "Toggle Between Two Tabs"), this); connect(action, &QAction::triggered, this, &Konsole::ViewManager::toggleTwoViews); collection->addAction(QStringLiteral("toggle-two-tabs"), action); _multiTabOnlyActions << action; action = new QAction(i18nc("@action Shortcut entry", "Last Used Tabs (Reverse)"), this); collection->addAction(QStringLiteral("last-used-tab-reverse"), action); collection->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_Tab); connect(action, &QAction::triggered, this, &ViewManager::lastUsedViewReverse); action = new QAction(i18nc("@action Shortcut entry", "Maximize current Terminal"), this); collection->addAction(QStringLiteral("maximize-current-terminal"), action); collection->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_E); connect(action, &QAction::triggered, _viewContainer, &TabbedViewContainer::toggleMaximizeCurrentTerminal); _multiSplitterOnlyActions << action; _viewContainer->addAction(action); action = new QAction(i18nc("@action Shortcut entry", "Move tab to the right"), this); collection->addAction(QStringLiteral("move-tab-to-right"), action); collection->setDefaultShortcut(action, Qt::CTRL + Qt::ALT + Qt::Key_Right); connect(action, &QAction::triggered, _viewContainer, &TabbedViewContainer::moveTabRight); _viewContainer->addAction(action); action = new QAction(i18nc("@action Shortcut entry", "Move tab to the left"), this); collection->addAction(QStringLiteral("move-tab-to-left"), action); collection->setDefaultShortcut(action, Qt::CTRL + Qt::ALT + Qt::Key_Left); connect(action, &QAction::triggered, _viewContainer, &TabbedViewContainer::moveTabLeft); _viewContainer->addAction(action); // _viewSplitter->addAction(lastUsedViewReverseAction); const int SWITCH_TO_TAB_COUNT = 19; for (int i = 0; i < SWITCH_TO_TAB_COUNT; i++) { action = new QAction(i18nc("@action Shortcut entry", "Switch to Tab %1", i + 1), this); connect(action, &QAction::triggered, this, [this, i]() { switchToView(i); }); collection->addAction(QStringLiteral("switch-to-tab-%1").arg(i), action); } connect(_viewContainer, &TabbedViewContainer::viewAdded, this, &ViewManager::toggleActionsBasedOnState); connect(_viewContainer, &TabbedViewContainer::viewRemoved, this, &ViewManager::toggleActionsBasedOnState); connect(_viewContainer, &QTabWidget::currentChanged, this, &ViewManager::toggleActionsBasedOnState); toggleActionsBasedOnState(); } void ViewManager::toggleActionsBasedOnState() { const int count = _viewContainer->count(); - foreach(QAction *tabOnlyAction, _multiTabOnlyActions) { + for (QAction *tabOnlyAction : qAsConst(_multiTabOnlyActions)) { tabOnlyAction->setEnabled(count > 1); } if ((_viewContainer != nullptr) && (_viewContainer->activeViewSplitter() != nullptr)) { const int splitCount = _viewContainer ->activeViewSplitter() ->getToplevelSplitter() ->findChildren() .count(); - foreach (QAction *action, _multiSplitterOnlyActions) { + for (QAction *action : qAsConst(_multiSplitterOnlyActions)) { action->setEnabled(splitCount > 1); } } } void ViewManager::switchToView(int index) { _viewContainer->setCurrentIndex(index); } void ViewManager::switchToTerminalDisplay(Konsole::TerminalDisplay* terminalDisplay) { auto splitter = qobject_cast(terminalDisplay->parentWidget()); auto toplevelSplitter = splitter->getToplevelSplitter(); // Focus the TermialDisplay terminalDisplay->setFocus(); if (_viewContainer->currentWidget() != toplevelSplitter) { // Focus the tab switchToView(_viewContainer->indexOf(toplevelSplitter)); } } void ViewManager::focusUp() { _viewContainer->activeViewSplitter()->focusUp(); } void ViewManager::focusDown() { _viewContainer->activeViewSplitter()->focusDown(); } void ViewManager::focusLeft() { _viewContainer->activeViewSplitter()->focusLeft(); } void ViewManager::focusRight() { _viewContainer->activeViewSplitter()->focusRight(); } void ViewManager::moveActiveViewLeft() { _viewContainer->moveActiveView(TabbedViewContainer::MoveViewLeft); } void ViewManager::moveActiveViewRight() { _viewContainer->moveActiveView(TabbedViewContainer::MoveViewRight); } void ViewManager::nextContainer() { // _viewSplitter->activateNextContainer(); } void ViewManager::nextView() { _viewContainer->activateNextView(); } void ViewManager::previousView() { _viewContainer->activatePreviousView(); } void ViewManager::lastView() { _viewContainer->activateLastView(); } void ViewManager::activateLastUsedView(bool reverse) { if (_terminalDisplayHistory.count() <= 1) { return; } if (_terminalDisplayHistoryIndex == -1) { _terminalDisplayHistoryIndex = reverse ? _terminalDisplayHistory.count() - 1 : 1; } else if (reverse) { if (_terminalDisplayHistoryIndex == 0) { _terminalDisplayHistoryIndex = _terminalDisplayHistory.count() - 1; } else { _terminalDisplayHistoryIndex--; } } else { if (_terminalDisplayHistoryIndex >= _terminalDisplayHistory.count() - 1) { _terminalDisplayHistoryIndex = 0; } else { _terminalDisplayHistoryIndex++; } } switchToTerminalDisplay(_terminalDisplayHistory[_terminalDisplayHistoryIndex]); } void ViewManager::lastUsedView() { activateLastUsedView(false); } void ViewManager::lastUsedViewReverse() { activateLastUsedView(true); } void ViewManager::toggleTwoViews() { if (_terminalDisplayHistory.count() <= 1) { return; } switchToTerminalDisplay(_terminalDisplayHistory.at(1)); } void ViewManager::detachActiveView() { // find the currently active view and remove it from its container if ((_viewContainer->findChildren()).count() > 1) { auto activeSplitter = _viewContainer->activeViewSplitter(); auto terminal = activeSplitter->activeTerminalDisplay(); auto newSplitter = new ViewSplitter(); newSplitter->addTerminalDisplay(terminal, Qt::Horizontal); QHash detachedSessions = forgetAll(newSplitter); emit terminalsDetached(newSplitter, detachedSessions); focusAnotherTerminal(activeSplitter->getToplevelSplitter()); toggleActionsBasedOnState(); } } void ViewManager::detachActiveTab() { const int currentIdx = _viewContainer->currentIndex(); detachTab(currentIdx); } void ViewManager::detachTab(int tabIdx) { ViewSplitter* splitter = _viewContainer->viewSplitterAt(tabIdx); QHash detachedSessions = forgetAll(_viewContainer->viewSplitterAt(tabIdx)); emit terminalsDetached(splitter, detachedSessions); } QHash ViewManager::forgetAll(ViewSplitter* splitter) { splitter->setParent(nullptr); QHash detachedSessions; - foreach(TerminalDisplay* terminal, splitter->findChildren()) { + const QList displays = splitter->findChildren(); + for (TerminalDisplay *terminal : displays) { Session* session = forgetTerminal(terminal); detachedSessions[terminal] = session; } return detachedSessions; } Session* ViewManager::forgetTerminal(TerminalDisplay* terminal) { disconnect(terminal, &TerminalDisplay::requestToggleExpansion, nullptr, nullptr); removeController(terminal->sessionController()); auto session = _sessionMap.take(terminal); if (session != nullptr) { disconnect(session, &Konsole::Session::finished, this, &Konsole::ViewManager::sessionFinished); } _viewContainer->disconnectTerminalDisplay(terminal); updateTerminalDisplayHistory(terminal, true); return session; } Session* ViewManager::createSession(const Profile::Ptr &profile, const QString &directory) { Session *session = SessionManager::instance()->createSession(profile); Q_ASSERT(session); if (!directory.isEmpty()) { session->setInitialWorkingDirectory(directory); } session->addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_WINDOW=/Windows/%1").arg(managerId())); return session; } void ViewManager::sessionFinished() { // if this slot is called after the view manager's main widget // has been destroyed, do nothing if (_viewContainer.isNull()) { return; } auto *session = qobject_cast(sender()); Q_ASSERT(session); auto view = _sessionMap.key(session); _sessionMap.remove(view); if (SessionManager::instance()->isClosingAllSessions()){ return; } // Before deleting the view, let's unmaximize if it's maximized. auto *splitter = qobject_cast(view->parentWidget()); if (splitter == nullptr) { return; } auto *toplevelSplitter = splitter->getToplevelSplitter(); toplevelSplitter->handleMinimizeMaximize(false); view->deleteLater(); // Only remove the controller from factory() if it's actually controlling // the session from the sender. // This fixes BUG: 348478 - messed up menus after a detached tab is closed if ((!_pluggedController.isNull()) && (_pluggedController->session() == session)) { // This is needed to remove this controller from factory() in // order to prevent BUG: 185466 - disappearing menu popup emit unplugController(_pluggedController); } if (!_sessionMap.empty()) { updateTerminalDisplayHistory(view, true); focusAnotherTerminal(toplevelSplitter); toggleActionsBasedOnState(); } } void ViewManager::focusAnotherTerminal(ViewSplitter *toplevelSplitter) { auto tabTterminalDisplays = toplevelSplitter->findChildren(); if (tabTterminalDisplays.count() == 0) { return; } if (tabTterminalDisplays.count() > 1) { // Give focus to the last used terminal in this tab for (auto *historyItem : _terminalDisplayHistory) { for (auto *terminalDisplay : tabTterminalDisplays) { if (terminalDisplay == historyItem) { terminalDisplay->setFocus(Qt::OtherFocusReason); return; } } } } else if (_terminalDisplayHistory.count() >= 1) { // Give focus to the last used terminal tab switchToTerminalDisplay(_terminalDisplayHistory[0]); } } void ViewManager::viewActivated(TerminalDisplay *view) { Q_ASSERT(view != nullptr); // focus the activated view, this will cause the SessionController // to notify the world that the view has been focused and the appropriate UI // actions will be plugged in. view->setFocus(Qt::OtherFocusReason); } void ViewManager::splitLeftRight() { splitView(Qt::Horizontal); } void ViewManager::splitTopBottom() { splitView(Qt::Vertical); } void ViewManager::splitView(Qt::Orientation orientation) { int currentSessionId = currentSession(); // At least one display/session exists if we are splitting Q_ASSERT(currentSessionId >= 0); Session *activeSession = SessionManager::instance()->idToSession(currentSessionId); Q_ASSERT(activeSession); auto profile = SessionManager::instance()->sessionProfile(activeSession); const QString directory = profile->startInCurrentSessionDir() ? activeSession->currentWorkingDirectory() : QString(); auto *session = createSession(profile, directory); auto terminalDisplay = createView(session); _viewContainer->splitView(terminalDisplay, orientation); toggleActionsBasedOnState(); // focus the new container terminalDisplay->setFocus(); } void ViewManager::expandActiveContainer() { _viewContainer->activeViewSplitter()->adjustActiveTerminalDisplaySize(10); } void ViewManager::shrinkActiveContainer() { _viewContainer->activeViewSplitter()->adjustActiveTerminalDisplaySize(-10); } SessionController *ViewManager::createController(Session *session, TerminalDisplay *view) { // create a new controller for the session, and ensure that this view manager // is notified when the view gains the focus auto controller = new SessionController(session, view, this); connect(controller, &Konsole::SessionController::focused, this, &Konsole::ViewManager::controllerChanged); connect(session, &Konsole::Session::destroyed, controller, &Konsole::SessionController::deleteLater); connect(session, &Konsole::Session::primaryScreenInUse, controller, &Konsole::SessionController::setupPrimaryScreenSpecificActions); connect(session, &Konsole::Session::selectionChanged, controller, &Konsole::SessionController::selectionChanged); connect(view, &Konsole::TerminalDisplay::destroyed, controller, &Konsole::SessionController::deleteLater); // if this is the first controller created then set it as the active controller if (_pluggedController.isNull()) { controllerChanged(controller); } return controller; } // should this be handed by ViewManager::unplugController signal void ViewManager::removeController(SessionController* controller) { if (_pluggedController == controller) { _pluggedController.clear(); } controller->deleteLater(); } void ViewManager::controllerChanged(SessionController *controller) { if (controller == _pluggedController) { return; } _viewContainer->setFocusProxy(controller->view()); updateTerminalDisplayHistory(controller->view()); _pluggedController = controller; emit activeViewChanged(controller); } SessionController *ViewManager::activeViewController() const { return _pluggedController; } void ViewManager::attachView(TerminalDisplay *terminal, Session *session) { connect(session, &Konsole::Session::finished, this, &Konsole::ViewManager::sessionFinished, Qt::UniqueConnection); // Disconnect from the other viewcontainer. disconnect(terminal, &TerminalDisplay::requestToggleExpansion, nullptr, nullptr); // reconnect on this container. connect(terminal, &TerminalDisplay::requestToggleExpansion, _viewContainer, &TabbedViewContainer::toggleMaximizeCurrentTerminal, Qt::UniqueConnection); _sessionMap[terminal] = session; createController(session, terminal); toggleActionsBasedOnState(); _terminalDisplayHistory.append(terminal); } TerminalDisplay *ViewManager::createView(Session *session) { // notify this view manager when the session finishes so that its view // can be deleted // // Use Qt::UniqueConnection to avoid duplicate connection connect(session, &Konsole::Session::finished, this, &Konsole::ViewManager::sessionFinished, Qt::UniqueConnection); TerminalDisplay *display = createTerminalDisplay(session); const Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); applyProfileToView(display, profile); // set initial size const QSize &preferredSize = session->preferredSize(); display->setSize(preferredSize.width(), preferredSize.height()); createController(session, display); _sessionMap[display] = session; session->addView(display); _terminalDisplayHistory.append(display); // tell the session whether it has a light or dark background session->setDarkBackground(colorSchemeForProfile(profile)->hasDarkBackground()); display->setFocus(Qt::OtherFocusReason); // updateDetachViewState(); return display; } TabbedViewContainer *ViewManager::createContainer() { auto *container = new TabbedViewContainer(this, nullptr); container->setNavigationVisibility(_navigationVisibility); connect(container, &TabbedViewContainer::detachTab, this, &ViewManager::detachTab); // connect signals and slots connect(container, &Konsole::TabbedViewContainer::viewAdded, this, [this, container]() { containerViewsChanged(container); }); connect(container, &Konsole::TabbedViewContainer::viewRemoved, this, [this, container]() { containerViewsChanged(container); }); connect(container, &TabbedViewContainer::newViewRequest, this, &ViewManager::newViewRequest); connect(container, &Konsole::TabbedViewContainer::newViewWithProfileRequest, this, &Konsole::ViewManager::newViewWithProfileRequest); connect(container, &Konsole::TabbedViewContainer::viewRemoved, this, &Konsole::ViewManager::viewDestroyed); connect(container, &Konsole::TabbedViewContainer::activeViewChanged, this, &Konsole::ViewManager::viewActivated); return container; } void ViewManager::setNavigationMethod(NavigationMethod method) { Q_ASSERT(_actionCollection); if (_actionCollection == nullptr) { return; } KActionCollection *collection = _actionCollection; _navigationMethod = method; // FIXME: The following disables certain actions for the KPart that it // doesn't actually have a use for, to avoid polluting the action/shortcut // namespace of an application using the KPart (otherwise, a shortcut may // be in use twice, and the user gets to see an "ambiguous shortcut over- // load" error dialog). However, this approach sucks - it's the inverse of // what it should be. Rather than disabling actions not used by the KPart, // a method should be devised to only enable those that are used, perhaps // by using a separate action collection. const bool enable = (method != NoNavigation); auto enableAction = [&enable, &collection](const QString& actionName) { auto *action = collection->action(actionName); if (action != nullptr) { action->setEnabled(enable); } }; enableAction(QStringLiteral("next-view")); enableAction(QStringLiteral("previous-view")); enableAction(QStringLiteral("last-tab")); enableAction(QStringLiteral("last-used-tab")); enableAction(QStringLiteral("last-used-tab-reverse")); enableAction(QStringLiteral("split-view-left-right")); enableAction(QStringLiteral("split-view-top-bottom")); enableAction(QStringLiteral("rename-session")); enableAction(QStringLiteral("move-view-left")); enableAction(QStringLiteral("move-view-right")); } ViewManager::NavigationMethod ViewManager::navigationMethod() const { return _navigationMethod; } void ViewManager::containerViewsChanged(TabbedViewContainer *container) { Q_UNUSED(container); // TODO: Verify that this is right. emit viewPropertiesChanged(viewProperties()); } void ViewManager::viewDestroyed(QWidget *view) { // Note: the received QWidget has already been destroyed, so // using dynamic_cast<> or qobject_cast<> does not work here // We only need the pointer address to look it up below auto *display = reinterpret_cast(view); // 1. detach view from session // 2. if the session has no views left, close it Session *session = _sessionMap[ display ]; _sessionMap.remove(display); if (session != nullptr) { if (session->views().count() == 0) { session->close(); } } //we only update the focus if the splitter is still alive toggleActionsBasedOnState(); // The below causes the menus to be messed up // Only happens when using the tab bar close button // if (_pluggedController) // emit unplugController(_pluggedController); } TerminalDisplay *ViewManager::createTerminalDisplay(Session *session) { auto display = new TerminalDisplay(nullptr); display->setRandomSeed(session->sessionId() | (qApp->applicationPid() << 10)); connect(display, &TerminalDisplay::requestToggleExpansion, _viewContainer, &TabbedViewContainer::toggleMaximizeCurrentTerminal); return display; } const ColorScheme *ViewManager::colorSchemeForProfile(const Profile::Ptr &profile) { const ColorScheme *colorScheme = ColorSchemeManager::instance()-> findColorScheme(profile->colorScheme()); if (colorScheme == nullptr) { colorScheme = ColorSchemeManager::instance()->defaultColorScheme(); } Q_ASSERT(colorScheme); return colorScheme; } bool ViewManager::profileHasBlurEnabled(const Profile::Ptr &profile) { return colorSchemeForProfile(profile)->blur(); } void ViewManager::applyProfileToView(TerminalDisplay *view, const Profile::Ptr &profile) { Q_ASSERT(profile); view->applyProfile(profile); emit updateWindowIcon(); emit blurSettingChanged(view->colorScheme()->blur()); } void ViewManager::updateViewsForSession(Session *session) { const Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); const QList sessionMapKeys = _sessionMap.keys(session); - foreach (TerminalDisplay *view, sessionMapKeys) { + for (TerminalDisplay *view : sessionMapKeys) { applyProfileToView(view, profile); } } void ViewManager::profileChanged(const Profile::Ptr &profile) { // update all views associated with this profile QHashIterator iter(_sessionMap); while (iter.hasNext()) { iter.next(); // if session uses this profile, update the display if (iter.key() != nullptr && iter.value() != nullptr && SessionManager::instance()->sessionProfile(iter.value()) == profile) { applyProfileToView(iter.key(), profile); } } } QList ViewManager::viewProperties() const { QList list; TabbedViewContainer *container = _viewContainer; if (container == nullptr) { return {}; } auto terminalContainers = _viewContainer->findChildren(); list.reserve(terminalContainers.size()); for(auto terminalDisplay : _viewContainer->findChildren()) { list.append(terminalDisplay->sessionController()); } return list; } namespace { QJsonObject saveSessionTerminal(TerminalDisplay *terminalDisplay) { QJsonObject thisTerminal; auto terminalSession = terminalDisplay->sessionController()->session(); const int sessionRestoreId = SessionManager::instance()->getRestoreId(terminalSession); thisTerminal.insert(QStringLiteral("SessionRestoreId"), sessionRestoreId); return thisTerminal; } QJsonObject saveSessionsRecurse(QSplitter *splitter) { QJsonObject thisSplitter; thisSplitter.insert( QStringLiteral("Orientation"), splitter->orientation() == Qt::Horizontal ? QStringLiteral("Horizontal") : QStringLiteral("Vertical") ); QJsonArray internalWidgets; for (int i = 0; i < splitter->count(); i++) { auto *widget = splitter->widget(i); auto *maybeSplitter = qobject_cast(widget); auto *maybeTerminalDisplay = qobject_cast(widget); if (maybeSplitter != nullptr) { internalWidgets.append(saveSessionsRecurse(maybeSplitter)); } else if (maybeTerminalDisplay != nullptr) { internalWidgets.append(saveSessionTerminal(maybeTerminalDisplay)); } } thisSplitter.insert(QStringLiteral("Widgets"), internalWidgets); return thisSplitter; } } // namespace void ViewManager::saveSessions(KConfigGroup &group) { QJsonArray rootArray; for(int i = 0; i < _viewContainer->count(); i++) { auto *splitter = qobject_cast(_viewContainer->widget(i)); rootArray.append(saveSessionsRecurse(splitter)); } group.writeEntry("Tabs", QJsonDocument(rootArray).toJson(QJsonDocument::Compact)); group.writeEntry("Active", _viewContainer->currentIndex()); } namespace { ViewSplitter *restoreSessionsSplitterRecurse(const QJsonObject& jsonSplitter, ViewManager *manager) { const QJsonArray splitterWidgets = jsonSplitter[QStringLiteral("Widgets")].toArray(); auto orientation = (jsonSplitter[QStringLiteral("Orientation")].toString() == QLatin1String("Horizontal")) ? Qt::Horizontal : Qt::Vertical; auto *currentSplitter = new ViewSplitter(); currentSplitter->setOrientation(orientation); for (const auto widgetJsonValue : splitterWidgets) { const auto widgetJsonObject = widgetJsonValue.toObject(); const auto sessionIterator = widgetJsonObject.constFind(QStringLiteral("SessionRestoreId")); if (sessionIterator != widgetJsonObject.constEnd()) { Session *session = SessionManager::instance()->idToSession(sessionIterator->toInt()); auto newView = manager->createView(session); currentSplitter->addWidget(newView); } else { auto nextSplitter = restoreSessionsSplitterRecurse(widgetJsonObject, manager); currentSplitter->addWidget(nextSplitter); } } return currentSplitter; } } // namespace void ViewManager::restoreSessions(const KConfigGroup &group) { const auto tabList = group.readEntry("Tabs", QByteArray("[]")); const auto jsonTabs = QJsonDocument::fromJson(tabList).array(); for (const auto& jsonSplitter : jsonTabs) { auto topLevelSplitter = restoreSessionsSplitterRecurse(jsonSplitter.toObject(), this); _viewContainer->addSplitter(topLevelSplitter, _viewContainer->count()); } if (!jsonTabs.isEmpty()) return; // Session file is unusable, try older format - QList ids = group.readEntry("Sessions", QList()); + const QList ids = group.readEntry("Sessions", QList()); int activeTab = group.readEntry("Active", 0); TerminalDisplay *display = nullptr; int tab = 1; - foreach (int id, ids) { + bool loadingFailed = false; + for (int id : ids) { Session *session = SessionManager::instance()->idToSession(id); if (session == nullptr) { qWarning() << "Unable to load session with id" << id; // Force a creation of a default session below - ids.clear(); + loadingFailed = true; break; } activeContainer()->addView(createView(session)); if (!session->isRunning()) { session->run(); } if (tab++ == activeTab) { display = qobject_cast(activeView()); } } if (display != nullptr) { activeContainer()->setCurrentWidget(display); display->setFocus(Qt::OtherFocusReason); } - if (ids.isEmpty()) { // Session file is unusable, start default Profile + if (loadingFailed) { // Session file is unusable, start default Profile Profile::Ptr profile = ProfileManager::instance()->defaultProfile(); Session *session = SessionManager::instance()->createSession(profile); activeContainer()->addView(createView(session)); if (!session->isRunning()) { session->run(); } } } TabbedViewContainer *ViewManager::activeContainer() { return _viewContainer; } int ViewManager::sessionCount() { return _sessionMap.size(); } QStringList ViewManager::sessionList() { QStringList ids; QHash::const_iterator i; for (i = _sessionMap.constBegin(); i != _sessionMap.constEnd(); ++i) { ids.append(QString::number(i.value()->sessionId())); } return ids; } int ViewManager::currentSession() { if (_pluggedController) { Q_ASSERT(_pluggedController->session() != nullptr); return _pluggedController->session()->sessionId(); } return -1; } void ViewManager::setCurrentSession(int sessionId) { auto *session = SessionManager::instance()->idToSession(sessionId); if (session == nullptr || session->views().count() == 0) { return; } auto *display = session->views().at(0); if (display != nullptr) { display->setFocus(Qt::OtherFocusReason); } } int ViewManager::newSession() { return newSession(QString(), QString()); } int ViewManager::newSession(const QString &profile) { return newSession(profile, QString()); } int ViewManager::newSession(const QString &profile, const QString &directory) { Profile::Ptr profileptr = ProfileManager::instance()->defaultProfile(); if(!profile.isEmpty()) { const QList profilelist = ProfileManager::instance()->allProfiles(); for (const auto &i : profilelist) { if (i->name() == profile) { profileptr = i; break; } } } Session *session = createSession(profileptr, directory); auto newView = createView(session); activeContainer()->addView(newView); session->run(); return session->sessionId(); } QString ViewManager::defaultProfile() { return ProfileManager::instance()->defaultProfile()->name(); } QStringList ViewManager::profileList() { return ProfileManager::instance()->availableProfileNames(); } void ViewManager::nextSession() { nextView(); } void ViewManager::prevSession() { previousView(); } void ViewManager::moveSessionLeft() { moveActiveViewLeft(); } void ViewManager::moveSessionRight() { moveActiveViewRight(); } void ViewManager::setTabWidthToText(bool setTabWidthToText) { _viewContainer->tabBar()->setExpanding(!setTabWidthToText); _viewContainer->tabBar()->update(); } void ViewManager::setNavigationVisibility(NavigationVisibility navigationVisibility) { if (_navigationVisibility != navigationVisibility) { _navigationVisibility = navigationVisibility; _viewContainer->setNavigationVisibility(navigationVisibility); } } void ViewManager::updateTerminalDisplayHistory(TerminalDisplay* terminalDisplay, bool remove) { if (terminalDisplay == nullptr) { if (_terminalDisplayHistoryIndex >= 0) { // This is the case when we finished walking through the history // (i.e. when Ctrl-Tab has been released) terminalDisplay = _terminalDisplayHistory[_terminalDisplayHistoryIndex]; _terminalDisplayHistoryIndex = -1; } else { return; } } if (_terminalDisplayHistoryIndex >= 0 && !remove) { // Do not reorder the tab history while we are walking through it return; } for (int i = 0; i < _terminalDisplayHistory.count(); i++) { if (_terminalDisplayHistory[i] == terminalDisplay) { _terminalDisplayHistory.removeAt(i); if (!remove) { _terminalDisplayHistory.prepend(terminalDisplay); } break; } } } diff --git a/src/settings/ProfileSettings.cpp b/src/settings/ProfileSettings.cpp index 3f1875ae..1e079f06 100644 --- a/src/settings/ProfileSettings.cpp +++ b/src/settings/ProfileSettings.cpp @@ -1,539 +1,544 @@ /* Copyright 2007-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "ProfileSettings.h" // Qt #include #include #include #include // Konsole #include "EditProfileDialog.h" #include "ProfileManager.h" #include "Session.h" #include "TerminalDisplay.h" #include "SessionManager.h" #include "SessionController.h" using namespace Konsole; ProfileSettings::ProfileSettings(QWidget* aParent) : QWidget(aParent) , _sessionModel(new QStandardItemModel(this)) { setupUi(this); profilesList->setItemDelegateForColumn(ShortcutColumn, new ShortcutItemDelegate(this)); // double clicking the profile name opens the profile edit dialog connect(profilesList, &QAbstractItemView::doubleClicked, this, &Konsole::ProfileSettings::doubleClicked); // populate the table with profiles populateTable(); // listen for changes to profiles connect(ProfileManager::instance(), &Konsole::ProfileManager::profileAdded, this, &Konsole::ProfileSettings::addItems); connect(ProfileManager::instance(), &Konsole::ProfileManager::profileRemoved, this, &Konsole::ProfileSettings::removeItems); connect(ProfileManager::instance(), &Konsole::ProfileManager::profileChanged, this, &Konsole::ProfileSettings::updateItems); connect(ProfileManager::instance(), &Konsole::ProfileManager::favoriteStatusChanged, this, &Konsole::ProfileSettings::updateFavoriteStatus); // setup buttons connect(newProfileButton, &QPushButton::clicked, this, &Konsole::ProfileSettings::createProfile); connect(editProfileButton, &QPushButton::clicked, this, &Konsole::ProfileSettings::editSelected); connect(deleteProfileButton, &QPushButton::clicked, this, &Konsole::ProfileSettings::deleteSelected); connect(setAsDefaultButton, &QPushButton::clicked, this, &Konsole::ProfileSettings::setSelectedAsDefault); } ProfileSettings::~ProfileSettings() = default; void ProfileSettings::slotAccepted() { ProfileManager::instance()->saveSettings(); deleteLater(); } void ProfileSettings::itemDataChanged(QStandardItem* item) { if (item->column() == ShortcutColumn) { QKeySequence sequence = QKeySequence::fromString(item->text()); QStandardItem *idItem = item->model()->item(item->row(), ProfileColumn); ProfileManager::instance()->setShortcut(idItem->data(ProfilePtrRole).value(), sequence); } else if (item->column() == FavoriteStatusColumn) { QStandardItem *idItem = item->model()->item(item->row(), ProfileColumn); const bool isFavorite = item->checkState() == Qt::Checked; ProfileManager::instance()->setFavorite(idItem->data(ProfilePtrRole).value(), isFavorite); updateShortcutField(item->model()->item(item->row(), ShortcutColumn), isFavorite); } } void ProfileSettings::updateShortcutField(QStandardItem *item, bool isFavorite) const { if(isFavorite) { item->setToolTip(i18nc("@info:tooltip", "Double click to change shortcut")); item->setForeground(palette().color(QPalette::Normal, QPalette::Text)); } else { item->setToolTip(i18nc("@info:tooltip", "Shortcut won't work while the profile is not marked as visible.")); item->setForeground(palette().color(QPalette::Disabled, QPalette::Text)); } } int ProfileSettings::rowForProfile(const Profile::Ptr &profile) const { const int rowCount = _sessionModel->rowCount(); for (int i = 0; i < rowCount; i++) { if (_sessionModel->item(i, ProfileColumn)->data(ProfilePtrRole) .value() == profile) { return i; } } return -1; } void ProfileSettings::removeItems(const Profile::Ptr &profile) { int row = rowForProfile(profile); if (row < 0) { return; } _sessionModel->removeRow(row); } void ProfileSettings::updateItems(const Profile::Ptr &profile) { const int row = rowForProfile(profile); if (row < 0) { return; } const auto items = QList { _sessionModel->item(row, FavoriteStatusColumn), _sessionModel->item(row, ProfileNameColumn), _sessionModel->item(row, ShortcutColumn), _sessionModel->item(row, ProfileColumn), }; updateItemsForProfile(profile, items); } void ProfileSettings::updateItemsForProfile(const Profile::Ptr &profile, const QList& items) const { // "Enabled" checkbox const auto isEnabled = ProfileManager::instance()->findFavorites().contains(profile); items[FavoriteStatusColumn]->setCheckState(isEnabled ? Qt::Checked : Qt::Unchecked); items[FavoriteStatusColumn]->setCheckable(true); items[FavoriteStatusColumn]->setToolTip( i18nc("@info:tooltip List item's checkbox for making item (profile) visible in a menu", "Show profile in menu")); // Profile Name items[ProfileNameColumn]->setText(profile->name()); if (!profile->icon().isEmpty()) { items[ProfileNameColumn]->setIcon(QIcon::fromTheme(profile->icon())); } // only allow renaming the profile from the edit profile dialog // so as to use ProfileManager::checkProfileName() items[ProfileNameColumn]->setEditable(false); // Shortcut const auto shortcut = ProfileManager::instance()->shortcut(profile).toString(); items[ShortcutColumn]->setText(shortcut); updateShortcutField(items[ShortcutColumn], isEnabled); // Profile ID (pointer to profile) - intended to be hidden in a view items[ProfileColumn]->setData(QVariant::fromValue(profile), ProfilePtrRole); } void ProfileSettings::doubleClicked(const QModelIndex &index) { QStandardItem *item = _sessionModel->itemFromIndex(index); if (item->column() == ProfileNameColumn) { editSelected(); } } void ProfileSettings::addItems(const Profile::Ptr &profile) { if (profile->isHidden()) { return; } // each _sessionModel row has three items. const auto items = QList { new QStandardItem(), new QStandardItem(), new QStandardItem(), new QStandardItem(), }; updateItemsForProfile(profile, items); _sessionModel->appendRow(items); } void ProfileSettings::populateTable() { Q_ASSERT(!profilesList->model()); profilesList->setModel(_sessionModel); _sessionModel->clear(); // setup session table _sessionModel->setHorizontalHeaderLabels({ QString(), // set using header item below i18nc("@title:column Profile name", "Name"), i18nc("@title:column Profile keyboard shortcut", "Shortcut"), QString(), }); auto *favoriteColumnHeaderItem = new QStandardItem(); favoriteColumnHeaderItem->setIcon(QIcon::fromTheme(QStringLiteral("visibility"))); favoriteColumnHeaderItem->setToolTip( i18nc("@info:tooltip List item's checkbox for making item (profile) visible in a menu", "Show profile in menu")); _sessionModel->setHorizontalHeaderItem(FavoriteStatusColumn, favoriteColumnHeaderItem); // Calculate favorite column width. resizeColumnToContents() // is not used because it takes distance between checkbox and // text into account, but there is no text and it looks weird. const int headerMargin = style()->pixelMetric(QStyle::PM_HeaderMargin, nullptr, profilesList->header()); const int iconWidth = style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, profilesList->header()); const int favoriteHeaderWidth = headerMargin * 2 + iconWidth; QStyleOptionViewItem opt; opt.features = QStyleOptionViewItem::HasCheckIndicator | QStyleOptionViewItem::HasDecoration; const QRect checkBoxRect = style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &opt, profilesList); // When right edge is at x < 0 it is assumed the checkbox is // placed on the right item's side and the margin between right // checkbox edge and right item edge should be used. const int checkBoxMargin = checkBoxRect.right() >= 0 ? checkBoxRect.x() : 0 - checkBoxRect.right(); const int favoriteItemWidth = checkBoxMargin * 2 + checkBoxRect.width(); auto *listHeader = profilesList->header(); profilesList->setColumnWidth(FavoriteStatusColumn, qMax(favoriteHeaderWidth, favoriteItemWidth)); profilesList->resizeColumnToContents(ProfileNameColumn); listHeader->setSectionResizeMode(FavoriteStatusColumn, QHeaderView::ResizeMode::Fixed); listHeader->setSectionResizeMode(ProfileNameColumn, QHeaderView::ResizeMode::Stretch); listHeader->setSectionResizeMode(ShortcutColumn, QHeaderView::ResizeMode::ResizeToContents); listHeader->setStretchLastSection(false); listHeader->setSectionsMovable(false); profilesList->hideColumn(ProfileColumn); QList profiles = ProfileManager::instance()->allProfiles(); ProfileManager::instance()->sortProfiles(profiles); - foreach(const Profile::Ptr& profile, profiles) { + for (const Profile::Ptr &profile : qAsConst(profiles)) { addItems(profile); } updateDefaultItem(); connect(_sessionModel, &QStandardItemModel::itemChanged, this, &Konsole::ProfileSettings::itemDataChanged); // listen for changes in the table selection and update the state of the form's buttons // accordingly. // // it appears that the selection model is changed when the model itself is replaced, // so the signals need to be reconnected each time the model is updated. connect(profilesList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Konsole::ProfileSettings::tableSelectionChanged); } void ProfileSettings::updateDefaultItem() { Profile::Ptr defaultProfile = ProfileManager::instance()->defaultProfile(); const QString defaultItemSuffix = i18nc("@item:intable Default list item's name suffix (with separator)", " (default)"); const int rowCount = _sessionModel->rowCount(); for (int i = 0; i < rowCount; i++) { QStandardItem* item = _sessionModel->item(i, ProfileNameColumn); QFont itemFont = item->font(); QStandardItem* profileIdItem = _sessionModel->item(i, ProfileColumn); auto profile = profileIdItem->data().value(); const bool isDefault = (defaultProfile == profile); const QString cleanItemName = profile != nullptr ? profile->name() : QString(); if (isDefault) { itemFont.setItalic(true); item->setFont(itemFont); item->setText(cleanItemName + defaultItemSuffix); } else if (!isDefault) { // FIXME: use default font itemFont.setItalic(false); item->setFont(itemFont); item->setText(cleanItemName); } } } void ProfileSettings::tableSelectionChanged(const QItemSelection&) { const ProfileManager* manager = ProfileManager::instance(); bool isNotDefault = true; bool isDeletable = true; const auto profiles = selectedProfiles(); for (const auto &profile: profiles) { isNotDefault = isNotDefault && (profile != manager->defaultProfile()); isDeletable = isDeletable && isProfileDeletable(profile); } newProfileButton->setEnabled(profiles.count() < 2); // FIXME: At some point editing 2+ profiles no longer works editProfileButton->setEnabled(profiles.count() == 1); // do not allow the default session type to be removed deleteProfileButton->setEnabled(isDeletable && isNotDefault && (profiles.count() > 0)); setAsDefaultButton->setEnabled(isNotDefault && (profiles.count() == 1)); } void ProfileSettings::deleteSelected() { - foreach(const Profile::Ptr & profile, selectedProfiles()) { + const QList profiles = selectedProfiles(); + for (const Profile::Ptr &profile : profiles) { if (profile != ProfileManager::instance()->defaultProfile()) { ProfileManager::instance()->deleteProfile(profile); } } } void ProfileSettings::setSelectedAsDefault() { ProfileManager::instance()->setDefaultProfile(currentProfile()); // do not allow the new default session type to be removed deleteProfileButton->setEnabled(false); setAsDefaultButton->setEnabled(false); // update font of new default item updateDefaultItem(); } void ProfileSettings::createProfile() { // setup a temporary profile which is a clone of the selected profile // or the default if no profile is selected Profile::Ptr sourceProfile = currentProfile() ? currentProfile() : ProfileManager::instance()->defaultProfile(); Q_ASSERT(sourceProfile); auto newProfile = Profile::Ptr(new Profile(ProfileManager::instance()->fallbackProfile())); newProfile->clone(sourceProfile, true); // TODO: add number suffix when the name is taken newProfile->setProperty(Profile::Name, i18nc("@item This will be used as part of the file name", "New Profile")); newProfile->setProperty(Profile::UntranslatedName, QStringLiteral("New Profile")); newProfile->setProperty(Profile::MenuIndex, QStringLiteral("0")); // Consider https://blogs.kde.org/2009/03/26/how-crash-almost-every-qtkde-application-and-how-fix-it-0 before changing the below QPointer dialog = new EditProfileDialog(this); dialog.data()->setProfile(newProfile); dialog.data()->selectProfileName(); if (dialog.data()->exec() == QDialog::Accepted) { ProfileManager::instance()->addProfile(newProfile); ProfileManager::instance()->setFavorite(newProfile, true); ProfileManager::instance()->changeProfile(newProfile, newProfile->setProperties()); } delete dialog.data(); } void ProfileSettings::editSelected() { - QList profiles(selectedProfiles()); - - foreach (Session* session, SessionManager::instance()->sessions()) { - foreach (TerminalDisplay* terminal, session->views()) { - // Searching for opened profiles - if (terminal->sessionController()->profileDialogPointer() != nullptr) { - foreach (const Profile::Ptr & profile, profiles) { - if (profile->name() == terminal->sessionController()->profileDialogPointer()->lookupProfile()->name() - && terminal->sessionController()->profileDialogPointer()->isVisible()) { - // close opened edit dialog - terminal->sessionController()->profileDialogPointer()->close(); - } - } - } + const QList profiles = selectedProfiles(); + EditProfileDialog *profileDialog = nullptr; + // sessions() returns a const QList + for (const Session *session : SessionManager::instance()->sessions()) { + const QList viewsList = session->views(); + for (TerminalDisplay *terminalDisplay : viewsList) { + // Searching for opened profiles + profileDialog = terminalDisplay->sessionController()->profileDialogPointer(); + if (profileDialog != nullptr) { + for (const Profile::Ptr &profile : profiles) { + if (profile->name() == profileDialog->lookupProfile()->name() + && profileDialog->isVisible()) { + // close opened edit dialog + profileDialog->close(); + } + } + } } } EditProfileDialog dialog(this); // the dialog will delete the profile group when it is destroyed ProfileGroup* group = new ProfileGroup; - foreach (const Profile::Ptr & profile, profiles) { + for (const Profile::Ptr &profile : profiles) { group->addProfile(profile); } group->updateValues(); dialog.setProfile(Profile::Ptr(group)); dialog.exec(); } QList ProfileSettings::selectedProfiles() const { QList list; QItemSelectionModel* selection = profilesList->selectionModel(); if (selection == nullptr) { return list; } - foreach(const QModelIndex & index, selection->selectedIndexes()) { + const QList selectedIndexes = selection->selectedIndexes(); + for (const QModelIndex &index : selectedIndexes) { if (index.column() == ProfileColumn) { list << index.data(ProfilePtrRole).value(); } } return list; } Profile::Ptr ProfileSettings::currentProfile() const { QItemSelectionModel* selection = profilesList->selectionModel(); if ((selection == nullptr) || selection->selectedRows().count() != 1) { return Profile::Ptr(); } return selection-> selectedIndexes().at(ProfileColumn).data(ProfilePtrRole).value(); } bool ProfileSettings::isProfileDeletable(Profile::Ptr profile) const { if (!profile) { return false; } const QFileInfo fileInfo(profile->path()); if (!fileInfo.exists()) { return false; } const QFileInfo dirInfo(fileInfo.path()); return dirInfo.isWritable(); } void ProfileSettings::updateFavoriteStatus(const Profile::Ptr &profile, bool favorite) { Q_ASSERT(_sessionModel); const int rowCount = _sessionModel->rowCount(); for (int i = 0; i < rowCount; i++) { auto *item = _sessionModel->item(i, ProfileColumn); if (item->data(ProfilePtrRole).value() == profile) { auto *favoriteItem = _sessionModel->item(i, FavoriteStatusColumn); favoriteItem->setCheckState(favorite ? Qt::Checked : Qt::Unchecked); break; } } } void ProfileSettings::setShortcutEditorVisible(bool visible) { profilesList->setColumnHidden(ShortcutColumn, !visible); } void StyledBackgroundPainter::drawBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex&) { const auto* opt = qstyleoption_cast(&option); const QWidget* widget = opt != nullptr ? opt->widget : nullptr; QStyle* style = widget != nullptr ? widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, widget); } ShortcutItemDelegate::ShortcutItemDelegate(QObject* aParent) : QStyledItemDelegate(aParent), _modifiedEditors(QSet()), _itemsBeingEdited(QSet()) { } void ShortcutItemDelegate::editorModified() { auto* editor = qobject_cast(sender()); Q_ASSERT(editor); _modifiedEditors.insert(editor); emit commitData(editor); emit closeEditor(editor); } void ShortcutItemDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { _itemsBeingEdited.remove(index); if (!_modifiedEditors.contains(editor)) { return; } QString shortcut = qobject_cast(editor)->keySequence().toString(); model->setData(index, shortcut, Qt::DisplayRole); _modifiedEditors.remove(editor); } QWidget* ShortcutItemDelegate::createEditor(QWidget* aParent, const QStyleOptionViewItem&, const QModelIndex& index) const { _itemsBeingEdited.insert(index); auto editor = new FilteredKeySequenceEdit(aParent); QString shortcutString = index.data(Qt::DisplayRole).toString(); editor->setKeySequence(QKeySequence::fromString(shortcutString)); connect(editor, &QKeySequenceEdit::editingFinished, this, &Konsole::ShortcutItemDelegate::editorModified); editor->setFocus(Qt::FocusReason::MouseFocusReason); return editor; } void ShortcutItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { if (_itemsBeingEdited.contains(index)) { StyledBackgroundPainter::drawBackground(painter, option, index); } else { QStyledItemDelegate::paint(painter, option, index); } } QSize Konsole::ShortcutItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { const QString shortcutString = index.data(Qt::DisplayRole).toString(); QFontMetrics fm = option.fontMetrics; static const int editorMargins = 16; // chosen empirically const int width = fm.boundingRect(shortcutString + QStringLiteral(", ...")).width() + editorMargins; return {width, QStyledItemDelegate::sizeHint(option, index).height()}; } void Konsole::ShortcutItemDelegate::destroyEditor(QWidget *editor, const QModelIndex &index) const { _itemsBeingEdited.remove(index); _modifiedEditors.remove(editor); editor->deleteLater(); } void Konsole::FilteredKeySequenceEdit::keyPressEvent(QKeyEvent *event) { if(event->modifiers() == Qt::NoModifier) { switch(event->key()) { case Qt::Key_Enter: case Qt::Key_Return: emit editingFinished(); return; case Qt::Key_Backspace: case Qt::Key_Delete: clear(); emit editingFinished(); event->accept(); return; default: event->accept(); return; } } QKeySequenceEdit::keyPressEvent(event); }