diff --git a/src/Application.cpp b/src/Application.cpp index 6cd10b8f..ec2da4cc 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -1,605 +1,605 @@ /* 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 "Application.h" // Qt #include #include #include #include #include #include // KDE #include #include #include // Konsole #include "SessionManager.h" #include "ProfileManager.h" #include "MainWindow.h" #include "Session.h" #include "ShellCommand.h" #include "KonsoleSettings.h" #include "ViewManager.h" #include "SessionController.h" #include "WindowSystemInfo.h" using namespace Konsole; Application::Application(QSharedPointer parser, const QStringList &customCommand) : _backgroundInstance(nullptr), m_parser(parser), m_customCommand(customCommand) { } void Application::populateCommandLineParser(QCommandLineParser *parser) { auto options = QVector { { { QStringLiteral("profile") }, i18nc("@info:shell", "Name of profile to use for new Konsole instance"), QStringLiteral("name") }, { { QStringLiteral("fallback-profile") }, i18nc("@info:shell", "Use the internal FALLBACK profile") }, { { QStringLiteral("workdir") }, i18nc("@info:shell", "Set the initial working directory of the new tab or window to 'dir'"), QStringLiteral("dir") }, { { QStringLiteral("hold"), QStringLiteral("noclose") }, i18nc("@info:shell", "Do not close the initial session automatically when it ends.") }, { {QStringLiteral("new-tab") }, i18nc("@info:shell", "Create a new tab in an existing window rather than creating a new window") }, { { QStringLiteral("tabs-from-file") }, i18nc("@info:shell","Create tabs as specified in given tabs configuration"" file"), QStringLiteral("file") }, { { QStringLiteral("background-mode") }, i18nc("@info:shell", "Start Konsole in the background and bring to the front when Ctrl+Shift+F12 (by default) is pressed") }, { { QStringLiteral("separate"), QStringLiteral("nofork") }, i18nc("@info:shell", "Run in a separate process") }, { { QStringLiteral("show-menubar") }, i18nc("@info:shell", "Show the menubar, overriding the default setting") }, { { QStringLiteral("hide-menubar") }, i18nc("@info:shell", "Hide the menubar, overriding the default setting") }, { { QStringLiteral("show-tabbar") }, i18nc("@info:shell", "Show the tabbar, overriding the default setting") }, { { QStringLiteral("hide-tabbar") }, i18nc("@info:shell", "Hide the tabbar, overriding the default setting") }, { { QStringLiteral("fullscreen") }, i18nc("@info:shell", "Start Konsole in fullscreen mode") }, { { QStringLiteral("notransparency") }, i18nc("@info:shell", "Disable transparent backgrounds, even if the system supports them.") }, { { QStringLiteral("list-profiles") }, i18nc("@info:shell", "List the available profiles") }, { { QStringLiteral("list-profile-properties") }, i18nc("@info:shell", "List all the profile properties names and their type (for use with -p)") }, { { QStringLiteral("p") }, i18nc("@info:shell", "Change the value of a profile property."), QStringLiteral("property=value") }, { { QStringLiteral("e") }, i18nc("@info:shell", "Command to execute. This option will catch all following arguments, so use it as the last option."), QStringLiteral("cmd") } }; foreach(const auto& option, options) { parser->addOption(option); } parser->addPositionalArgument(QStringLiteral("[args]"), i18nc("@info:shell", "Arguments passed to command")); // Add a no-op compatibility option to make Konsole compatible with // Debian's policy on X terminal emulators. // -T is technically meant to set a title, that is not really meaningful // for Konsole as we have multiple user-facing options controlling // the title and overriding whatever is set elsewhere. // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=532029 // https://www.debian.org/doc/debian-policy/ch-customized-programs.html#s11.8.3 auto titleOption = QCommandLineOption({ QStringLiteral("T") }, QStringLiteral("Debian policy compatibility, not used"), QStringLiteral("value")); titleOption.setHidden(true); parser->addOption(titleOption); } QStringList Application::getCustomCommand(QStringList &args) { int i = args.indexOf(QStringLiteral("-e")); QStringList customCommand; if ((0 < i) && (i < (args.size() - 1))) { // -e was specified with at least one extra argument // if -e was specified without arguments, QCommandLineParser will deal // with that args.removeAt(i); while (args.size() > i) { customCommand << args.takeAt(i); } } return customCommand; } Application::~Application() { SessionManager::instance()->closeAllSessions(); ProfileManager::instance()->saveSettings(); } MainWindow *Application::newMainWindow() { WindowSystemInfo::HAVE_TRANSPARENCY = !m_parser->isSet(QStringLiteral("notransparency")); auto window = new MainWindow(); connect(window, &Konsole::MainWindow::newWindowRequest, this, &Konsole::Application::createWindow); connect(window, &Konsole::MainWindow::viewDetached, this, &Konsole::Application::detachView); return window; } -void Application::createWindow(Profile::Ptr profile, const QString &directory) +void Application::createWindow(const Profile::Ptr &profile, const QString &directory) { MainWindow *window = newMainWindow(); ViewManager *viewManager = window->viewManager(); window->createSession(viewManager->activeContainer(), profile, directory); finalizeNewMainWindow(window); } void Application::detachView(Session *session) { MainWindow *currentWindow = qobject_cast(sender()); MainWindow *window = newMainWindow(); ViewManager *manager = window->viewManager(); manager->createView(manager->activeContainer(), session); // Since user is dragging and dropping, move dnd window to where // the user has the cursor (correct multiple monitor setups). window->move(QCursor::pos()); window->resize(currentWindow->geometry().width(), currentWindow->geometry().height()); window->show(); } int Application::newInstance() { // handle session management // returns from processWindowArgs(args, createdNewMainWindow) // if a new window was created bool createdNewMainWindow = false; // check for arguments to print help or other information to the // terminal, quit if such an argument was found if (processHelpArgs()) { return 0; } // create a new window or use an existing one MainWindow *window = processWindowArgs(createdNewMainWindow); if (m_parser->isSet(QStringLiteral("tabs-from-file"))) { // create new session(s) as described in file if (!processTabsFromFileArgs(window)) { return 0; } } // select profile to use Profile::Ptr baseProfile = processProfileSelectArgs(); // process various command-line options which cause a property of the // selected profile to be changed Profile::Ptr newProfile = processProfileChangeArgs(baseProfile); // create new session ViewManager *viewManager = window->viewManager(); Session *session = window->createSession(viewManager->activeContainer(), newProfile, QString()); if (m_parser->isSet(QStringLiteral("noclose"))) { session->setAutoClose(false); } // if the background-mode argument is supplied, start the background // session ( or bring to the front if it already exists ) if (m_parser->isSet(QStringLiteral("background-mode"))) { startBackgroundMode(window); } else { // Qt constrains top-level windows which have not been manually // resized (via QWidget::resize()) to a maximum of 2/3rds of the // screen size. // // This means that the terminal display might not get the width/ // height it asks for. To work around this, the widget must be // manually resized to its sizeHint(). // // This problem only affects the first time the application is run. // run. After that KMainWindow will have manually resized the // window to its saved size at this point (so the Qt::WA_Resized // attribute will be set) // If not restoring size from last time or only adding new tab, // resize window to chosen profile size (see Bug:345403) if (createdNewMainWindow) { finalizeNewMainWindow(window); } else { window->show(); } } return 1; } /* Documentation for tab file: * * ;; is the token separator * # at the beginning of line results in line being ignored. * supported tokens: title, command, profile and workdir * * Note that the title is static and the tab will close when the * command is complete (do not use --noclose). You can start new tabs. * * Example below will create 6 tabs as listed and a 7th default tab title: This is the title;; command: ssh localhost title: This is the title;; command: ssh localhost;; profile: Shell title: Top this!;; command: top title: mc this!;; command: mc;; workdir: /tmp #this line is comment command: ssh localhost profile: Shell */ bool Application::processTabsFromFileArgs(MainWindow *window) { // Open tab configuration file const QString tabsFileName(m_parser->value(QStringLiteral("tabs-from-file"))); QFile tabsFile(tabsFileName); if (!tabsFile.open(QFile::ReadOnly)) { qWarning() << "ERROR: Cannot open tabs file " << tabsFileName.toLocal8Bit().data(); return false; } unsigned int sessions = 0; while (!tabsFile.atEnd()) { QString lineString(QString::fromUtf8(tabsFile.readLine()).trimmed()); if ((lineString.isEmpty()) || (lineString[0] == QLatin1Char('#'))) { continue; } QHash lineTokens; QStringList lineParts = lineString.split(QStringLiteral(";;"), QString::SkipEmptyParts); for (int i = 0; i < lineParts.size(); ++i) { QString key = lineParts.at(i).section(QLatin1Char(':'), 0, 0).trimmed().toLower(); QString value = lineParts.at(i).section(QLatin1Char(':'), 1, -1).trimmed(); lineTokens[key] = value; } // should contain at least one of 'command' and 'profile' if (lineTokens.contains(QStringLiteral("command")) || lineTokens.contains(QStringLiteral("profile"))) { createTabFromArgs(window, lineTokens); sessions++; } else { qWarning() << "Each line should contain at least one of 'command' and 'profile'."; } } tabsFile.close(); if (sessions < 1) { qWarning() << "No valid lines found in " << tabsFileName.toLocal8Bit().data(); return false; } return true; } void Application::createTabFromArgs(MainWindow *window, const QHash &tokens) { const QString &title = tokens[QStringLiteral("title")]; const QString &command = tokens[QStringLiteral("command")]; const QString &profile = tokens[QStringLiteral("profile")]; const QString &workdir = tokens[QStringLiteral("workdir")]; Profile::Ptr baseProfile; if (!profile.isEmpty()) { baseProfile = ProfileManager::instance()->loadProfile(profile); } if (!baseProfile) { // fallback to default profile baseProfile = ProfileManager::instance()->defaultProfile(); } Profile::Ptr newProfile = Profile::Ptr(new Profile(baseProfile)); newProfile->setHidden(true); // FIXME: the method of determining whether to use newProfile does not // scale well when we support more fields in the future bool shouldUseNewProfile = false; if (!command.isEmpty()) { newProfile->setProperty(Profile::Command, command); newProfile->setProperty(Profile::Arguments, command.split(QLatin1Char(' '))); shouldUseNewProfile = true; } if (!title.isEmpty()) { newProfile->setProperty(Profile::LocalTabTitleFormat, title); newProfile->setProperty(Profile::RemoteTabTitleFormat, title); shouldUseNewProfile = true; } if (m_parser->isSet(QStringLiteral("workdir"))) { newProfile->setProperty(Profile::Directory, m_parser->value(QStringLiteral("workdir"))); shouldUseNewProfile = true; } if (!workdir.isEmpty()) { newProfile->setProperty(Profile::Directory, workdir); shouldUseNewProfile = true; } // Create the new session Profile::Ptr theProfile = shouldUseNewProfile ? newProfile : baseProfile; ViewManager *viewManager = window->viewManager(); Session *session = window->createSession(viewManager->activeContainer(), theProfile, QString()); if (m_parser->isSet(QStringLiteral("noclose"))) { session->setAutoClose(false); } if (!window->testAttribute(Qt::WA_Resized)) { window->resize(window->sizeHint()); } // FIXME: this ugly hack here is to make the session start running, so that // its tab title is displayed as expected. // // This is another side effect of the commit fixing BKO 176902. window->show(); window->hide(); } // Creates a new Konsole window. // If --new-tab is given, use existing window. MainWindow *Application::processWindowArgs(bool &createdNewMainWindow) { MainWindow *window = nullptr; if (m_parser->isSet(QStringLiteral("new-tab"))) { QListIterator iter(QApplication::topLevelWidgets()); iter.toBack(); while (iter.hasPrevious()) { window = qobject_cast(iter.previous()); if (window != nullptr) { break; } } } if (window == nullptr) { createdNewMainWindow = true; window = newMainWindow(); // override default menubar visibility if (m_parser->isSet(QStringLiteral("show-menubar"))) { window->setMenuBarInitialVisibility(true); } if (m_parser->isSet(QStringLiteral("hide-menubar"))) { window->setMenuBarInitialVisibility(false); } if (m_parser->isSet(QStringLiteral("fullscreen"))) { window->viewFullScreen(true); } if (m_parser->isSet(QStringLiteral("show-tabbar"))) { window->viewManager()->setNavigationVisibility(ViewManager::AlwaysShowNavigation); } else if (m_parser->isSet(QStringLiteral("hide-tabbar"))) { window->viewManager()->setNavigationVisibility(ViewManager::AlwaysHideNavigation); } } return window; } // Loads a profile. // If --profile is given, loads profile . // If --fallback-profile is given, loads profile FALLBACK/. // Else loads the default profile. Profile::Ptr Application::processProfileSelectArgs() { Profile::Ptr defaultProfile = ProfileManager::instance()->defaultProfile(); if (m_parser->isSet(QStringLiteral("profile"))) { Profile::Ptr profile = ProfileManager::instance()->loadProfile( m_parser->value(QStringLiteral("profile"))); if (profile) { return profile; } } else if (m_parser->isSet(QStringLiteral("fallback-profile"))) { Profile::Ptr profile = ProfileManager::instance()->loadProfile(QStringLiteral("FALLBACK/")); if (profile) { return profile; } } return defaultProfile; } bool Application::processHelpArgs() { if (m_parser->isSet(QStringLiteral("list-profiles"))) { listAvailableProfiles(); return true; } else if (m_parser->isSet(QStringLiteral("list-profile-properties"))) { listProfilePropertyInfo(); return true; } return false; } void Application::listAvailableProfiles() { QStringList paths = ProfileManager::instance()->availableProfilePaths(); foreach (const QString &path, paths) { QFileInfo info(path); printf("%s\n", info.completeBaseName().toLocal8Bit().constData()); } } void Application::listProfilePropertyInfo() { Profile::Ptr tempProfile = ProfileManager::instance()->defaultProfile(); const QStringList names = tempProfile->propertiesInfoList(); foreach (const QString &name, names) { printf("%s\n", name.toLocal8Bit().constData()); } } Profile::Ptr Application::processProfileChangeArgs(Profile::Ptr baseProfile) { bool shouldUseNewProfile = false; Profile::Ptr newProfile = Profile::Ptr(new Profile(baseProfile)); newProfile->setHidden(true); // change the initial working directory if (m_parser->isSet(QStringLiteral("workdir"))) { newProfile->setProperty(Profile::Directory, m_parser->value(QStringLiteral("workdir"))); shouldUseNewProfile = true; } // temporary changes to profile options specified on the command line const QStringList profileProperties = m_parser->values(QStringLiteral("p")); foreach (const QString &value, profileProperties) { ProfileCommandParser parser; QHashIterator iter(parser.parse(value)); while (iter.hasNext()) { iter.next(); newProfile->setProperty(iter.key(), iter.value()); } shouldUseNewProfile = true; } // run a custom command if (!m_customCommand.isEmpty()) { // Example: konsole -e man ls QString commandExec = m_customCommand[0]; QStringList commandArguments(m_customCommand); if ((m_customCommand.size() == 1) && (QStandardPaths::findExecutable(commandExec).isEmpty())) { // Example: konsole -e "man ls" ShellCommand shellCommand(commandExec); commandExec = shellCommand.command(); commandArguments = shellCommand.arguments(); } if (commandExec.startsWith(QLatin1String("./"))) { commandExec = QDir::currentPath() + commandExec.mid(1); } newProfile->setProperty(Profile::Command, commandExec); newProfile->setProperty(Profile::Arguments, commandArguments); shouldUseNewProfile = true; } if (shouldUseNewProfile) { return newProfile; } else { return baseProfile; } } void Application::startBackgroundMode(MainWindow *window) { if (_backgroundInstance != nullptr) { return; } KActionCollection* collection = window->actionCollection(); QAction* action = collection->addAction(QStringLiteral("toggle-background-window")); action->setObjectName(QStringLiteral("Konsole Background Mode")); action->setText(i18nc("@item", "Toggle Background Window")); KGlobalAccel::self()->setGlobalShortcut(action, QKeySequence(Konsole::ACCEL + Qt::SHIFT + Qt::Key_F12)); connect(action, &QAction::triggered, this, &Application::toggleBackgroundInstance); _backgroundInstance = window; } void Application::toggleBackgroundInstance() { Q_ASSERT(_backgroundInstance); if (!_backgroundInstance->isVisible()) { _backgroundInstance->show(); // ensure that the active terminal display has the focus. Without // this, an odd problem occurred where the focus widget would change // each time the background instance was shown _backgroundInstance->setFocus(); } else { _backgroundInstance->hide(); } } void Application::slotActivateRequested(QStringList args, const QString & /*workingDir*/) { // QCommandLineParser expects the first argument to be the executable name // In the current version it just strips it away args.prepend(qApp->applicationFilePath()); m_customCommand = getCustomCommand(args); // We can't re-use QCommandLineParser instances, it preserves earlier parsed values auto parser = new QCommandLineParser; populateCommandLineParser(parser); parser->parse(args); m_parser.reset(parser); newInstance(); } void Application::finalizeNewMainWindow(MainWindow *window) { if (!KonsoleSettings::saveGeometryOnExit()) { window->resize(window->sizeHint()); } window->show(); } diff --git a/src/Application.h b/src/Application.h index ff97a19d..c702ca5a 100644 --- a/src/Application.h +++ b/src/Application.h @@ -1,97 +1,97 @@ /* Copyright 2007-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef APPLICATION_H #define APPLICATION_H // Qt #include // Konsole #include "Profile.h" namespace Konsole { class MainWindow; class Session; /** * The Konsole Application. * * The application consists of one or more main windows and a set of * factories to create new sessions and views. * * To create a new main window with a default terminal session, call * the newInstance(). Empty main windows can be created using newMainWindow(). * * The factory used to create new terminal sessions can be retrieved using * the sessionManager() accessor. */ class Application : public QObject { Q_OBJECT public: /** Constructs a new Konsole application. */ explicit Application(QSharedPointer parser, const QStringList &customCommand); static void populateCommandLineParser(QCommandLineParser *parser); static QStringList getCustomCommand(QStringList &args); ~Application() Q_DECL_OVERRIDE; /** Creates a new main window and opens a default terminal session */ int newInstance(); /** * Creates a new, empty main window and connects to its newSessionRequest() * and newWindowRequest() signals to trigger creation of new sessions or * windows when then they are emitted. */ MainWindow *newMainWindow(); private Q_SLOTS: - void createWindow(Profile::Ptr profile, const QString &directory); + void createWindow(const Profile::Ptr &profile, const QString &directory); void detachView(Session *session); void toggleBackgroundInstance(); public Q_SLOTS: void slotActivateRequested(QStringList args, const QString &workingDir); private: Q_DISABLE_COPY(Application) void listAvailableProfiles(); void listProfilePropertyInfo(); void startBackgroundMode(MainWindow *window); bool processHelpArgs(); MainWindow *processWindowArgs(bool &createdNewMainWindow); Profile::Ptr processProfileSelectArgs(); Profile::Ptr processProfileChangeArgs(Profile::Ptr baseProfile); bool processTabsFromFileArgs(MainWindow *window); void createTabFromArgs(MainWindow *window, const QHash &); void finalizeNewMainWindow(MainWindow *window); MainWindow *_backgroundInstance; QSharedPointer m_parser; QStringList m_customCommand; }; } #endif // APPLICATION_H diff --git a/src/EditProfileDialog.cpp b/src/EditProfileDialog.cpp index 7d18463b..f590cfa9 100644 --- a/src/EditProfileDialog.cpp +++ b/src/EditProfileDialog.cpp @@ -1,1934 +1,1934 @@ /* Copyright 2007-2008 by Robert Knight Copyright 2018 by Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "EditProfileDialog.h" // Standard #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include // Konsole #include "ui_EditProfileGeneralPage.h" #include "ui_EditProfileTabsPage.h" #include "ui_EditProfileAppearancePage.h" #include "ui_EditProfileScrollingPage.h" #include "ui_EditProfileKeyboardPage.h" #include "ui_EditProfileMousePage.h" #include "ui_EditProfileAdvancedPage.h" #include "ColorSchemeManager.h" #include "KeyBindingEditor.h" #include "KeyboardTranslator.h" #include "KeyboardTranslatorManager.h" #include "ProfileManager.h" #include "ShellCommand.h" #include "WindowSystemInfo.h" #include "Shortcut_p.h" #include "FontDialog.h" using namespace Konsole; EditProfileDialog::EditProfileDialog(QWidget *parent) : KPageDialog(parent) , _generalUi(nullptr) , _tabsUi(nullptr) , _appearanceUi(nullptr) , _scrollingUi(nullptr) , _keyboardUi(nullptr) , _mouseUi(nullptr) , _advancedUi(nullptr) , _tempProfile(nullptr) , _profile(nullptr) , _previewedProperties(QHash()) , _delayedPreviewProperties(QHash()) , _delayedPreviewTimer(new QTimer(this)) , _colorDialog(nullptr) , _buttonBox(nullptr) , _fontDialog(nullptr) { setWindowTitle(i18n("Edit Profile")); setFaceType(KPageDialog::List); _buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply); setButtonBox(_buttonBox); QPushButton *okButton = _buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); connect(_buttonBox, &QDialogButtonBox::accepted, this, &Konsole::EditProfileDialog::accept); connect(_buttonBox, &QDialogButtonBox::rejected, this, &Konsole::EditProfileDialog::reject); // disable the apply button , since no modification has been made _buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); connect(_buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &Konsole::EditProfileDialog::apply); connect(_delayedPreviewTimer, &QTimer::timeout, this, &Konsole::EditProfileDialog::delayedPreviewActivate); // General page const QString generalPageName = i18nc("@title:tab Generic, common options", "General"); auto *generalPageWidget = new QWidget(this); _generalUi = new Ui::EditProfileGeneralPage(); _generalUi->setupUi(generalPageWidget); auto *generalPageItem = addPage(generalPageWidget, generalPageName); generalPageItem->setHeader(generalPageName); generalPageItem->setIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal"))); _pages[generalPageItem] = Page(&EditProfileDialog::setupGeneralPage); // Tabs page const QString tabsPageName = i18n("Tabs"); auto *tabsPageWidget = new QWidget(this); _tabsUi = new Ui::EditProfileTabsPage(); _tabsUi->setupUi(tabsPageWidget); auto *tabsPageItem = addPage(tabsPageWidget, tabsPageName); tabsPageItem->setHeader(tabsPageName); tabsPageItem->setIcon(QIcon::fromTheme(QStringLiteral("tab-duplicate"))); _pages[tabsPageItem] = Page(&EditProfileDialog::setupTabsPage); LabelsAligner tabsAligner(tabsPageWidget); tabsAligner.addLayout(dynamic_cast(_tabsUi->tabMonitoringGroup->layout())); tabsAligner.addLayout(dynamic_cast(_tabsUi->renameTabWidget->layout())); tabsAligner.updateLayouts(); tabsAligner.align(); // Appearance page const QString appearancePageName = i18n("Appearance"); auto *appearancePageWidget = new QWidget(this); _appearanceUi = new Ui::EditProfileAppearancePage(); _appearanceUi->setupUi(appearancePageWidget); auto *appearancePageItem = addPage(appearancePageWidget, appearancePageName); appearancePageItem->setHeader(appearancePageName); appearancePageItem->setIcon(QIcon::fromTheme(QStringLiteral("kcolorchooser"))); _pages[appearancePageItem] = Page(&EditProfileDialog::setupAppearancePage); LabelsAligner appearanceAligner(appearancePageWidget); appearanceAligner.addLayout(dynamic_cast(_appearanceUi->miscTabLayout)); appearanceAligner.addLayout(dynamic_cast(_appearanceUi->contentsGroup->layout())); appearanceAligner.updateLayouts(); appearanceAligner.align(); // Scrolling page const QString scrollingPageName = i18n("Scrolling"); auto *scrollingPageWidget = new QWidget(this); _scrollingUi = new Ui::EditProfileScrollingPage(); _scrollingUi->setupUi(scrollingPageWidget); auto *scrollingPageItem = addPage(scrollingPageWidget, scrollingPageName); scrollingPageItem->setHeader(scrollingPageName); scrollingPageItem->setIcon(QIcon::fromTheme(QStringLiteral("transform-move-vertical"))); _pages[scrollingPageItem] = Page(&EditProfileDialog::setupScrollingPage); // adjust "history size" label height to match history size widget's first radio button _scrollingUi->historySizeLabel->setFixedHeight(_scrollingUi->historySizeWidget->preferredLabelHeight()); // Keyboard page const QString keyboardPageName = i18n("Keyboard"); const QString keyboardPageTitle = i18n("Key bindings"); auto *keyboardPageWidget = new QWidget(this); _keyboardUi = new Ui::EditProfileKeyboardPage(); _keyboardUi->setupUi(keyboardPageWidget); auto *keyboardPageItem = addPage(keyboardPageWidget, keyboardPageName); keyboardPageItem->setHeader(keyboardPageTitle); keyboardPageItem->setIcon(QIcon::fromTheme(QStringLiteral("input-keyboard"))); _pages[keyboardPageItem] = Page(&EditProfileDialog::setupKeyboardPage); // Mouse page const QString mousePageName = i18n("Mouse"); auto *mousePageWidget = new QWidget(this); _mouseUi = new Ui::EditProfileMousePage(); _mouseUi->setupUi(mousePageWidget); auto *mousePageItem = addPage(mousePageWidget, mousePageName); mousePageItem->setHeader(mousePageName); mousePageItem->setIcon(QIcon::fromTheme(QStringLiteral("input-mouse"))); _pages[mousePageItem] = Page(&EditProfileDialog::setupMousePage); // Advanced page const QString advancedPageName = i18nc("@title:tab Complex options", "Advanced"); auto *advancedPageWidget = new QWidget(this); _advancedUi = new Ui::EditProfileAdvancedPage(); _advancedUi->setupUi(advancedPageWidget); auto *advancedPageItem = addPage(advancedPageWidget, advancedPageName); advancedPageItem->setHeader(advancedPageName); advancedPageItem->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); _pages[advancedPageItem] = Page(&EditProfileDialog::setupAdvancedPage); // there are various setupXYZPage() methods to load the items // for each page and update their states to match the profile // being edited. // // these are only called when needed ( ie. when the user clicks // the tab to move to that page ). // // the _pageNeedsUpdate vector keeps track of the pages that have // not been updated since the last profile change and will need // to be refreshed when the user switches to them connect(this, &KPageDialog::currentPageChanged, this, &Konsole::EditProfileDialog::preparePage); QFontMetrics fm(font()); const int ch = fm.width(QLatin1Char('0')); // Increase width a bit instead of using possible minimum resize(sizeHint().width() + 10*ch, sizeHint().height()); createTempProfile(); } EditProfileDialog::~EditProfileDialog() { delete _generalUi; delete _tabsUi; delete _appearanceUi; delete _scrollingUi; delete _keyboardUi; delete _mouseUi; delete _advancedUi; } void EditProfileDialog::save() { if (_tempProfile->isEmpty()) { return; } ProfileManager::instance()->changeProfile(_profile, _tempProfile->setProperties()); // ensure that these settings are not undone by a call // to unpreview() QHashIterator iter(_tempProfile->setProperties()); while (iter.hasNext()) { iter.next(); _previewedProperties.remove(iter.key()); } createTempProfile(); _buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); } void EditProfileDialog::reject() { unpreviewAll(); QDialog::reject(); } void EditProfileDialog::accept() { // if the Apply button is disabled then no settings were changed // or the changes have already been saved by apply() if (_buttonBox->button(QDialogButtonBox::Apply)->isEnabled()) { if (!isValidProfileName()) { return; } save(); } unpreviewAll(); QDialog::accept(); } void EditProfileDialog::apply() { if (!isValidProfileName()) { return; } save(); } bool EditProfileDialog::isValidProfileName() { Q_ASSERT(_profile); Q_ASSERT(_tempProfile); // check whether the user has enough permissions to save the profile QFileInfo fileInfo(_profile->path()); if (fileInfo.exists() && !fileInfo.isWritable()) { if (!_tempProfile->isPropertySet(Profile::Name) || (_tempProfile->name() == _profile->name())) { KMessageBox::sorry(this, i18n("

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

" "

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

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

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

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

A profile with this name already exists.

")); // revert the name in the dialog _generalUi->profileNameEdit->setText(_profile->name()); selectProfileName(); return false; } else { return true; } } -QString EditProfileDialog::groupProfileNames(const ProfileGroup::Ptr group, int maxLength) +QString EditProfileDialog::groupProfileNames(const ProfileGroup::Ptr &group, int maxLength) { QString caption; int count = group->profiles().count(); for (int i = 0; i < count; i++) { caption += group->profiles()[i]->name(); if (i < (count - 1)) { caption += QLatin1Char(','); // limit caption length to prevent very long window titles if (maxLength > 0 && caption.length() > maxLength) { caption += QLatin1String("..."); break; } } } return caption; } -void EditProfileDialog::updateCaption(const Profile::Ptr profile) +void EditProfileDialog::updateCaption(const Profile::Ptr &profile) { const int MAX_GROUP_CAPTION_LENGTH = 25; ProfileGroup::Ptr group = profile->asGroup(); if (group && group->profiles().count() > 1) { QString caption = groupProfileNames(group, MAX_GROUP_CAPTION_LENGTH); setWindowTitle(i18np("Editing profile: %2", "Editing %1 profiles: %2", group->profiles().count(), caption)); } else { setWindowTitle(i18n("Edit Profile \"%1\"", profile->name())); } } -void EditProfileDialog::setProfile(Konsole::Profile::Ptr profile) +void EditProfileDialog::setProfile(const Konsole::Profile::Ptr &profile) { Q_ASSERT(profile); _profile = profile; // update caption updateCaption(profile); // mark each page of the dialog as out of date // and force an update of the currently visible page // // the other pages will be updated as necessary for (Page &page: _pages) { page.needsUpdate = true; } preparePage(currentPage()); if (_tempProfile) { createTempProfile(); } } const Profile::Ptr EditProfileDialog::lookupProfile() const { return _profile; } const QString EditProfileDialog::currentColorSchemeName() const { const QString ¤tColorSchemeName = lookupProfile()->colorScheme(); return currentColorSchemeName; } void EditProfileDialog::preparePage(KPageWidgetItem *current, KPageWidgetItem *before) { Q_UNUSED(before); Q_ASSERT(current); Q_ASSERT(_pages.contains(current)); const Profile::Ptr profile = lookupProfile(); auto setupPage = _pages[current].setupPage; Q_ASSERT(profile); Q_ASSERT(setupPage); if (_pages[current].needsUpdate) { (*this.*setupPage)(profile); _pages[current].needsUpdate = false; } } void Konsole::EditProfileDialog::selectProfileName() { _generalUi->profileNameEdit->setFocus(); _generalUi->profileNameEdit->selectAll(); } void EditProfileDialog::setupGeneralPage(const Profile::Ptr &profile) { // basic profile options { ProfileGroup::Ptr group = profile->asGroup(); if (!group || group->profiles().count() < 2) { _generalUi->profileNameEdit->setText(profile->name()); _generalUi->profileNameEdit->setClearButtonEnabled(true); } else { _generalUi->profileNameEdit->setText(groupProfileNames(group, -1)); _generalUi->profileNameEdit->setEnabled(false); } } ShellCommand command(profile->command(), profile->arguments()); _generalUi->commandEdit->setText(command.fullCommand()); // If a "completion" is requested, consider changing this to KLineEdit // and using KCompletion. _generalUi->initialDirEdit->setText(profile->defaultWorkingDirectory()); _generalUi->initialDirEdit->setClearButtonEnabled(true); _generalUi->initialDirEdit->setPlaceholderText(QStandardPaths::standardLocations(QStandardPaths::HomeLocation).value(0)); _generalUi->dirSelectButton->setIcon(QIcon::fromTheme(QStringLiteral("folder-open"))); _generalUi->iconSelectButton->setIcon(QIcon::fromTheme(profile->icon())); _generalUi->startInSameDirButton->setChecked(profile->startInCurrentSessionDir()); // initial terminal size const auto rowsSuffix = ki18ncp("Suffix of the number of columns (N columns)", " column", " columns"); const auto colsSuffix = ki18ncp("Suffix of the number of rows (N rows)", " row", " rows"); _generalUi->terminalColumnsEntry->setValue(profile->terminalColumns()); _generalUi->terminalRowsEntry->setValue(profile->terminalRows()); _generalUi->terminalColumnsEntry->setSuffix(colsSuffix); _generalUi->terminalRowsEntry->setSuffix(rowsSuffix); // make width of initial terminal size spinboxes equal const int sizeEntryWidth = qMax(maxSpinBoxWidth(_generalUi->terminalColumnsEntry, colsSuffix), maxSpinBoxWidth(_generalUi->terminalRowsEntry, rowsSuffix)); _generalUi->terminalColumnsEntry->setFixedWidth(sizeEntryWidth); _generalUi->terminalRowsEntry->setFixedWidth(sizeEntryWidth); // signals and slots connect(_generalUi->dirSelectButton, &QToolButton::clicked, this, &Konsole::EditProfileDialog::selectInitialDir); connect(_generalUi->iconSelectButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::selectIcon); connect(_generalUi->startInSameDirButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::startInSameDir); connect(_generalUi->profileNameEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::profileNameChanged); connect(_generalUi->initialDirEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::initialDirChanged); connect(_generalUi->commandEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::commandChanged); connect(_generalUi->environmentEditButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::showEnvironmentEditor); connect(_generalUi->terminalColumnsEntry, static_cast(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::terminalColumnsEntryChanged); connect(_generalUi->terminalRowsEntry, static_cast(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::terminalRowsEntryChanged); } void EditProfileDialog::showEnvironmentEditor() { bool ok; const Profile::Ptr profile = lookupProfile(); QStringList currentEnvironment; // The user could re-open the environment editor before clicking // OK/Apply in the parent edit profile dialog, so we make sure // to show the new environment vars if (_tempProfile->isPropertySet(Profile::Environment)) { currentEnvironment = _tempProfile->environment(); } else { currentEnvironment = profile->environment(); } QString text = QInputDialog::getMultiLineText(this, i18n("Edit Environment"), i18n("One environment variable per line"), currentEnvironment.join(QStringLiteral("\n")), &ok); QStringList newEnvironment; if (ok) { if(!text.isEmpty()) { newEnvironment = text.split(QLatin1Char('\n')); updateTempProfileProperty(Profile::Environment, newEnvironment); } else { // the user could have removed all entries so we return an empty list updateTempProfileProperty(Profile::Environment, newEnvironment); } } } void EditProfileDialog::setupTabsPage(const Profile::Ptr &profile) { // tab title format _tabsUi->renameTabWidget->setTabTitleText(profile->localTabTitleFormat()); _tabsUi->renameTabWidget->setRemoteTabTitleText(profile->remoteTabTitleFormat()); connect(_tabsUi->renameTabWidget, &Konsole::RenameTabWidget::tabTitleFormatChanged, this, &Konsole::EditProfileDialog::tabTitleFormatChanged); connect(_tabsUi->renameTabWidget, &Konsole::RenameTabWidget::remoteTabTitleFormatChanged, this, &Konsole::EditProfileDialog::remoteTabTitleFormatChanged); // tab monitoring const int silenceSeconds = profile->silenceSeconds(); _tabsUi->silenceSecondsSpinner->setValue(silenceSeconds); auto suffix = ki18ncp("Unit of time", " second", " seconds"); _tabsUi->silenceSecondsSpinner->setSuffix(suffix); int silenceCheckBoxWidth = maxSpinBoxWidth(_generalUi->terminalColumnsEntry, suffix); _tabsUi->silenceSecondsSpinner->setFixedWidth(silenceCheckBoxWidth); connect(_tabsUi->silenceSecondsSpinner, static_cast(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::silenceSecondsChanged); } void EditProfileDialog::terminalColumnsEntryChanged(int value) { updateTempProfileProperty(Profile::TerminalColumns, value); } void EditProfileDialog::terminalRowsEntryChanged(int value) { updateTempProfileProperty(Profile::TerminalRows, value); } void EditProfileDialog::showTerminalSizeHint(bool value) { updateTempProfileProperty(Profile::ShowTerminalSizeHint, value); } void EditProfileDialog::setDimWhenInactive(bool value) { updateTempProfileProperty(Profile::DimWhenInactive, value); } void EditProfileDialog::tabTitleFormatChanged(const QString &format) { updateTempProfileProperty(Profile::LocalTabTitleFormat, format); } void EditProfileDialog::remoteTabTitleFormatChanged(const QString &format) { updateTempProfileProperty(Profile::RemoteTabTitleFormat, format); } void EditProfileDialog::silenceSecondsChanged(int seconds) { updateTempProfileProperty(Profile::SilenceSeconds, seconds); } void EditProfileDialog::selectIcon() { const QString &icon = KIconDialog::getIcon(KIconLoader::Desktop, KIconLoader::Application, false, 0, false, this); if (!icon.isEmpty()) { _generalUi->iconSelectButton->setIcon(QIcon::fromTheme(icon)); updateTempProfileProperty(Profile::Icon, icon); } } void EditProfileDialog::profileNameChanged(const QString &name) { updateTempProfileProperty(Profile::Name, name); updateTempProfileProperty(Profile::UntranslatedName, name); updateCaption(_tempProfile); } void EditProfileDialog::startInSameDir(bool sameDir) { updateTempProfileProperty(Profile::StartInCurrentSessionDir, sameDir); } void EditProfileDialog::initialDirChanged(const QString &dir) { updateTempProfileProperty(Profile::Directory, dir); } void EditProfileDialog::commandChanged(const QString &command) { ShellCommand shellCommand(command); updateTempProfileProperty(Profile::Command, shellCommand.command()); updateTempProfileProperty(Profile::Arguments, shellCommand.arguments()); } void EditProfileDialog::selectInitialDir() { const QUrl url = QFileDialog::getExistingDirectoryUrl(this, i18n("Select Initial Directory"), QUrl::fromUserInput(_generalUi->initialDirEdit->text())); if (!url.isEmpty()) { _generalUi->initialDirEdit->setText(url.path()); } } void EditProfileDialog::setupAppearancePage(const Profile::Ptr &profile) { auto delegate = new ColorSchemeViewDelegate(this); _appearanceUi->colorSchemeList->setItemDelegate(delegate); _appearanceUi->transparencyWarningWidget->setVisible(false); _appearanceUi->transparencyWarningWidget->setWordWrap(true); _appearanceUi->transparencyWarningWidget->setCloseButtonVisible(false); _appearanceUi->transparencyWarningWidget->setMessageType(KMessageWidget::Warning); _appearanceUi->colorSchemeMessageWidget->setVisible(false); _appearanceUi->colorSchemeMessageWidget->setWordWrap(true); _appearanceUi->colorSchemeMessageWidget->setCloseButtonVisible(false); _appearanceUi->colorSchemeMessageWidget->setMessageType(KMessageWidget::Warning); _appearanceUi->editColorSchemeButton->setEnabled(false); _appearanceUi->removeColorSchemeButton->setEnabled(false); _appearanceUi->resetColorSchemeButton->setEnabled(false); _appearanceUi->downloadColorSchemeButton->setConfigFile(QStringLiteral("konsole.knsrc")); // setup color list // select the colorScheme used in the current profile updateColorSchemeList(currentColorSchemeName()); _appearanceUi->colorSchemeList->setMouseTracking(true); _appearanceUi->colorSchemeList->installEventFilter(this); _appearanceUi->colorSchemeList->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(_appearanceUi->colorSchemeList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Konsole::EditProfileDialog::colorSchemeSelected); connect(_appearanceUi->colorSchemeList, &QListView::entered, this, &Konsole::EditProfileDialog::previewColorScheme); updateColorSchemeButtons(); connect(_appearanceUi->editColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::editColorScheme); connect(_appearanceUi->removeColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::removeColorScheme); connect(_appearanceUi->newColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::newColorScheme); connect(_appearanceUi->downloadColorSchemeButton, &KNS3::Button::dialogFinished, this, &Konsole::EditProfileDialog::gotNewColorSchemes); connect(_appearanceUi->resetColorSchemeButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::resetColorScheme); connect(_appearanceUi->chooseFontButton, &QAbstractButton::clicked, this, &EditProfileDialog::showFontDialog); // setup font preview const bool antialias = profile->antiAliasFonts(); QFont profileFont = profile->font(); profileFont.setStyleStrategy(antialias ? QFont::PreferAntialias : QFont::NoAntialias); _appearanceUi->fontPreview->setFont(profileFont); _appearanceUi->fontPreview->setText(QStringLiteral("%1 %2pt").arg(profileFont.family()).arg(profileFont.pointSize())); // setup font smoothing _appearanceUi->antialiasTextButton->setChecked(antialias); connect(_appearanceUi->antialiasTextButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::setAntialiasText); _appearanceUi->boldIntenseButton->setChecked(profile->boldIntense()); connect(_appearanceUi->boldIntenseButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::setBoldIntense); _appearanceUi->useFontLineCharactersButton->setChecked(profile->useFontLineCharacters()); connect(_appearanceUi->useFontLineCharactersButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::useFontLineCharacters); _mouseUi->enableMouseWheelZoomButton->setChecked(profile->mouseWheelZoomEnabled()); connect(_mouseUi->enableMouseWheelZoomButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::toggleMouseWheelZoom); // cursor options const auto options = QVector{ { _appearanceUi->enableBlinkingCursorButton, Profile::BlinkingCursorEnabled, SLOT(toggleBlinkingCursor(bool)) }, }; setupCheckBoxes(options, profile); if (profile->useCustomCursorColor()) { _appearanceUi->customCursorColorButton->setChecked(true); } else { _appearanceUi->autoCursorColorButton->setChecked(true); } _appearanceUi->customColorSelectButton->setColor(profile->customCursorColor()); connect(_appearanceUi->customCursorColorButton, &QRadioButton::clicked, this, &Konsole::EditProfileDialog::customCursorColor); connect(_appearanceUi->autoCursorColorButton, &QRadioButton::clicked, this, &Konsole::EditProfileDialog::autoCursorColor); connect(_appearanceUi->customColorSelectButton, &KColorButton::changed, this, &Konsole::EditProfileDialog::customCursorColorChanged); // Make radio buttons height equal int cursorColorRadioHeight = qMax(_appearanceUi->autoCursorColorButton->minimumSizeHint().height(), _appearanceUi->customColorSelectButton->minimumSizeHint().height()); _appearanceUi->autoCursorColorButton->setMinimumHeight(cursorColorRadioHeight); _appearanceUi->customCursorColorButton->setMinimumHeight(cursorColorRadioHeight); const ButtonGroupOptions cursorShapeOptions = { _appearanceUi->cursorShape, // group Profile::CursorShape, // profileProperty true, // preview { // buttons {_appearanceUi->cursorShapeBlock, Enum::BlockCursor}, {_appearanceUi->cursorShapeIBeam, Enum::IBeamCursor}, {_appearanceUi->cursorShapeUnderline, Enum::UnderlineCursor}, }, }; setupButtonGroup(cursorShapeOptions, profile); _appearanceUi->marginsSpinner->setValue(profile->terminalMargin()); connect(_appearanceUi->marginsSpinner, static_cast(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::terminalMarginChanged); _appearanceUi->lineSpacingSpinner->setValue(profile->lineSpacing()); connect(_appearanceUi->lineSpacingSpinner, static_cast(&QSpinBox::valueChanged), this, &Konsole::EditProfileDialog::lineSpacingChanged); _appearanceUi->alignToCenterButton->setChecked(profile->terminalCenter()); connect(_appearanceUi->alignToCenterButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::setTerminalCenter); _appearanceUi->showTerminalSizeHintButton->setChecked(profile->showTerminalSizeHint()); connect(_appearanceUi->showTerminalSizeHintButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::showTerminalSizeHint); _appearanceUi->dimWhenInactiveCheckbox->setChecked(profile->dimWhenInactive()); connect(_appearanceUi->dimWhenInactiveCheckbox, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::setDimWhenInactive); } void EditProfileDialog::setAntialiasText(bool enable) { preview(Profile::AntiAliasFonts, enable); updateTempProfileProperty(Profile::AntiAliasFonts, enable); const QFont font = _profile->font(); updateFontPreview(font); } void EditProfileDialog::setBoldIntense(bool enable) { preview(Profile::BoldIntense, enable); updateTempProfileProperty(Profile::BoldIntense, enable); } void EditProfileDialog::useFontLineCharacters(bool enable) { preview(Profile::UseFontLineCharacters, enable); updateTempProfileProperty(Profile::UseFontLineCharacters, enable); } void EditProfileDialog::toggleBlinkingCursor(bool enable) { preview(Profile::BlinkingCursorEnabled, enable); updateTempProfileProperty(Profile::BlinkingCursorEnabled, enable); } void EditProfileDialog::setCursorShape(int index) { preview(Profile::CursorShape, index); updateTempProfileProperty(Profile::CursorShape, index); } void EditProfileDialog::autoCursorColor() { preview(Profile::UseCustomCursorColor, false); updateTempProfileProperty(Profile::UseCustomCursorColor, false); } void EditProfileDialog::customCursorColor() { preview(Profile::UseCustomCursorColor, true); updateTempProfileProperty(Profile::UseCustomCursorColor, true); } void EditProfileDialog::customCursorColorChanged(const QColor &color) { preview(Profile::CustomCursorColor, color); updateTempProfileProperty(Profile::CustomCursorColor, color); // ensure that custom cursor colors are enabled _appearanceUi->customCursorColorButton->click(); } void EditProfileDialog::terminalMarginChanged(int margin) { preview(Profile::TerminalMargin, margin); updateTempProfileProperty(Profile::TerminalMargin, margin); } void EditProfileDialog::lineSpacingChanged(int spacing) { preview(Profile::LineSpacing, spacing); updateTempProfileProperty(Profile::LineSpacing, spacing); } void EditProfileDialog::setTerminalCenter(bool enable) { preview(Profile::TerminalCenter, enable); updateTempProfileProperty(Profile::TerminalCenter, enable); } void EditProfileDialog::toggleMouseWheelZoom(bool enable) { updateTempProfileProperty(Profile::MouseWheelZoomEnabled, enable); } void EditProfileDialog::toggleAlternateScrolling(bool enable) { updateTempProfileProperty(Profile::AlternateScrolling, enable); } void EditProfileDialog::updateColorSchemeList(const QString &selectedColorSchemeName) { if (_appearanceUi->colorSchemeList->model() == nullptr) { _appearanceUi->colorSchemeList->setModel(new QStandardItemModel(this)); } const ColorScheme *selectedColorScheme = ColorSchemeManager::instance()->findColorScheme(selectedColorSchemeName); auto *model = qobject_cast(_appearanceUi->colorSchemeList->model()); Q_ASSERT(model); model->clear(); QStandardItem *selectedItem = nullptr; QList schemeList = ColorSchemeManager::instance()->allColorSchemes(); foreach (const ColorScheme *scheme, schemeList) { QStandardItem *item = new QStandardItem(scheme->description()); item->setData(QVariant::fromValue(scheme), Qt::UserRole + 1); item->setData(QVariant::fromValue(_profile->font()), Qt::UserRole + 2); item->setFlags(item->flags()); // if selectedColorSchemeName is not empty then select that scheme // after saving the changes in the colorScheme editor if (selectedColorScheme == scheme) { selectedItem = item; } model->appendRow(item); } model->sort(0); if (selectedItem != nullptr) { _appearanceUi->colorSchemeList->updateGeometry(); _appearanceUi->colorSchemeList->selectionModel()->setCurrentIndex(selectedItem->index(), QItemSelectionModel::Select); // update transparency warning label updateTransparencyWarning(); } } void EditProfileDialog::updateKeyBindingsList(const QString &selectKeyBindingsName) { if (_keyboardUi->keyBindingList->model() == nullptr) { _keyboardUi->keyBindingList->setModel(new QStandardItemModel(this)); } auto *model = qobject_cast(_keyboardUi->keyBindingList->model()); Q_ASSERT(model); model->clear(); QStandardItem *selectedItem = nullptr; const QStringList &translatorNames = _keyManager->allTranslators(); for (const QString &translatorName : translatorNames) { const KeyboardTranslator *translator = _keyManager->findTranslator(translatorName); if (translator == nullptr) { continue; } QStandardItem *item = new QStandardItem(translator->description()); item->setEditable(false); item->setData(QVariant::fromValue(translator), Qt::UserRole + 1); item->setData(QVariant::fromValue(_keyManager->findTranslatorPath(translatorName)), Qt::ToolTipRole); item->setData(QVariant::fromValue(_profile->font()), Qt::UserRole + 2); item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-keyboard"))); if (selectKeyBindingsName == translatorName) { selectedItem = item; } model->appendRow(item); } model->sort(0); if (selectedItem != nullptr) { _keyboardUi->keyBindingList->selectionModel()->setCurrentIndex(selectedItem->index(), QItemSelectionModel::Select); } } bool EditProfileDialog::eventFilter(QObject *watched, QEvent *event) { if (watched == _appearanceUi->colorSchemeList && event->type() == QEvent::Leave) { if (_tempProfile->isPropertySet(Profile::ColorScheme)) { preview(Profile::ColorScheme, _tempProfile->colorScheme()); } else { unpreview(Profile::ColorScheme); } } return QDialog::eventFilter(watched, event); } QSize EditProfileDialog::sizeHint() const { // return QSize(); return QDialog::sizeHint(); } void EditProfileDialog::unpreviewAll() { _delayedPreviewTimer->stop(); _delayedPreviewProperties.clear(); QHash map; QHashIterator iter(_previewedProperties); while (iter.hasNext()) { iter.next(); map.insert(static_cast(iter.key()), iter.value()); } // undo any preview changes if (!map.isEmpty()) { ProfileManager::instance()->changeProfile(_profile, map, false); } } void EditProfileDialog::unpreview(int property) { _delayedPreviewProperties.remove(property); if (!_previewedProperties.contains(property)) { return; } QHash map; map.insert(static_cast(property), _previewedProperties[property]); ProfileManager::instance()->changeProfile(_profile, map, false); _previewedProperties.remove(property); } void EditProfileDialog::delayedPreview(int property, const QVariant &value) { _delayedPreviewProperties.insert(property, value); _delayedPreviewTimer->stop(); _delayedPreviewTimer->start(300); } void EditProfileDialog::delayedPreviewActivate() { Q_ASSERT(qobject_cast(sender())); QMutableHashIterator iter(_delayedPreviewProperties); if (iter.hasNext()) { iter.next(); preview(iter.key(), iter.value()); } } void EditProfileDialog::preview(int property, const QVariant &value) { QHash map; map.insert(static_cast(property), value); _delayedPreviewProperties.remove(property); const Profile::Ptr original = lookupProfile(); // skip previews for profile groups if the profiles in the group // have conflicting original values for the property // // TODO - Save the original values for each profile and use to unpreview properties ProfileGroup::Ptr group = original->asGroup(); if (group && group->profiles().count() > 1 && original->property(static_cast(property)).isNull()) { return; } if (!_previewedProperties.contains(property)) { _previewedProperties.insert(property, original->property(static_cast(property))); } // temporary change to color scheme ProfileManager::instance()->changeProfile(_profile, map, false); } void EditProfileDialog::previewColorScheme(const QModelIndex &index) { const QString &name = index.data(Qt::UserRole + 1).value()->name(); delayedPreview(Profile::ColorScheme, name); } void EditProfileDialog::showFontDialog() { if (!_fontDialog) { _fontDialog = new FontDialog(this); - connect(_fontDialog, &FontDialog::fontChanged, this, [this](const QFont font) { + connect(_fontDialog, &FontDialog::fontChanged, this, [this](const QFont &font) { preview(Profile::Font, font); updateFontPreview(font); }); connect(_fontDialog, &FontDialog::accepted, this, [this]() { const QFont font = _fontDialog->font(); preview(Profile::Font, font); updateTempProfileProperty(Profile::Font, font); updateFontPreview(font); }); connect(_fontDialog, &FontDialog::rejected, this, [this]() { unpreview(Profile::Font); const QFont font = _profile->font(); updateFontPreview(font); }); } const QFont font = _profile->font(); _fontDialog->setFont(font); _fontDialog->exec(); } void EditProfileDialog::updateFontPreview(QFont font) { bool aa = _profile->antiAliasFonts(); font.setStyleStrategy(aa ? QFont::PreferAntialias : QFont::NoAntialias); _appearanceUi->fontPreview->setFont(font); _appearanceUi->fontPreview->setText(QStringLiteral("%1 %2pt").arg(font.family()).arg(font.pointSize())); } void EditProfileDialog::removeColorScheme() { QModelIndexList selected = _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes(); if (selected.isEmpty()) { return; } // The actual delete runs async because we need to on-demand query // files managed by KNS. Deleting files managed by KNS screws up the // KNS states (entry gets shown as installed when in fact we deleted it). auto *manager = new KNSCore::DownloadManager(QStringLiteral("konsole.knsrc"), this); connect(manager, &KNSCore::DownloadManager::searchResult, this, [=](const KNSCore::EntryInternal::List &entries) { const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); Q_ASSERT(!name.isEmpty()); bool uninstalled = false; // Check if the theme was installed by KNS, if so uninstall it through // there and unload it. for (auto &entry : entries) { for (const auto &file : entry.installedFiles()) { if (ColorSchemeManager::colorSchemeNameFromPath(file) != name) { continue; } // Make sure the manager can unload it before uninstalling it. if (ColorSchemeManager::instance()->unloadColorScheme(file)) { manager->uninstallEntry(entry); uninstalled = true; } } if (uninstalled) { break; } } // If KNS wasn't able to remove it is a custom theme and we'll drop // it manually. if (!uninstalled) { uninstalled = ColorSchemeManager::instance()->deleteColorScheme(name); } if (uninstalled) { _appearanceUi->colorSchemeList->model()->removeRow(selected.first().row()); } manager->deleteLater(); }); manager->checkForInstalled(); } void EditProfileDialog::gotNewColorSchemes(const KNS3::Entry::List &changedEntries) { int failures = 0; for (auto &entry : changedEntries) { switch (entry.status()) { case KNS3::Entry::Installed: for (const auto &file : entry.installedFiles()) { if (ColorSchemeManager::instance()->loadColorScheme(file)) { continue; } qWarning() << "Failed to load file" << file; ++failures; } if (failures == entry.installedFiles().size()) { _appearanceUi->colorSchemeMessageWidget->setText( xi18nc("@info", "Scheme %1 failed to load.", entry.name())); _appearanceUi->colorSchemeMessageWidget->animatedShow(); QTimer::singleShot(8000, _appearanceUi->colorSchemeMessageWidget, &KMessageWidget::animatedHide); } break; case KNS3::Entry::Deleted: for (const auto &file : entry.uninstalledFiles()) { if (ColorSchemeManager::instance()->unloadColorScheme(file)) { continue; } qWarning() << "Failed to unload file" << file; // If unloading fails we do not care. Iff the scheme failed here // it either wasn't loaded or was invalid to begin with. } break; case KNS3::Entry::Invalid: case KNS3::Entry::Installing: case KNS3::Entry::Downloadable: case KNS3::Entry::Updateable: case KNS3::Entry::Updating: // Not interesting. break; } } updateColorSchemeList(currentColorSchemeName()); } void EditProfileDialog::resetColorScheme() { QModelIndexList selected = _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); ColorSchemeManager::instance()->deleteColorScheme(name); // select the colorScheme used in the current profile updateColorSchemeList(currentColorSchemeName()); } } void EditProfileDialog::showColorSchemeEditor(bool isNewScheme) { // Finding selected ColorScheme QModelIndexList selected = _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes(); QAbstractItemModel *model = _appearanceUi->colorSchemeList->model(); const ColorScheme *colors = nullptr; if (!selected.isEmpty()) { colors = model->data(selected.first(), Qt::UserRole + 1).value(); } else { colors = ColorSchemeManager::instance()->defaultColorScheme(); } Q_ASSERT(colors); // Setting up ColorSchemeEditor ui // close any running ColorSchemeEditor if (_colorDialog != nullptr) { closeColorSchemeEditor(); } _colorDialog = new ColorSchemeEditor(this); connect(_colorDialog, &Konsole::ColorSchemeEditor::colorSchemeSaveRequested, this, &Konsole::EditProfileDialog::saveColorScheme); _colorDialog->setup(colors, isNewScheme); _colorDialog->show(); } void EditProfileDialog::closeColorSchemeEditor() { if (_colorDialog != nullptr) { _colorDialog->close(); delete _colorDialog; } } void EditProfileDialog::newColorScheme() { showColorSchemeEditor(true); } void EditProfileDialog::editColorScheme() { showColorSchemeEditor(false); } void EditProfileDialog::saveColorScheme(const ColorScheme &scheme, bool isNewScheme) { auto newScheme = new ColorScheme(scheme); // if this is a new color scheme, pick a name based on the description if (isNewScheme) { newScheme->setName(newScheme->description()); } ColorSchemeManager::instance()->addColorScheme(newScheme); const QString &selectedColorSchemeName = newScheme->name(); // select the edited or the new colorScheme after saving the changes updateColorSchemeList(selectedColorSchemeName); preview(Profile::ColorScheme, newScheme->name()); } void EditProfileDialog::colorSchemeSelected() { QModelIndexList selected = _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { QAbstractItemModel *model = _appearanceUi->colorSchemeList->model(); const auto *colors = model->data(selected.first(), Qt::UserRole + 1).value(); if (colors != nullptr) { updateTempProfileProperty(Profile::ColorScheme, colors->name()); previewColorScheme(selected.first()); updateTransparencyWarning(); } } updateColorSchemeButtons(); } void EditProfileDialog::updateColorSchemeButtons() { enableIfNonEmptySelection(_appearanceUi->editColorSchemeButton, _appearanceUi->colorSchemeList->selectionModel()); QModelIndexList selected = _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); bool isResettable = ColorSchemeManager::instance()->canResetColorScheme(name); _appearanceUi->resetColorSchemeButton->setEnabled(isResettable); bool isDeletable = ColorSchemeManager::instance()->isColorSchemeDeletable(name); // if a colorScheme can be restored then it can't be deleted _appearanceUi->removeColorSchemeButton->setEnabled(isDeletable && !isResettable); } else { _appearanceUi->removeColorSchemeButton->setEnabled(false); _appearanceUi->resetColorSchemeButton->setEnabled(false); } } void EditProfileDialog::updateKeyBindingsButtons() { QModelIndexList selected = _keyboardUi->keyBindingList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { _keyboardUi->editKeyBindingsButton->setEnabled(true); const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); bool isResettable = _keyManager->isTranslatorResettable(name); _keyboardUi->resetKeyBindingsButton->setEnabled(isResettable); bool isDeletable = _keyManager->isTranslatorDeletable(name); // if a key bindings scheme can be reset then it can't be deleted _keyboardUi->removeKeyBindingsButton->setEnabled(isDeletable && !isResettable); } } void EditProfileDialog::enableIfNonEmptySelection(QWidget *widget, QItemSelectionModel *selectionModel) { widget->setEnabled(selectionModel->hasSelection()); } void EditProfileDialog::updateTransparencyWarning() { // zero or one indexes can be selected foreach (const QModelIndex &index, _appearanceUi->colorSchemeList->selectionModel()->selectedIndexes()) { bool needTransparency = index.data(Qt::UserRole + 1).value()->opacity() < 1.0; if (!needTransparency) { _appearanceUi->transparencyWarningWidget->setHidden(true); } else if (!KWindowSystem::compositingActive()) { _appearanceUi->transparencyWarningWidget->setText(i18n( "This color scheme uses a transparent background" " which does not appear to be supported on your" " desktop")); _appearanceUi->transparencyWarningWidget->setHidden(false); } else if (!WindowSystemInfo::HAVE_TRANSPARENCY) { _appearanceUi->transparencyWarningWidget->setText(i18n( "Konsole was started before desktop effects were enabled." " You need to restart Konsole to see transparent background.")); _appearanceUi->transparencyWarningWidget->setHidden(false); } } } void EditProfileDialog::createTempProfile() { _tempProfile = Profile::Ptr(new Profile); _tempProfile->setHidden(true); } void EditProfileDialog::updateTempProfileProperty(Profile::Property property, const QVariant &value) { _tempProfile->setProperty(property, value); updateButtonApply(); } void EditProfileDialog::updateButtonApply() { bool userModified = false; QHashIterator iter(_tempProfile->setProperties()); while (iter.hasNext()) { iter.next(); Profile::Property property = iter.key(); QVariant value = iter.value(); // for previewed property if (_previewedProperties.contains(static_cast(property))) { if (value != _previewedProperties.value(static_cast(property))) { userModified = true; break; } // for not-previewed property // // for the Profile::KeyBindings property, if it's set in the _tempProfile // then the user opened the edit key bindings dialog and clicked // OK, and could have add/removed a key bindings rule } else if (property == Profile::KeyBindings || (value != _profile->property(property))) { userModified = true; break; } } _buttonBox->button(QDialogButtonBox::Apply)->setEnabled(userModified); } void EditProfileDialog::setupKeyboardPage(const Profile::Ptr &/* profile */) { // setup translator list updateKeyBindingsList(lookupProfile()->keyBindings()); connect(_keyboardUi->keyBindingList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Konsole::EditProfileDialog::keyBindingSelected); connect(_keyboardUi->newKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::newKeyBinding); _keyboardUi->editKeyBindingsButton->setEnabled(false); _keyboardUi->removeKeyBindingsButton->setEnabled(false); _keyboardUi->resetKeyBindingsButton->setEnabled(false); updateKeyBindingsButtons(); connect(_keyboardUi->editKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::editKeyBinding); connect(_keyboardUi->removeKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::removeKeyBinding); connect(_keyboardUi->resetKeyBindingsButton, &QPushButton::clicked, this, &Konsole::EditProfileDialog::resetKeyBindings); } void EditProfileDialog::keyBindingSelected() { QModelIndexList selected = _keyboardUi->keyBindingList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { QAbstractItemModel *model = _keyboardUi->keyBindingList->model(); const auto *translator = model->data(selected.first(), Qt::UserRole + 1) .value(); if (translator != nullptr) { updateTempProfileProperty(Profile::KeyBindings, translator->name()); } } updateKeyBindingsButtons(); } void EditProfileDialog::removeKeyBinding() { QModelIndexList selected = _keyboardUi->keyBindingList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); if (KeyboardTranslatorManager::instance()->deleteTranslator(name)) { _keyboardUi->keyBindingList->model()->removeRow(selected.first().row()); } } } void EditProfileDialog::showKeyBindingEditor(bool isNewTranslator) { QModelIndexList selected = _keyboardUi->keyBindingList->selectionModel()->selectedIndexes(); QAbstractItemModel *model = _keyboardUi->keyBindingList->model(); const KeyboardTranslator *translator = nullptr; if (!selected.isEmpty()) { translator = model->data(selected.first(), Qt::UserRole + 1).value(); } else { translator = _keyManager->defaultTranslator(); } Q_ASSERT(translator); auto editor = new KeyBindingEditor(this); if (translator != nullptr) { editor->setup(translator, lookupProfile()->keyBindings(), isNewTranslator); } connect(editor, &Konsole::KeyBindingEditor::updateKeyBindingsListRequest, this, &Konsole::EditProfileDialog::updateKeyBindingsList); connect(editor, &Konsole::KeyBindingEditor::updateTempProfileKeyBindingsRequest, this, &Konsole::EditProfileDialog::updateTempProfileProperty); editor->exec(); } void EditProfileDialog::newKeyBinding() { showKeyBindingEditor(true); } void EditProfileDialog::editKeyBinding() { showKeyBindingEditor(false); } void EditProfileDialog::resetKeyBindings() { QModelIndexList selected = _keyboardUi->keyBindingList->selectionModel()->selectedIndexes(); if (!selected.isEmpty()) { const QString &name = selected.first().data(Qt::UserRole + 1).value()->name(); _keyManager->deleteTranslator(name); // find and load the translator _keyManager->findTranslator(name); updateKeyBindingsList(name); } } void EditProfileDialog::setupCheckBoxes(const QVector& options, const Profile::Ptr &profile) { for(const auto& option : options) { option.button->setChecked(profile->property(option.property)); connect(option.button, SIGNAL(toggled(bool)), this, option.slot); } } void EditProfileDialog::setupRadio(const QVector& possibilities, int actual) { for(const auto& possibility : possibilities) { possibility.button->setChecked(possibility.value == actual); connect(possibility.button, SIGNAL(clicked()), this, possibility.slot); } } void EditProfileDialog::setupButtonGroup(const ButtonGroupOptions &options, const Profile::Ptr &profile) { auto currentValue = profile->property(options.profileProperty); for (auto option: options.buttons) { options.group->setId(option.button, option.value); } Q_ASSERT(options.buttons.count() > 0); auto *activeButton = options.group->button(currentValue); if (!activeButton) { activeButton = options.buttons[0].button; } activeButton->setChecked(true); connect(options.group, QOverload::of(&QButtonGroup::buttonClicked), this, [this, options](int value) { if (options.preview) { preview(options.profileProperty, value); } updateTempProfileProperty(options.profileProperty, value); }); } void EditProfileDialog::setupScrollingPage(const Profile::Ptr &profile) { // setup scrollbar radio const ButtonGroupOptions scrollBarPositionOptions = { _scrollingUi->scrollBarPosition, // group Profile::ScrollBarPosition, // profileProperty false, // preview { // buttons {_scrollingUi->scrollBarRightButton, Enum::ScrollBarRight}, {_scrollingUi->scrollBarLeftButton, Enum::ScrollBarLeft}, {_scrollingUi->scrollBarHiddenButton, Enum::ScrollBarHidden}, }, }; setupButtonGroup(scrollBarPositionOptions, profile); // setup scrollback type radio auto scrollBackType = profile->property(Profile::HistoryMode); _scrollingUi->historySizeWidget->setMode(Enum::HistoryModeEnum(scrollBackType)); connect(_scrollingUi->historySizeWidget, &Konsole::HistorySizeWidget::historyModeChanged, this, &Konsole::EditProfileDialog::historyModeChanged); // setup scrollback line count spinner const int historySize = profile->historySize(); _scrollingUi->historySizeWidget->setLineCount(historySize); // setup scrollpageamount type radio auto scrollFullPage = profile->property(Profile::ScrollFullPage); const auto pageamounts = QVector{ {_scrollingUi->scrollHalfPage, Enum::ScrollPageHalf, SLOT(scrollHalfPage())}, {_scrollingUi->scrollFullPage, Enum::ScrollPageFull, SLOT(scrollFullPage())} }; setupRadio(pageamounts, scrollFullPage); // signals and slots connect(_scrollingUi->historySizeWidget, &Konsole::HistorySizeWidget::historySizeChanged, this, &Konsole::EditProfileDialog::historySizeChanged); } void EditProfileDialog::historySizeChanged(int lineCount) { updateTempProfileProperty(Profile::HistorySize, lineCount); } void EditProfileDialog::historyModeChanged(Enum::HistoryModeEnum mode) { updateTempProfileProperty(Profile::HistoryMode, mode); } void EditProfileDialog::scrollFullPage() { updateTempProfileProperty(Profile::ScrollFullPage, Enum::ScrollPageFull); } void EditProfileDialog::scrollHalfPage() { updateTempProfileProperty(Profile::ScrollFullPage, Enum::ScrollPageHalf); } void EditProfileDialog::setupMousePage(const Profile::Ptr &profile) { const auto options = QVector{ { _mouseUi->underlineLinksButton, Profile::UnderlineLinksEnabled, SLOT(toggleUnderlineLinks(bool)) }, { _mouseUi->underlineFilesButton, Profile::UnderlineFilesEnabled, SLOT(toggleUnderlineFiles(bool)) }, { _mouseUi->ctrlRequiredForDragButton, Profile::CtrlRequiredForDrag, SLOT(toggleCtrlRequiredForDrag(bool)) }, { _mouseUi->copyTextAsHTMLButton, Profile::CopyTextAsHTML, SLOT(toggleCopyTextAsHTML(bool)) }, { _mouseUi->copyTextToClipboardButton, Profile::AutoCopySelectedText, SLOT(toggleCopyTextToClipboard(bool)) }, { _mouseUi->trimLeadingSpacesButton, Profile::TrimLeadingSpacesInSelectedText, SLOT(toggleTrimLeadingSpacesInSelectedText(bool)) }, { _mouseUi->trimTrailingSpacesButton, Profile::TrimTrailingSpacesInSelectedText, SLOT(toggleTrimTrailingSpacesInSelectedText(bool)) }, { _mouseUi->openLinksByDirectClickButton, Profile::OpenLinksByDirectClickEnabled, SLOT(toggleOpenLinksByDirectClick(bool)) }, { _mouseUi->dropUrlsAsText, Profile::DropUrlsAsText, SLOT(toggleDropUrlsAsText(bool)) }, { _mouseUi->enableAlternateScrollingButton, Profile::AlternateScrolling, SLOT(toggleAlternateScrolling(bool)) } }; setupCheckBoxes(options, profile); // setup middle click paste mode const auto middleClickPasteMode = profile->property(Profile::MiddleClickPasteMode); const auto pasteModes = QVector { {_mouseUi->pasteFromX11SelectionButton, Enum::PasteFromX11Selection, SLOT(pasteFromX11Selection())}, {_mouseUi->pasteFromClipboardButton, Enum::PasteFromClipboard, SLOT(pasteFromClipboard())} }; setupRadio(pasteModes, middleClickPasteMode); // interaction options _mouseUi->wordCharacterEdit->setText(profile->wordCharacters()); connect(_mouseUi->wordCharacterEdit, &QLineEdit::textChanged, this, &Konsole::EditProfileDialog::wordCharactersChanged); const ButtonGroupOptions tripleClickModeOptions = { _mouseUi->tripleClickMode, // group Profile::TripleClickMode, // profileProperty false, // preview { // buttons {_mouseUi->tripleClickSelectsTheWholeLine, Enum::SelectWholeLine}, {_mouseUi->tripleClickSelectsFromMousePosition, Enum::SelectForwardsFromCursor}, }, }; setupButtonGroup(tripleClickModeOptions, profile); _mouseUi->openLinksByDirectClickButton->setEnabled(_mouseUi->underlineLinksButton->isChecked() || _mouseUi->underlineFilesButton->isChecked()); _mouseUi->enableMouseWheelZoomButton->setChecked(profile->mouseWheelZoomEnabled()); connect(_mouseUi->enableMouseWheelZoomButton, &QCheckBox::toggled, this, &Konsole::EditProfileDialog::toggleMouseWheelZoom); } void EditProfileDialog::setupAdvancedPage(const Profile::Ptr &profile) { const auto options = QVector{ { _advancedUi->enableBlinkingTextButton, Profile::BlinkingTextEnabled, SLOT(toggleBlinkingText(bool)) }, { _advancedUi->enableFlowControlButton, Profile::FlowControlEnabled, SLOT(toggleFlowControl(bool)) }, { _appearanceUi->enableBlinkingCursorButton, Profile::BlinkingCursorEnabled, SLOT(toggleBlinkingCursor(bool)) }, { _advancedUi->enableBidiRenderingButton, Profile::BidiRenderingEnabled, SLOT(togglebidiRendering(bool)) }, { _advancedUi->enableReverseUrlHints, Profile::ReverseUrlHints, SLOT(toggleReverseUrlHints(bool)) } }; setupCheckBoxes(options, profile); // Setup the URL hints modifier checkboxes { auto modifiers = profile->property(Profile::UrlHintsModifiers); _advancedUi->urlHintsModifierShift->setChecked((modifiers &Qt::ShiftModifier) != 0u); _advancedUi->urlHintsModifierCtrl->setChecked((modifiers &Qt::ControlModifier) != 0u); _advancedUi->urlHintsModifierAlt->setChecked((modifiers &Qt::AltModifier) != 0u); _advancedUi->urlHintsModifierMeta->setChecked((modifiers &Qt::MetaModifier) != 0u); connect(_advancedUi->urlHintsModifierShift, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); connect(_advancedUi->urlHintsModifierCtrl, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); connect(_advancedUi->urlHintsModifierAlt, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); connect(_advancedUi->urlHintsModifierMeta, &QCheckBox::toggled, this, &EditProfileDialog::updateUrlHintsModifier); } // encoding options auto codecAction = new KCodecAction(this); _advancedUi->selectEncodingButton->setMenu(codecAction->menu()); connect(codecAction, static_cast(&KCodecAction::triggered), this, &Konsole::EditProfileDialog::setDefaultCodec); _advancedUi->selectEncodingButton->setText(profile->defaultEncoding()); } int EditProfileDialog::maxSpinBoxWidth(const KPluralHandlingSpinBox *spinBox, const KLocalizedString &suffix) { static const int cursorWidth = 2; const QFontMetrics fm(spinBox->fontMetrics()); const QString plural = suffix.subs(2).toString(); const QString singular = suffix.subs(1).toString(); const QString min = QString::number(spinBox->minimum()); const QString max = QString::number(spinBox->maximum()); const int pluralWidth = fm.width(plural); const int singularWidth = fm.width(singular); const int minWidth = fm.width(min); const int maxWidth = fm.width(max); const int width = qMax(pluralWidth, singularWidth) + qMax(minWidth, maxWidth) + cursorWidth; // Based on QAbstractSpinBox::initStyleOption() from Qt QStyleOptionSpinBox opt; opt.initFrom(spinBox); opt.activeSubControls = QStyle::SC_None; opt.buttonSymbols = spinBox->buttonSymbols(); // Assume all spinboxes have buttons opt.subControls = QStyle::SC_SpinBoxFrame | QStyle::SC_SpinBoxEditField | QStyle::SC_SpinBoxUp | QStyle::SC_SpinBoxDown; opt.frame = spinBox->hasFrame(); const QSize hint(width, spinBox->sizeHint().height()); const QSize spinBoxSize = style()->sizeFromContents(QStyle::CT_SpinBox, &opt, hint, spinBox) .expandedTo(QApplication::globalStrut()); return spinBoxSize.width(); } void EditProfileDialog::setDefaultCodec(QTextCodec *codec) { QString name = QString::fromLocal8Bit(codec->name()); updateTempProfileProperty(Profile::DefaultEncoding, name); _advancedUi->selectEncodingButton->setText(name); } void EditProfileDialog::wordCharactersChanged(const QString &text) { updateTempProfileProperty(Profile::WordCharacters, text); } void EditProfileDialog::togglebidiRendering(bool enable) { updateTempProfileProperty(Profile::BidiRenderingEnabled, enable); } void EditProfileDialog::toggleUnderlineLinks(bool enable) { updateTempProfileProperty(Profile::UnderlineLinksEnabled, enable); bool enableClick = _mouseUi->underlineFilesButton->isChecked() || enable; _mouseUi->openLinksByDirectClickButton->setEnabled(enableClick); } void EditProfileDialog::toggleUnderlineFiles(bool enable) { updateTempProfileProperty(Profile::UnderlineFilesEnabled, enable); bool enableClick = _mouseUi->underlineLinksButton->isChecked() || enable; _mouseUi->openLinksByDirectClickButton->setEnabled(enableClick); } void EditProfileDialog::toggleCtrlRequiredForDrag(bool enable) { updateTempProfileProperty(Profile::CtrlRequiredForDrag, enable); } void EditProfileDialog::toggleDropUrlsAsText(bool enable) { updateTempProfileProperty(Profile::DropUrlsAsText, enable); } void EditProfileDialog::toggleOpenLinksByDirectClick(bool enable) { updateTempProfileProperty(Profile::OpenLinksByDirectClickEnabled, enable); } void EditProfileDialog::toggleCopyTextAsHTML(bool enable) { updateTempProfileProperty(Profile::CopyTextAsHTML, enable); } void EditProfileDialog::toggleCopyTextToClipboard(bool enable) { updateTempProfileProperty(Profile::AutoCopySelectedText, enable); } void EditProfileDialog::toggleTrimLeadingSpacesInSelectedText(bool enable) { updateTempProfileProperty(Profile::TrimLeadingSpacesInSelectedText, enable); } void EditProfileDialog::toggleTrimTrailingSpacesInSelectedText(bool enable) { updateTempProfileProperty(Profile::TrimTrailingSpacesInSelectedText, enable); } void EditProfileDialog::pasteFromX11Selection() { updateTempProfileProperty(Profile::MiddleClickPasteMode, Enum::PasteFromX11Selection); } void EditProfileDialog::pasteFromClipboard() { updateTempProfileProperty(Profile::MiddleClickPasteMode, Enum::PasteFromClipboard); } void EditProfileDialog::TripleClickModeChanged(int newValue) { updateTempProfileProperty(Profile::TripleClickMode, newValue); } void EditProfileDialog::updateUrlHintsModifier(bool) { Qt::KeyboardModifiers modifiers; if (_advancedUi->urlHintsModifierShift->isChecked()) { modifiers |= Qt::ShiftModifier; } if (_advancedUi->urlHintsModifierCtrl->isChecked()) { modifiers |= Qt::ControlModifier; } if (_advancedUi->urlHintsModifierAlt->isChecked()) { modifiers |= Qt::AltModifier; } if (_advancedUi->urlHintsModifierMeta->isChecked()) { modifiers |= Qt::MetaModifier; } updateTempProfileProperty(Profile::UrlHintsModifiers, int(modifiers)); } void EditProfileDialog::toggleReverseUrlHints(bool enable) { updateTempProfileProperty(Profile::ReverseUrlHints, enable); } void EditProfileDialog::toggleBlinkingText(bool enable) { updateTempProfileProperty(Profile::BlinkingTextEnabled, enable); } void EditProfileDialog::toggleFlowControl(bool enable) { updateTempProfileProperty(Profile::FlowControlEnabled, enable); } ColorSchemeViewDelegate::ColorSchemeViewDelegate(QObject *parent) : QAbstractItemDelegate(parent) { } void ColorSchemeViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { const auto *scheme = index.data(Qt::UserRole + 1).value(); QFont profileFont = index.data(Qt::UserRole + 2).value(); Q_ASSERT(scheme); if (scheme == nullptr) { return; } painter->setRenderHint(QPainter::Antialiasing); // Draw background QStyle *style = option.widget != nullptr ? option.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, option.widget); // Draw name QPalette::ColorRole textColor = ((option.state & QStyle::State_Selected) != 0) ? QPalette::HighlightedText : QPalette::Text; painter->setPen(option.palette.color(textColor)); painter->setFont(option.font); // Determine width of sample text using profile's font const QString sampleText = i18n("AaZz09..."); QFontMetrics profileFontMetrics(profileFont); const int sampleTextWidth = profileFontMetrics.width(sampleText); painter->drawText(option.rect.adjusted(sampleTextWidth + 15, 0, 0, 0), Qt::AlignLeft | Qt::AlignVCenter, index.data(Qt::DisplayRole).toString()); // Draw the preview const int x = option.rect.left(); const int y = option.rect.top(); QRect previewRect(x + 4, y + 4, sampleTextWidth + 8, option.rect.height() - 8); bool transparencyAvailable = KWindowSystem::compositingActive(); if (transparencyAvailable) { painter->save(); QColor color = scheme->backgroundColor(); color.setAlphaF(scheme->opacity()); painter->setPen(Qt::NoPen); painter->setCompositionMode(QPainter::CompositionMode_Source); painter->setBrush(color); painter->drawRect(previewRect); painter->restore(); } else { painter->setPen(Qt::NoPen); painter->setBrush(scheme->backgroundColor()); painter->drawRect(previewRect); } // draw color scheme name using scheme's foreground color QPen pen(scheme->foregroundColor()); painter->setPen(pen); // TODO: respect antialias setting painter->setFont(profileFont); painter->drawText(previewRect, Qt::AlignCenter, sampleText); } QSize ColorSchemeViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex & /*index*/) const { const int width = 200; const int margin = 5; const int colorWidth = width / TABLE_COLORS; const int heightForWidth = (colorWidth * 2) + option.fontMetrics.height() + margin; // temporary return {width, heightForWidth}; } diff --git a/src/EditProfileDialog.h b/src/EditProfileDialog.h index 0e987e71..21e46e60 100644 --- a/src/EditProfileDialog.h +++ b/src/EditProfileDialog.h @@ -1,475 +1,475 @@ /* Copyright 2007-2008 by Robert Knight Copyright 2018 by Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef EDITPROFILEDIALOG_H #define EDITPROFILEDIALOG_H // Qt #include #include #include #include #include #include #include #include // KDE #include #include // Konsole #include "Profile.h" #include "Enumeration.h" #include "ColorScheme.h" #include "ColorSchemeEditor.h" #include "KeyboardTranslatorManager.h" #include "FontDialog.h" class KPluralHandlingSpinBox; class KLocalizedString; class QItemSelectionModel; namespace Ui { class EditProfileGeneralPage; class EditProfileTabsPage; class EditProfileAppearancePage; class EditProfileScrollingPage; class EditProfileKeyboardPage; class EditProfileMousePage; class EditProfileAdvancedPage; } namespace Konsole { /** * A dialog which allows the user to edit a profile. * After the dialog is created, it can be initialized with the settings * for a profile using setProfile(). When the user makes changes to the * dialog and accepts the changes, the dialog will update the * profile in the SessionManager by calling the SessionManager's * changeProfile() method. * * Some changes made in the dialog are preview-only changes which cause * the SessionManager's changeProfile() method to be called with * the persistent argument set to false. These changes are then * un-done when the dialog is closed. */ class KONSOLEPRIVATE_EXPORT EditProfileDialog: public KPageDialog { Q_OBJECT public: /** Constructs a new dialog with the specified parent. */ explicit EditProfileDialog(QWidget *parent = nullptr); ~EditProfileDialog() Q_DECL_OVERRIDE; /** * Initializes the dialog with the settings for the specified session * type. * * When the dialog closes, the profile will be updated in the SessionManager * with the altered settings. * * @param profile The profile to be edited */ - void setProfile(Profile::Ptr profile); + void setProfile(const Profile::Ptr &profile); /** * Selects the text in the profile name edit area. * When the dialog is being used to create a new profile, * this can be used to draw the user's attention to the profile name * and make it easy for them to change it. */ void selectProfileName(); const Profile::Ptr lookupProfile() const; public Q_SLOTS: // reimplemented void accept() Q_DECL_OVERRIDE; // reimplemented void reject() Q_DECL_OVERRIDE; void apply(); protected: bool eventFilter(QObject *watched, QEvent *event) Q_DECL_OVERRIDE; private Q_SLOTS: QSize sizeHint() const override; // sets up the specified tab page if necessary void preparePage(KPageWidgetItem *current, KPageWidgetItem *before = nullptr); // saves changes to profile void save(); // general page void selectInitialDir(); void selectIcon(); void profileNameChanged(const QString &name); void initialDirChanged(const QString &dir); void startInSameDir(bool); void commandChanged(const QString &command); void tabTitleFormatChanged(const QString &format); void remoteTabTitleFormatChanged(const QString &format); void terminalColumnsEntryChanged(int); void terminalRowsEntryChanged(int); void showTerminalSizeHint(bool); void setDimWhenInactive(bool); void showEnvironmentEditor(); void silenceSecondsChanged(int); // appearance page void setAntialiasText(bool enable); void setBoldIntense(bool enable); void useFontLineCharacters(bool enable); void newColorScheme(); void editColorScheme(); void saveColorScheme(const ColorScheme &scheme, bool isNewScheme); void removeColorScheme(); void gotNewColorSchemes(const KNS3::Entry::List &changedEntries); void toggleBlinkingCursor(bool); void setCursorShape(int); void autoCursorColor(); void customCursorColor(); void customCursorColorChanged(const QColor &); void terminalMarginChanged(int margin); void lineSpacingChanged(int); void setTerminalCenter(bool enable); /** * Deletes the selected colorscheme from the user's home dir location * so that the original one from the system-wide location can be used * instead */ void resetColorScheme(); void colorSchemeSelected(); void previewColorScheme(const QModelIndex &index); void showFontDialog(); void toggleMouseWheelZoom(bool enable); // scrolling page void historyModeChanged(Enum::HistoryModeEnum mode); void historySizeChanged(int); void scrollFullPage(); void scrollHalfPage(); // keyboard page void editKeyBinding(); void newKeyBinding(); void keyBindingSelected(); void removeKeyBinding(); void resetKeyBindings(); // mouse page void toggleUnderlineFiles(bool enable); void toggleUnderlineLinks(bool); void toggleOpenLinksByDirectClick(bool); void toggleCtrlRequiredForDrag(bool); void toggleDropUrlsAsText(bool); void toggleCopyTextToClipboard(bool); void toggleCopyTextAsHTML(bool); void toggleTrimLeadingSpacesInSelectedText(bool); void toggleTrimTrailingSpacesInSelectedText(bool); void pasteFromX11Selection(); void pasteFromClipboard(); void toggleAlternateScrolling(bool enable); void TripleClickModeChanged(int); void wordCharactersChanged(const QString &); // advanced page void toggleBlinkingText(bool); void toggleFlowControl(bool); void togglebidiRendering(bool); void updateUrlHintsModifier(bool); void toggleReverseUrlHints(bool); void setDefaultCodec(QTextCodec *); // apply the first previewed changes stored up by delayedPreview() void delayedPreviewActivate(); private: Q_DISABLE_COPY(EditProfileDialog) enum PageID { GeneralPage = 0, TabsPage, AppearancePage, ScrollingPage, KeyboardPage, MousePage, AdvancedPage, PagesCount, }; // initialize various pages of the dialog void setupGeneralPage(const Profile::Ptr &profile); void setupTabsPage(const Profile::Ptr &profile); void setupAppearancePage(const Profile::Ptr &profile); void setupKeyboardPage(const Profile::Ptr &profile); void setupScrollingPage(const Profile::Ptr &profile); void setupAdvancedPage(const Profile::Ptr &profile); void setupMousePage(const Profile::Ptr &profile); int maxSpinBoxWidth(const KPluralHandlingSpinBox *spinBox, const KLocalizedString &suffix); // Returns the name of the colorScheme used in the current profile const QString currentColorSchemeName() const; // select @p selectedColorSchemeName after the changes are saved // in the colorScheme editor void updateColorSchemeList(const QString &selectedColorSchemeName = QString()); void updateColorSchemeButtons(); // Convenience method KeyboardTranslatorManager *_keyManager = KeyboardTranslatorManager::instance(); // Updates the key bindings list widget on the Keyboard tab and selects // @p selectKeyBindingsName void updateKeyBindingsList(const QString &selectKeyBindingsName = QString()); void updateKeyBindingsButtons(); void showKeyBindingEditor(bool isNewTranslator); void showColorSchemeEditor(bool isNewScheme); void closeColorSchemeEditor(); void preview(int property, const QVariant &value); void delayedPreview(int property, const QVariant &value); void unpreview(int property); void unpreviewAll(); void enableIfNonEmptySelection(QWidget *widget, QItemSelectionModel *selectionModel); - void updateCaption(const Profile::Ptr profile); + void updateCaption(const Profile::Ptr &profile); void updateTransparencyWarning(); void updateFontPreview(QFont font); // Update _tempProfile in a way of respecting the apply button. // When used with some previewed property, this method should // always come after the preview operation. void updateTempProfileProperty(Profile::Property, const QVariant &value); // helper method for creating an empty & hidden profile and assigning // it to _tempProfile. void createTempProfile(); // Enable or disable apply button, used only within // updateTempProfileProperty(). void updateButtonApply(); - static QString groupProfileNames(const ProfileGroup::Ptr group, int maxLength = -1); + static QString groupProfileNames(const ProfileGroup::Ptr &group, int maxLength = -1); struct RadioOption { QAbstractButton *button; int value; const char *slot; }; void setupRadio(const QVector& possibilities, int actual); struct BooleanOption { QAbstractButton *button; Profile::Property property; const char *slot; }; void setupCheckBoxes(const QVector& options, const Profile::Ptr &profile); struct ButtonGroupOption { QAbstractButton *button; int value; }; struct ButtonGroupOptions { QButtonGroup *group; Profile::Property profileProperty; bool preview; QVector buttons; }; void setupButtonGroup(const ButtonGroupOptions &options, const Profile::Ptr &profile); // returns false if: // - the profile name is empty // - the name matches the name of an already existing profile // - the existing profile config file is read-only // otherwise returns true. bool isValidProfileName(); Ui::EditProfileGeneralPage *_generalUi; Ui::EditProfileTabsPage *_tabsUi; Ui::EditProfileAppearancePage *_appearanceUi; Ui::EditProfileScrollingPage *_scrollingUi; Ui::EditProfileKeyboardPage *_keyboardUi; Ui::EditProfileMousePage *_mouseUi; Ui::EditProfileAdvancedPage *_advancedUi; using PageSetupMethod = void (EditProfileDialog::*)(const Profile::Ptr &); struct Page { Page(PageSetupMethod setupPage = nullptr, bool needsUpdate = false) : setupPage(setupPage) , needsUpdate(needsUpdate) {} PageSetupMethod setupPage; bool needsUpdate; }; QMap _pages; Profile::Ptr _tempProfile; Profile::Ptr _profile; QHash _previewedProperties; QHash _delayedPreviewProperties; QTimer *_delayedPreviewTimer; ColorSchemeEditor *_colorDialog; QDialogButtonBox *_buttonBox; FontDialog *_fontDialog; }; /** * A delegate which can display and edit color schemes in a view. */ class ColorSchemeViewDelegate : public QAbstractItemDelegate { Q_OBJECT public: explicit ColorSchemeViewDelegate(QObject *parent = nullptr); // reimplemented void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; }; /** * An utility class for aligning 0th column in multiple QGridLayouts. * * Limitations: * - a layout can't be nested in another layout * - reference widget must be an ancestor of all added layouts * - only 0th column is processed (widgets spanning multiple columns * are ignored) */ class LabelsAligner: public QObject { Q_OBJECT public: explicit LabelsAligner(QWidget *refWidget): _refWidget(refWidget) {} void addLayout(QGridLayout *layout) { _layouts.append(layout); } void addLayouts(const QVector &layouts) { _layouts.append(layouts); } void setReferenceWidget(QWidget *refWidget) { _refWidget = refWidget; } public Q_SLOTS: void updateLayouts() { for (const auto *layout: qAsConst(_layouts)) { QWidget *widget = layout->parentWidget(); Q_ASSERT(widget); do { QLayout *widgetLayout = widget->layout(); if (widgetLayout) { widgetLayout->update(); widgetLayout->activate(); } widget = widget->parentWidget(); } while (widget != _refWidget && widget != nullptr); } } void align() { Q_ASSERT(_refWidget); if (_layouts.count() <= 1) { return; } int maxRight = 0; for (const auto *layout: qAsConst(_layouts)) { int left = getLeftMargin(layout); for (int row = 0; row < layout->rowCount(); ++row) { QLayoutItem *layoutItem = layout->itemAtPosition(row, LABELS_COLUMN); if (!layoutItem) { continue; } QWidget *widget = layoutItem->widget(); if (!widget) { continue; } const int idx = layout->indexOf(widget); int rows, cols, rowSpan, colSpan; layout->getItemPosition(idx, &rows, &cols, &rowSpan, &colSpan); if (colSpan > 1) { continue; } const int right = left + widget->sizeHint().width(); if (maxRight < right) { maxRight = right; } } } for (auto *l: qAsConst(_layouts)) { int left = getLeftMargin(l); l->setColumnMinimumWidth(LABELS_COLUMN, maxRight - left); } } private: int getLeftMargin(const QGridLayout *layout) { int left = layout->contentsMargins().left(); if (layout->parent()->isWidgetType()) { auto *parentWidget = layout->parentWidget(); Q_ASSERT(parentWidget); left += parentWidget->contentsMargins().left(); } else { auto *parentLayout = qobject_cast(layout->parent()); Q_ASSERT(parentLayout); left += parentLayout->contentsMargins().left(); } QWidget *parent = layout->parentWidget(); while (parent != _refWidget && parent != nullptr) { left = parent->mapToParent(QPoint(left, 0)).x(); parent = parent->parentWidget(); } return left; } static constexpr int LABELS_COLUMN = 0; QWidget *_refWidget; QVector _layouts; }; } #endif // EDITPROFILEDIALOG_H diff --git a/src/Filter.cpp b/src/Filter.cpp index b11e6aea..1a6d7995 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -1,642 +1,642 @@ /* 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 "Filter.h" #include "konsoledebug.h" // Qt #include #include #include #include #include #include #include #include #include // KDE #include #include // Konsole #include "Session.h" #include "TerminalCharacterDecoder.h" using namespace Konsole; FilterChain::~FilterChain() { QMutableListIterator iter(*this); while (iter.hasNext()) { Filter *filter = iter.next(); iter.remove(); delete filter; } } void FilterChain::addFilter(Filter *filter) { append(filter); } void FilterChain::removeFilter(Filter *filter) { removeAll(filter); } void FilterChain::reset() { QListIterator iter(*this); while (iter.hasNext()) { iter.next()->reset(); } } void FilterChain::setBuffer(const QString *buffer, const QList *linePositions) { QListIterator iter(*this); while (iter.hasNext()) { iter.next()->setBuffer(buffer, linePositions); } } void FilterChain::process() { QListIterator iter(*this); while (iter.hasNext()) { iter.next()->process(); } } void FilterChain::clear() { QList::clear(); } Filter::HotSpot *FilterChain::hotSpotAt(int line, int column) const { QListIterator iter(*this); while (iter.hasNext()) { Filter *filter = iter.next(); Filter::HotSpot *spot = filter->hotSpotAt(line, column); if (spot != nullptr) { return spot; } } return nullptr; } QList FilterChain::hotSpots() const { QList list; QListIterator iter(*this); while (iter.hasNext()) { Filter *filter = iter.next(); list << filter->hotSpots(); } return list; } TerminalImageFilterChain::TerminalImageFilterChain() : _buffer(nullptr), _linePositions(nullptr) { } TerminalImageFilterChain::~TerminalImageFilterChain() { delete _buffer; delete _linePositions; } void TerminalImageFilterChain::setImage(const Character * const image, int lines, int columns, const QVector &lineProperties) { if (empty()) { return; } // reset all filters and hotspots reset(); PlainTextDecoder decoder; decoder.setLeadingWhitespace(true); decoder.setTrailingWhitespace(true); // setup new shared buffers for the filters to process on auto newBuffer = new QString(); auto newLinePositions = new QList(); setBuffer(newBuffer, newLinePositions); // free the old buffers delete _buffer; delete _linePositions; _buffer = newBuffer; _linePositions = newLinePositions; QTextStream lineStream(_buffer); decoder.begin(&lineStream); for (int i = 0; i < lines; i++) { _linePositions->append(_buffer->length()); decoder.decodeLine(image + i * columns, columns, LINE_DEFAULT); // pretend that each line ends with a newline character. // this prevents a link that occurs at the end of one line // being treated as part of a link that occurs at the start of the next line // // the downside is that links which are spread over more than one line are not // highlighted. // // TODO - Use the "line wrapped" attribute associated with lines in a // terminal image to avoid adding this imaginary character for wrapped // lines if ((lineProperties.value(i, LINE_DEFAULT) & LINE_WRAPPED) == 0) { lineStream << QLatin1Char('\n'); } } decoder.end(); } Filter::Filter() : _hotspots(QMultiHash()), _hotspotList(QList()), _linePositions(nullptr), _buffer(nullptr) { } Filter::~Filter() { QListIterator iter(_hotspotList); while (iter.hasNext()) { delete iter.next(); } } void Filter::reset() { _hotspots.clear(); _hotspotList.clear(); } void Filter::setBuffer(const QString *buffer, const QList *linePositions) { _buffer = buffer; _linePositions = linePositions; } void Filter::getLineColumn(int position, int &startLine, int &startColumn) { Q_ASSERT(_linePositions); Q_ASSERT(_buffer); for (int i = 0; i < _linePositions->count(); i++) { int nextLine = 0; if (i == _linePositions->count() - 1) { nextLine = _buffer->length() + 1; } else { nextLine = _linePositions->value(i + 1); } if (_linePositions->value(i) <= position && position < nextLine) { startLine = i; startColumn = Character::stringWidth(buffer()->mid(_linePositions->value(i), position - _linePositions->value(i))); return; } } } const QString *Filter::buffer() { return _buffer; } Filter::HotSpot::~HotSpot() = default; void Filter::addHotSpot(HotSpot *spot) { _hotspotList << spot; for (int line = spot->startLine(); line <= spot->endLine(); line++) { _hotspots.insert(line, spot); } } QList Filter::hotSpots() const { return _hotspotList; } QList Filter::hotSpotsAtLine(int line) const { return _hotspots.values(line); } Filter::HotSpot *Filter::hotSpotAt(int line, int column) const { QList hotspots = _hotspots.values(line); foreach (HotSpot *spot, hotspots) { if (spot->startLine() == line && spot->startColumn() > column) { continue; } if (spot->endLine() == line && spot->endColumn() < column) { continue; } return spot; } return nullptr; } Filter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn) : _startLine(startLine), _startColumn(startColumn), _endLine(endLine), _endColumn(endColumn), _type(NotSpecified) { } QList Filter::HotSpot::actions() { return QList(); } int Filter::HotSpot::startLine() const { return _startLine; } int Filter::HotSpot::endLine() const { return _endLine; } int Filter::HotSpot::startColumn() const { return _startColumn; } int Filter::HotSpot::endColumn() const { return _endColumn; } Filter::HotSpot::Type Filter::HotSpot::type() const { return _type; } void Filter::HotSpot::setType(Type type) { _type = type; } RegExpFilter::RegExpFilter() : _searchText(QRegularExpression()) { } RegExpFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn, const QStringList &capturedTexts) : Filter::HotSpot(startLine, startColumn, endLine, endColumn), _capturedTexts(capturedTexts) { setType(Marker); } void RegExpFilter::HotSpot::activate(QObject *) { } QStringList RegExpFilter::HotSpot::capturedTexts() const { return _capturedTexts; } void RegExpFilter::setRegExp(const QRegularExpression ®Exp) { _searchText = regExp; _searchText.optimize(); } QRegularExpression RegExpFilter::regExp() const { return _searchText; } void RegExpFilter::process() { const QString *text = buffer(); Q_ASSERT(text); if (!_searchText.isValid() || _searchText.pattern().isEmpty()) { return; } QRegularExpressionMatchIterator iterator(_searchText.globalMatch(*text)); while (iterator.hasNext()) { QRegularExpressionMatch match(iterator.next()); int startLine = 0; int endLine = 0; int startColumn = 0; int endColumn = 0; getLineColumn(match.capturedStart(), startLine, startColumn); getLineColumn(match.capturedEnd(), endLine, endColumn); RegExpFilter::HotSpot *spot = newHotSpot(startLine, startColumn, endLine, endColumn, match.capturedTexts()); if (spot == nullptr) { continue; } addHotSpot(spot); } } RegExpFilter::HotSpot *RegExpFilter::newHotSpot(int startLine, int startColumn, int endLine, int endColumn, const QStringList &capturedTexts) { return new RegExpFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts); } RegExpFilter::HotSpot *UrlFilter::newHotSpot(int startLine, int startColumn, int endLine, int endColumn, const QStringList &capturedTexts) { return new UrlFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts); } UrlFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn, const QStringList &capturedTexts) : RegExpFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts), _urlObject(new FilterObject(this)) { setType(Link); } UrlFilter::HotSpot::UrlType UrlFilter::HotSpot::urlType() const { const QString url = capturedTexts().at(0); if (FullUrlRegExp.match(url).hasMatch()) { return StandardUrl; } else if (EmailAddressRegExp.match(url).hasMatch()) { return Email; } else { return Unknown; } } void UrlFilter::HotSpot::activate(QObject *object) { QString url = capturedTexts().at(0); const UrlType kind = urlType(); const QString &actionName = object != nullptr ? object->objectName() : QString(); if (actionName == QLatin1String("copy-action")) { QApplication::clipboard()->setText(url); return; } if ((object == nullptr) || actionName == QLatin1String("open-action")) { if (kind == StandardUrl) { // if the URL path does not include the protocol ( eg. "www.kde.org" ) then // prepend http:// ( eg. "www.kde.org" --> "http://www.kde.org" ) if (!url.contains(QLatin1String("://"))) { url.prepend(QLatin1String("http://")); } } else if (kind == Email) { url.prepend(QLatin1String("mailto:")); } new KRun(QUrl(url), QApplication::activeWindow()); } } // Note: Altering these regular expressions can have a major effect on the performance of the filters // used for finding URLs in the text, especially if they are very general and could match very long // pieces of text. // Please be careful when altering them. //regexp matches: // full url: // protocolname:// or www. followed by anything other than whitespaces, <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :, comma and dot const QRegularExpression UrlFilter::FullUrlRegExp(QStringLiteral("(www\\.(?!\\.)|[a-z][a-z0-9+.-]*://)[^\\s<>'\"]+[^!,\\.\\s<>'\"\\]\\)\\:]"), QRegularExpression::OptimizeOnFirstUsageOption); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] const QRegularExpression UrlFilter::EmailAddressRegExp(QStringLiteral("\\b(\\w|\\.|-|\\+)+@(\\w|\\.|-)+\\.\\w+\\b"), QRegularExpression::OptimizeOnFirstUsageOption); // matches full url or email address const QRegularExpression UrlFilter::CompleteUrlRegExp(QLatin1Char('(') + FullUrlRegExp.pattern() + QLatin1Char('|') + EmailAddressRegExp.pattern() + QLatin1Char(')'), QRegularExpression::OptimizeOnFirstUsageOption); UrlFilter::UrlFilter() { setRegExp(CompleteUrlRegExp); } UrlFilter::HotSpot::~HotSpot() { delete _urlObject; } void FilterObject::activated() { _filter->activate(sender()); } QList UrlFilter::HotSpot::actions() { auto openAction = new QAction(_urlObject); auto copyAction = new QAction(_urlObject); const UrlType kind = urlType(); Q_ASSERT(kind == StandardUrl || kind == Email); if (kind == StandardUrl) { openAction->setText(i18n("Open Link")); copyAction->setText(i18n("Copy Link Address")); } else if (kind == Email) { openAction->setText(i18n("Send Email To...")); copyAction->setText(i18n("Copy Email Address")); } // object names are set here so that the hotspot performs the // correct action when activated() is called with the triggered // action passed as a parameter. openAction->setObjectName(QStringLiteral("open-action")); copyAction->setObjectName(QStringLiteral("copy-action")); QObject::connect(openAction, &QAction::triggered, _urlObject, &Konsole::FilterObject::activated); QObject::connect(copyAction, &QAction::triggered, _urlObject, &Konsole::FilterObject::activated); QList actions; actions << openAction; actions << copyAction; return actions; } /** * File Filter - Construct a filter that works on local file paths using the * posix portable filename character set combined with KDE's mimetype filename * extension blob patterns. * http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_267 */ RegExpFilter::HotSpot *FileFilter::newHotSpot(int startLine, int startColumn, int endLine, int endColumn, const QStringList &capturedTexts) { if (_session.isNull()) { qCDebug(KonsoleDebug) << "Trying to create new hot spot without session!"; return nullptr; } QString filename = capturedTexts.first(); if (filename.startsWith(QLatin1Char('\'')) && filename.endsWith(QLatin1Char('\''))) { filename.remove(0, 1); filename.chop(1); } if (!_currentFiles.contains(filename)) { return nullptr; } return new FileFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts, _dirPath + filename); } void FileFilter::process() { const QDir dir(_session->currentWorkingDirectory()); _dirPath = dir.canonicalPath() + QLatin1Char('/'); _currentFiles = dir.entryList(QDir::Files).toSet(); RegExpFilter::process(); } FileFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn, const QStringList &capturedTexts, const QString &filePath) : RegExpFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts), _fileObject(new FilterObject(this)), _filePath(filePath) { setType(Link); } void FileFilter::HotSpot::activate(QObject *) { new KRun(QUrl::fromLocalFile(_filePath), QApplication::activeWindow()); } -static QString createFileRegex(const QStringList &patterns, const QString &filePattern, const QString pathPattern) +static QString createFileRegex(const QStringList &patterns, const QString &filePattern, const QString &pathPattern) { QStringList suffixes = patterns.filter(QRegularExpression(QStringLiteral("^\\*") + filePattern + QStringLiteral("$"))); QStringList prefixes = patterns.filter(QRegularExpression(QStringLiteral("^") + filePattern + QStringLiteral("+\\*$"))); QStringList fullNames = patterns.filter(QRegularExpression(QStringLiteral("^") + filePattern + QStringLiteral("$"))); suffixes.replaceInStrings(QStringLiteral("*"), QString()); suffixes.replaceInStrings(QStringLiteral("."), QStringLiteral("\\.")); prefixes.replaceInStrings(QStringLiteral("*"), QString()); prefixes.replaceInStrings(QStringLiteral("."), QStringLiteral("\\.")); return QString( // Optional path in front pathPattern + QLatin1Char('?') + // Files with known suffixes QLatin1Char('(') + filePattern + QLatin1String("(") + suffixes.join(QLatin1Char('|')) + QLatin1String(")") + // Files with known prefixes QLatin1String("|") + QLatin1String("(") + prefixes.join(QLatin1Char('|')) + QLatin1String(")") + filePattern + // Files with known full names QLatin1String("|") + fullNames.join(QLatin1Char('|')) + QLatin1String(")") ); } FileFilter::FileFilter(Session *session) : _session(session) , _dirPath(QString()) , _currentFiles(QSet()) { QStringList patterns; QMimeDatabase mimeDatabase; const QList allMimeTypes = mimeDatabase.allMimeTypes(); for (const QMimeType &mimeType : allMimeTypes) { patterns.append(mimeType.globPatterns()); } patterns.removeDuplicates(); QString validFilename(QStringLiteral("[A-Za-z0-9\\._\\-]+")); QString pathRegex(QStringLiteral("([A-Za-z0-9\\._\\-/]+/)")); QString noSpaceRegex = QLatin1String("\\b") + createFileRegex(patterns, validFilename, pathRegex) + QLatin1String("\\b"); QString spaceRegex = QLatin1String("'") + createFileRegex(patterns, validFilename, pathRegex) + QLatin1String("'"); QString regex = QLatin1String("(") + noSpaceRegex + QLatin1String(")|(") + spaceRegex + QLatin1String(")"); setRegExp(QRegularExpression(regex, QRegularExpression::DontCaptureOption)); } FileFilter::HotSpot::~HotSpot() { delete _fileObject; } QList FileFilter::HotSpot::actions() { auto openAction = new QAction(_fileObject); openAction->setText(i18n("Open File")); QObject::connect(openAction, &QAction::triggered, _fileObject, &Konsole::FilterObject::activated); QList actions; actions << openAction; return actions; } diff --git a/src/FontDialog.h b/src/FontDialog.h index dae5781c..7c3893d5 100644 --- a/src/FontDialog.h +++ b/src/FontDialog.h @@ -1,52 +1,52 @@ /* Copyright 2018 by Mariusz Glebocki This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef FONTDIALOG_H #define FONTDIALOG_H // Qt #include #include #include #include #include namespace Konsole { class FontDialog: public QDialog { Q_OBJECT public: explicit FontDialog(QWidget *parent = nullptr); QFont font() const { return _fontChooser->font(); } void setFont(const QFont &font); Q_SIGNALS: - void fontChanged(QFont font); + void fontChanged(const QFont &font); private: KFontChooser *_fontChooser; QCheckBox *_showAllFonts; QToolButton *_showAllFontsWarningButton; QDialogButtonBox *_buttonBox; }; } #endif // FONTDIALOG_H diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index c1a321c3..ea3d26ae 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,890 +1,890 @@ /* 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 #include // KDE #include #include #include #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/FileLocationSettings.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::viewDetached, this, &Konsole::MainWindow::viewDetached); 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(false, 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()) { 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()) { menuItem->setText(menuItem->text().replace(QLatin1Char('&'), QString())); } } void MainWindow::restoreMenuAccelerators() { foreach (QAction *menuItem, menuBar()->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); } } void MainWindow::activeViewChanged(SessionController *controller) { // 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, [this] { newTab(_viewManager->activeContainer());}); 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, - [this](Profile::Ptr profile) { newFromProfile(_viewManager->activeContainer(), profile);}); + [this](const Profile::Ptr &profile) { newFromProfile(_viewManager->activeContainer(), profile);}); 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) { _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(); Q_FOREACH (const auto &url, urls) { if (url.isLocalFile()) { createSession(_viewManager->activeContainer(), defaultProfile, url.path()); } else if (url.scheme() == QLatin1String("ssh")) { createSSHSession(_viewManager->activeContainer(), defaultProfile, url); } } } void MainWindow::newTab(TabbedViewContainer *tabWidget) { Profile::Ptr defaultProfile = ProfileManager::instance()->defaultProfile(); createSession(tabWidget, defaultProfile, activeSessionDir()); } void MainWindow::cloneTab() { Q_ASSERT(_pluggedController); Session *session = _pluggedController->session(); Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); if (profile) { createSession(_viewManager->activeContainer(), profile, activeSessionDir()); } else { // something must be wrong: every session should be associated with profile Q_ASSERT(false); newTab(_viewManager->activeContainer()); } } Session *MainWindow::createSession(TabbedViewContainer *tabWidget, Profile::Ptr profile, const QString &directory) { if (!profile) { profile = ProfileManager::instance()->defaultProfile(); } Session *session = SessionManager::instance()->createSession(profile); if (!directory.isEmpty() && profile->startInCurrentSessionDir()) { session->setInitialWorkingDirectory(directory); } session->addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_WINDOW=/Windows/%1").arg(_viewManager->managerId())); // 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 _viewManager->createView(tabWidget, session); return session; } Session *MainWindow::createSSHSession(TabbedViewContainer *tabWidget, 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 _viewManager->createView(tabWidget, session); 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) { 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(), true); } int result; if (!processesRunning.isEmpty()) { 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(), QStringLiteral("CloseAllTabs")); } else { result = KMessageBox::warningYesNoCancel(this, i18nc("@info", "There are %1 open tabs 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(), QStringLiteral("CloseAllEmptyTabs")); } switch (result) { case KMessageBox::Yes: return true; case KMessageBox::No: if ((!_pluggedController.isNull()) && (!_pluggedController->session().isNull())) { disconnectController(_pluggedController); _pluggedController->session()->closeInNormalWay(); } 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()) { 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()) { 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()) { 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()) { controller->reloadXML(); if ((controller->factory() != nullptr) && controller != _pluggedController) { syncActiveShortcuts(controller->actionCollection(), _pluggedController->actionCollection()); } } } } -void MainWindow::newFromProfile(TabbedViewContainer *tabWidget, Profile::Ptr profile) +void MainWindow::newFromProfile(TabbedViewContainer *tabWidget, const Profile::Ptr &profile) { createSession(tabWidget, profile, activeSessionDir()); } void MainWindow::showManageProfilesDialog() { showSettingsDialog(true); } void MainWindow::showSettingsDialog(const bool showProfilePage) { if (KConfigDialog::showDialog(QStringLiteral("settings"))) { return; } KConfigDialog *settingsDialog = new KConfigDialog(this, QStringLiteral("settings"), KonsoleSettings::self()); settingsDialog->setFaceType(KPageDialog::Tabbed); auto generalSettings = new GeneralSettings(settingsDialog); settingsDialog->addPage(generalSettings, i18nc("@title Preferences page name", "General"), QStringLiteral("utilities-terminal")); auto profileSettings = new ProfileSettings(settingsDialog); KPageWidgetItem *profilePage = settingsDialog->addPage(profileSettings, i18nc("@title Preferences page name", "Profiles"), QStringLiteral("configure")); auto tabBarSettings = new TabBarSettings(settingsDialog); settingsDialog->addPage(tabBarSettings, i18nc("@title Preferences page name", "TabBar"), QStringLiteral("system-run")); auto fileLocationSettings = new FileLocationSettings(settingsDialog); settingsDialog->addPage(fileLocationSettings, i18nc("@title Preferences page name", "File Location"), QStringLiteral("configure")); if (showProfilePage) { settingsDialog->setCurrentPage(profilePage); } settingsDialog->show(); } void MainWindow::applyKonsoleSettings() { setMenuBarInitialVisibility(KonsoleSettings::showMenuBarByDefault()); setRemoveWindowTitleBarAndFrame(KonsoleSettings::removeWindowTitleBarAndFrame()); if (KonsoleSettings::allowMenuAccelerators()) { restoreMenuAccelerators(); } else { removeMenuAccelerators(); } _viewManager->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) { // This is used to check if the window is in "opening" state // And avoid the visibility change when we change the window flag bool oldVisibility = isVisible(); if (frameless) { setWindowFlags(Qt::FramelessWindowHint); } else { setWindowFlags(Qt::Widget); } if (oldVisibility && !isVisible()) { setVisible(true); } } 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 (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/MainWindow.h b/src/MainWindow.h index 6b30b240..6551173b 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -1,207 +1,207 @@ /* 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. */ #ifndef MAINWINDOW_H #define MAINWINDOW_H // Qt #include #include // KDE #include // Konsole #include "Profile.h" class QAction; class KActionMenu; class KToggleAction; namespace Konsole { class IncrementalSearchBar; class ViewManager; class ViewProperties; class Session; class SessionController; class ProfileList; class BookmarkHandler; class TabbedViewContainer; /** * The main window. This contains the menus and an area which contains the terminal displays. * * The main window does not create the views or the container widgets which hold the views. * This is done by the ViewManager class. When a MainWindow is instantiated, it creates * a new ViewManager. The ViewManager can then be used to create new terminal displays * inside the window. * * Do not construct new main windows directly, use Application's newMainWindow() method. */ class MainWindow : public KXmlGuiWindow { Q_OBJECT public: /** * Constructs a new main window. Do not create new main windows directly, use Application's * newMainWindow() method instead. */ MainWindow(); /** * Returns the view manager associated with this window. The view manager can be used to * create new views on particular session objects inside this window. */ ViewManager *viewManager() const; /** * Create a new session. * * @param tabWidget where the new widget should be added. * @param profile The profile to use to create the new session. * @param directory Initial working directory for the new session or empty * if the default working directory associated with the profile should be used. */ Session *createSession(TabbedViewContainer *tabWidget, Profile::Ptr profile, const QString &directory); /** * create a new SSH session. * * @param tabWidget where the new widget should be added. * @param profile The profile to use to create the new session. * @param url the URL representing the new SSH connection */ Session *createSSHSession(TabbedViewContainer *tabWidget, Profile::Ptr profile, const QUrl &url); /** * Helper method to make this window get input focus */ void setFocus(); /** * Set the initial visibility of the menubar. */ void setMenuBarInitialVisibility(bool visible); /** * @brief Set the frameless state * * @param frameless */ void setRemoveWindowTitleBarAndFrame(bool frameless); Q_SIGNALS: /** * Emitted by the main window to request the creation of a * new session in a new window. * * @param profile The profile to use to create the * first session in the new window. * @param directory Initial working directory for the new window or empty * if the default working directory associated with the profile should * be used. */ - void newWindowRequest(Profile::Ptr profile, const QString &directory); + void newWindowRequest(const Profile::Ptr &profile, const QString &directory); /** * Emitted when a view for one session is detached from this window */ void viewDetached(Session *session); protected: // Reimplemented for internal reasons. void showEvent(QShowEvent *event) Q_DECL_OVERRIDE; bool eventFilter(QObject *obj, QEvent *event) Q_DECL_OVERRIDE; // reimplemented from KMainWindow bool queryClose() Q_DECL_OVERRIDE; void saveProperties(KConfigGroup &group) Q_DECL_OVERRIDE; void readProperties(const KConfigGroup &group) Q_DECL_OVERRIDE; void saveGlobalProperties(KConfig *config) Q_DECL_OVERRIDE; void readGlobalProperties(KConfig *config) Q_DECL_OVERRIDE; // reimplemented from QWidget bool focusNextPrevChild(bool next) Q_DECL_OVERRIDE; private Q_SLOTS: void newTab(TabbedViewContainer *tabWidget); void cloneTab(); void newWindow(); void showManageProfilesDialog(); void activateMenuBar(); void showSettingsDialog(const bool showProfilePage = false); void showShortcutsDialog(); - void newFromProfile(TabbedViewContainer *tabWidget, Profile::Ptr profile); + void newFromProfile(TabbedViewContainer *tabWidget, const Profile::Ptr &profile); void activeViewChanged(SessionController *controller); void disconnectController(SessionController *controller); void activeViewTitleChanged(ViewProperties *); void profileListChanged(const QList &sessionActions); void configureNotifications(); void setBlur(bool blur); void updateWindowIcon(); void updateWindowCaption(); void openUrls(const QList &urls); // Sets the list of profiles to be displayed under the "New Tab" action void setProfileList(ProfileList *list); void applyKonsoleSettings(); void updateUseTransparency(); public Q_SLOTS: void viewFullScreen(bool fullScreen); private: void correctStandardShortcuts(); void rememberMenuAccelerators(); void removeMenuAccelerators(); void restoreMenuAccelerators(); void setupActions(); QString activeSessionDir() const; void triggerAction(const QString &name) const; /** * Returns the bookmark handler associated with this window. */ BookmarkHandler *bookmarkHandler() const; // sets the active shortcuts of actions in 'dest' to the shortcuts of actions // with the same name in 'source' (see QAction::ActiveShortcut) static void syncActiveShortcuts(KActionCollection *dest, const KActionCollection *source); private: ViewManager *_viewManager; BookmarkHandler *_bookmarkHandler; KToggleAction *_toggleMenuBarAction; KActionMenu *_newTabMenuAction; QPointer _pluggedController; bool _menuBarInitialVisibility; bool _menuBarInitialVisibilityApplied; }; } #endif // MAINWINDOW_H diff --git a/src/Profile.cpp b/src/Profile.cpp index 3fc853c3..8bfef31f 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(Profile::Ptr parent) +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(Profile::Ptr parent) +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) { profile->setProperty(p, value); } } diff --git a/src/Profile.h b/src/Profile.h index 622b560a..45ad2609 100644 --- a/src/Profile.h +++ b/src/Profile.h @@ -1,806 +1,806 @@ /* This source file is part of Konsole, a terminal emulator. Copyright 2007-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef PROFILE_H #define PROFILE_H // Qt #include #include #include #include #include // Konsole #include "konsoleprivate_export.h" namespace Konsole { class ProfileGroup; /** * Represents a terminal set-up which can be used to * set the initial state of new terminal sessions or applied * to existing sessions. Profiles consist of a number of named * properties, which can be retrieved using property() and * set using setProperty(). isPropertySet() can be used to check * whether a particular property has been set in a profile. * * Profiles support a simple form of inheritance. When a new Profile * is constructed, a pointer to a parent profile can be passed to * the constructor. When querying a particular property of a profile * using property(), the profile will return its own value for that * property if one has been set or otherwise it will return the * parent's value for that property. * * Profiles can be loaded from disk using ProfileReader instances * and saved to disk using ProfileWriter instances. */ class KONSOLEPRIVATE_EXPORT Profile : public QSharedData { friend class ProfileReader; friend class ProfileWriter; friend class ProfileGroup; public: typedef QExplicitlySharedDataPointer Ptr; typedef QExplicitlySharedDataPointer GroupPtr; /** * This enum describes the available properties * which a Profile may consist of. * * Properties can be set using setProperty() and read * using property() */ enum Property { /** (QString) Path to the profile's configuration file on-disk. */ Path, /** (QString) The descriptive name of this profile. */ Name, /** (QString) The untranslated name of this profile. * Warning: this is an internal property. Do not touch it. */ UntranslatedName, /** (QString) The name of the icon associated with this profile. * This is used in menus and tabs to represent the profile. */ Icon, /** (QString) The command to execute ( excluding arguments ) when * creating a new terminal session using this profile. */ Command, /** (QStringList) The arguments which are passed to the program * specified by the Command property when creating a new terminal * session using this profile. */ Arguments, /** (QStringList) Additional environment variables (in the form of * NAME=VALUE pairs) which are passed to the program specified by * the Command property when creating a new terminal session using * this profile. */ Environment, /** (QString) The initial working directory for sessions created * using this profile. */ Directory, /** (QString) The format used for tab titles when running normal * commands. */ LocalTabTitleFormat, /** (QString) The format used for tab titles when the session is * running a remote command (eg. SSH) */ RemoteTabTitleFormat, /** (bool) Specifies whether show hint for terminal size after * resizing the application window. */ ShowTerminalSizeHint, /** (bool) If the background color should change to indicate if the window is active */ DimWhenInactive, /** (QFont) The font to use in terminal displays using this profile. */ Font, /** (QString) The name of the color scheme to use in terminal * displays using this profile. * Color schemes are managed by the ColorSchemeManager class. */ ColorScheme, /** (QString) The name of the key bindings. * Key bindings are managed by the KeyboardTranslatorManager class. */ KeyBindings, /** (HistoryModeEnum) Specifies the storage type used for keeping * the output produced by terminal sessions using this profile. * * See Enum::HistoryModeEnum */ HistoryMode, /** (int) Specifies the number of lines of output to remember in * terminal sessions using this profile. Once the limit is reached, * the oldest lines are lost if the HistoryMode property is * FixedSizeHistory */ HistorySize, /** (ScrollBarPositionEnum) Specifies the position of the scroll bar * in terminal displays using this profile. * * See Enum::ScrollBarPositionEnum */ ScrollBarPosition, /** (bool) Specifies whether the PageUp/Down will scroll the full * height or half height. */ ScrollFullPage, /** (bool) Specifies whether the terminal will enable Bidirectional * text display */ BidiRenderingEnabled, /** (bool) Specifies whether text in terminal displays is allowed * to blink. */ BlinkingTextEnabled, /** (bool) Specifies whether the flow control keys (typically Ctrl+S, * Ctrl+Q) have any effect. Also known as Xon/Xoff */ FlowControlEnabled, /** (int) Specifies the pixels between the terminal lines. */ LineSpacing, /** (bool) Specifies whether the cursor blinks ( in a manner similar * to text editing applications ) */ BlinkingCursorEnabled, /** (bool) If true, terminal displays use a fixed color to draw the * cursor, specified by the CustomCursorColor property. Otherwise * the cursor changes color to match the character underneath it. */ UseCustomCursorColor, /** (CursorShapeEnum) The shape used by terminal displays to * represent the cursor. * * See Enum::CursorShapeEnum */ CursorShape, /** (QColor) The color used by terminal displays to draw the cursor. * Only applicable if the UseCustomCursorColor property is true. */ CustomCursorColor, /** (QString) A string consisting of the characters used to delimit * words when selecting text in the terminal display. */ WordCharacters, /** (TripleClickModeEnum) Specifies which part of current line should * be selected with triple click action. * * See Enum::TripleClickModeEnum */ TripleClickMode, /** (bool) If true, text that matches a link or an email address is * underlined when hovered by the mouse pointer. */ UnderlineLinksEnabled, /** (bool) If true, text that matches a file is * underlined when hovered by the mouse pointer. */ UnderlineFilesEnabled, /** (bool) If true, links can be opened by direct mouse click.*/ OpenLinksByDirectClickEnabled, /** (bool) If true, control key must be pressed to click and drag selected text. */ CtrlRequiredForDrag, /** (bool) If true, automatically copy selected text into the clipboard */ AutoCopySelectedText, /** (bool) The QMimeData object used when copying text always * has the plain/text MIME set and if this is @c true then the * text/html MIME is set too in that object i.e. the copied * text will include formatting, font faces, colors... etc; users * can paste the text as HTML (default) or as plain/text by using * e.g. the "Paste Special" functionality in LibreOffice. */ CopyTextAsHTML, /** (bool) If true, leading spaces are trimmed in selected text */ TrimLeadingSpacesInSelectedText, /** (bool) If true, trailing spaces are trimmed in selected text */ TrimTrailingSpacesInSelectedText, /** (bool) If true, then dropped URLs will be pasted as text without asking */ DropUrlsAsText, /** (bool) If true, middle mouse button pastes from X Selection */ PasteFromSelectionEnabled, /** (bool) If true, middle mouse button pastes from Clipboard */ PasteFromClipboardEnabled, /** (MiddleClickPasteModeEnum) Specifies the source from which mouse * middle click pastes data. * * See Enum::MiddleClickPasteModeEnum */ MiddleClickPasteMode, /** (String) Default text codec */ DefaultEncoding, /** (bool) Whether fonts should be aliased or not */ AntiAliasFonts, /** (bool) Whether character with intense colors should be rendered * in bold font or just in bright color. */ BoldIntense, /** (bool) Whether to use font's line characters instead of the * builtin code. */ UseFontLineCharacters, /** (bool) Whether new sessions should be started in the same * directory as the currently active session. */ StartInCurrentSessionDir, /** (int) Specifies the threshold of detected silence in seconds. */ SilenceSeconds, /** (BellModeEnum) Specifies the behavior of bell. * * See Enum::BellModeEnum */ BellMode, /** (int) Specifies the preferred columns. */ TerminalColumns, /** (int) Specifies the preferred rows. */ TerminalRows, /** Index of profile in the File Menu * WARNING: this is currently an internal field, which is * expected to be zero on disk. Do not modify it manually. * * In future, the format might be #.#.# to account for levels */ MenuIndex, /** (int) Margin width in pixels */ TerminalMargin, /** (bool) Center terminal when there is a margin */ TerminalCenter, /** (bool) If true, mouse wheel scroll with Ctrl key pressed * increases/decreases the terminal font size. */ MouseWheelZoomEnabled, /** (bool) Specifies whether emulated up/down key press events are * sent, for mouse scroll wheel events, to programs using the * Alternate Screen buffer; this is mainly for the benefit of * programs that don't natively support mouse scroll events, e.g. * less. * * This also works for scrolling in applications that support Mouse * Tracking events but don't indicate they're interested in those * events; for example, when vim doesn't indicate it's interested * in Mouse Tracking events (i.e. when the mouse is in Normal * (not Visual) mode): http://vimdoc.sourceforge.net/htmldoc/intro.html#Normal * mouse wheel scroll events will send up/down key press events. * * Default value is true. * See also, MODE_Mouse1007 in the Emulation header, which toggles * Alternate Scrolling with escape sequences. */ AlternateScrolling, /** (int) Keyboard modifiers to show URL hints */ UrlHintsModifiers, /** (bool) Reverse the order of URL hints */ ReverseUrlHints }; /** * Constructs a new profile * * @param parent The parent profile. When querying the value of a * property using property(), if the property has not been set in this * profile then the parent's value for the property will be returned. */ - explicit Profile(Ptr parent = Ptr()); + explicit Profile(const Ptr &parent = Ptr()); virtual ~Profile(); /** * Copies all properties except Name and Path from the specified @p * profile into this profile * * @param profile The profile to copy properties from * @param differentOnly If true, only properties in @p profile which have * a different value from this profile's current value (either set via * setProperty() or inherited from the parent profile) will be set. */ void clone(Ptr profile, bool differentOnly = true); /** * A profile which contains a number of default settings for various * properties. This can be used as a parent for other profiles or a * fallback in case a profile cannot be loaded from disk. */ void useFallback(); /** * Changes the parent profile. When calling the property() method, * if the specified property has not been set for this profile, * the parent's value for the property will be returned instead. */ - void setParent(Ptr parent); + void setParent(const Ptr &parent); /** Returns the parent profile. */ const Ptr parent() const; /** Returns this profile as a group or null if this profile is not a * group. */ const GroupPtr asGroup() const; GroupPtr asGroup(); /** * Returns the current value of the specified @p property, cast to type T. * Internally properties are stored using the QVariant type and cast to T * using QVariant::value(); * * If the specified @p property has not been set in this profile, * and a non-null parent was specified in the Profile's constructor, * the parent's value for @p property will be returned. */ template T property(Property p) const; /** Sets the value of the specified @p property to @p value. */ virtual void setProperty(Property p, const QVariant &value); /** Returns true if the specified property has been set in this Profile * instance. */ virtual bool isPropertySet(Property p) const; /** Returns a map of the properties set in this Profile instance. */ virtual QHash setProperties() const; /** Returns true if no properties have been set in this Profile instance. */ bool isEmpty() const; /** * Returns true if this is a 'hidden' profile which should not be * displayed in menus or saved to disk. * * This is used for the fallback profile, in case there are no profiles on * disk which can be loaded, or for overlay profiles created to handle * command-line arguments which change profile properties. */ bool isHidden() const; /** Specifies whether this is a hidden profile. See isHidden() */ void setHidden(bool hidden); // // Convenience methods for property() and setProperty() go here // /** Convenience method for property(Profile::Path) */ QString path() const { return property(Profile::Path); } /** Convenience method for property(Profile::Name) */ QString name() const { return property(Profile::Name); } /** Convenience method for property(Profile::UntranslatedName) */ QString untranslatedName() const { return property(Profile::UntranslatedName); } /** Convenience method for property(Profile::Directory) */ QString defaultWorkingDirectory() const { return property(Profile::Directory); } /** Convenience method for property(Profile::Icon) */ QString icon() const { return property(Profile::Icon); } /** Convenience method for property(Profile::Command) */ QString command() const { return property(Profile::Command); } /** Convenience method for property(Profile::Arguments) */ QStringList arguments() const { return property(Profile::Arguments); } /** Convenience method for property(Profile::LocalTabTitleFormat) */ QString localTabTitleFormat() const { return property(Profile::LocalTabTitleFormat); } /** Convenience method for property(Profile::RemoteTabTitleFormat) */ QString remoteTabTitleFormat() const { return property(Profile::RemoteTabTitleFormat); } /** Convenience method for property(Profile::ShowTerminalSizeHint) */ bool showTerminalSizeHint() const { return property(Profile::ShowTerminalSizeHint); } /** Convenience method for property(Profile::DimWhenInactive) */ bool dimWhenInactive() const { return property(Profile::DimWhenInactive); } /** Convenience method for property(Profile::Font) */ QFont font() const { return property(Profile::Font); } /** Convenience method for property(Profile::ColorScheme) */ QString colorScheme() const { return property(Profile::ColorScheme); } /** Convenience method for property(Profile::Environment) */ QStringList environment() const { return property(Profile::Environment); } /** Convenience method for property(Profile::KeyBindings) */ QString keyBindings() const { return property(Profile::KeyBindings); } /** Convenience method for property(Profile::HistorySize) */ int historySize() const { return property(Profile::HistorySize); } /** Convenience method for property(Profile::BidiRenderingEnabled) */ bool bidiRenderingEnabled() const { return property(Profile::BidiRenderingEnabled); } /** Convenience method for property(Profile::LineSpacing) */ int lineSpacing() const { return property(Profile::LineSpacing); } /** Convenience method for property(Profile::BlinkingTextEnabled) */ bool blinkingTextEnabled() const { return property(Profile::BlinkingTextEnabled); } /** Convenience method for property(Profile::MouseWheelZoomEnabled) */ bool mouseWheelZoomEnabled() const { return property(Profile::MouseWheelZoomEnabled); } /** Convenience method for property(Profile::BlinkingCursorEnabled) */ bool blinkingCursorEnabled() const { return property(Profile::BlinkingCursorEnabled); } /** Convenience method for property(Profile::FlowControlEnabled) */ bool flowControlEnabled() const { return property(Profile::FlowControlEnabled); } /** Convenience method for property(Profile::UseCustomCursorColor) */ bool useCustomCursorColor() const { return property(Profile::UseCustomCursorColor); } /** Convenience method for property(Profile::CustomCursorColor) */ QColor customCursorColor() const { return property(Profile::CustomCursorColor); } /** Convenience method for property(Profile::WordCharacters) */ QString wordCharacters() const { return property(Profile::WordCharacters); } /** Convenience method for property(Profile::UnderlineLinksEnabled) */ bool underlineLinksEnabled() const { return property(Profile::UnderlineLinksEnabled); } /** Convenience method for property(Profile::UnderlineFilesEnabled) */ bool underlineFilesEnabled() const { return property(Profile::UnderlineFilesEnabled); } bool autoCopySelectedText() const { return property(Profile::AutoCopySelectedText); } /** Convenience method for property(Profile::DefaultEncoding) */ QString defaultEncoding() const { return property(Profile::DefaultEncoding); } /** Convenience method for property(Profile::AntiAliasFonts) */ bool antiAliasFonts() const { return property(Profile::AntiAliasFonts); } /** Convenience method for property(Profile::BoldIntense) */ bool boldIntense() const { return property(Profile::BoldIntense); } /** Convenience method for property(Profile::UseFontLineCharacters)*/ bool useFontLineCharacters() const { return property(Profile::UseFontLineCharacters); } /** Convenience method for property(Profile::StartInCurrentSessionDir) */ bool startInCurrentSessionDir() const { return property(Profile::StartInCurrentSessionDir); } /** Convenience method for property(Profile::SilenceSeconds) */ int silenceSeconds() const { return property(Profile::SilenceSeconds); } /** Convenience method for property(Profile::TerminalColumns) */ int terminalColumns() const { return property(Profile::TerminalColumns); } /** Convenience method for property(Profile::TerminalRows) */ int terminalRows() const { return property(Profile::TerminalRows); } /** Convenience method for property(Profile::TerminalMargin) */ int terminalMargin() const { return property(Profile::TerminalMargin); } /** Convenience method for property(Profile::TerminalCenter) */ bool terminalCenter() const { return property(Profile::TerminalCenter); } /** Convenience method for property(Profile::MenuIndex) */ QString menuIndex() const { return property(Profile::MenuIndex); } int menuIndexAsInt() const; /** Return a list of all properties names and their type * (for use with -p option). */ const QStringList propertiesInfoList() const; /** * Returns the element from the Property enum associated with the * specified @p name. * * @param name The name of the property to look for, this is case * insensitive. */ static Property lookupByName(const QString &name); private: struct PropertyInfo; // Defines a new property, this property is then available // to all Profile instances. static void registerProperty(const PropertyInfo &info); // fills the table with default names for profile properties // the first time it is called. // subsequent calls return immediately static void fillTableWithDefaultNames(); // returns true if the property can be inherited static bool canInheritProperty(Property p); QHash _propertyValues; Ptr _parent; bool _hidden; static QHash PropertyInfoByName; static QHash PropertyInfoByProperty; // Describes a property. Each property has a name and group // which is used when saving/loading the profile. struct PropertyInfo { Property property; const char *name; const char *group; QVariant::Type type; }; static const PropertyInfo DefaultPropertyNames[]; }; inline bool Profile::canInheritProperty(Property p) { return p != Name && p != Path; } template inline T Profile::property(Property p) const { return property(p).value(); } template<> inline QVariant Profile::property(Property p) const { if (_propertyValues.contains(p)) { return _propertyValues[p]; } else if (_parent && canInheritProperty(p)) { return _parent->property(p); } else { return QVariant(); } } /** * A composite profile which allows a group of profiles to be treated as one. * When setting a property, the new value is applied to all profiles in the * group. When reading a property, if all profiles in the group have the same * value then that value is returned, otherwise the result is null. * * Profiles can be added to the group using addProfile(). When all profiles * have been added updateValues() must be called * to sync the group's property values with those of the group's profiles. * * The Profile::Name and Profile::Path properties are unique to individual * profiles, setting these properties on a ProfileGroup has no effect. */ class KONSOLEPRIVATE_EXPORT ProfileGroup : public Profile { public: typedef QExplicitlySharedDataPointer Ptr; /** Construct a new profile group, which is hidden by default. */ - explicit ProfileGroup(Profile::Ptr profileParent = Profile::Ptr()); + explicit ProfileGroup(const Profile::Ptr &profileParent = Profile::Ptr()); /** Add a profile to the group. Calling setProperty() will update this * profile. When creating a group, add the profiles to the group then * call updateValues() to make the group's property values reflect the * profiles currently in the group. */ - void addProfile(Profile::Ptr profile) + void addProfile(const Profile::Ptr &profile) { _profiles.append(profile); } /** Remove a profile from the group. Calling setProperty() will no longer * affect this profile. */ - void removeProfile(Profile::Ptr profile) + void removeProfile(const Profile::Ptr &profile) { _profiles.removeAll(profile); } /** Returns the profiles in this group .*/ QList profiles() const { return _profiles; } /** * Updates the property values in this ProfileGroup to match those from * the group's profiles() * * For each available property, if each profile in the group has the same * value then the ProfileGroup will use that value for the property. * Otherwise the value for the property will be set to a null QVariant * * Some properties such as the name and the path of the profile * will always be set to null if the group has more than one profile. */ void updateValues(); /** Sets the value of @p property in each of the group's profiles to * @p value. */ void setProperty(Property p, const QVariant &value) Q_DECL_OVERRIDE; private: Q_DISABLE_COPY(ProfileGroup) QList _profiles; }; -inline ProfileGroup::ProfileGroup(Profile::Ptr profileParent) : +inline ProfileGroup::ProfileGroup(const Profile::Ptr &profileParent) : Profile(profileParent), _profiles(QList()) { setHidden(true); } inline const Profile::GroupPtr Profile::asGroup() const { const Profile::GroupPtr ptr(dynamic_cast( const_cast(this))); return ptr; } inline Profile::GroupPtr Profile::asGroup() { return Profile::GroupPtr(dynamic_cast(this)); } /** * Parses an input string consisting of property names * and assigned values and returns a table of properties * and values. * * The input string will typically look like this: * * @code * PropertyName=Value;PropertyName=Value ... * @endcode * * For example: * * @code * Icon=konsole;Directory=/home/bob * @endcode */ class KONSOLEPRIVATE_EXPORT ProfileCommandParser { public: /** * Parses an input string consisting of property names * and assigned values and returns a table of * properties and values. */ QHash parse(const QString &input); }; } Q_DECLARE_METATYPE(Konsole::Profile::Ptr) #endif // PROFILE_H diff --git a/src/ProfileList.cpp b/src/ProfileList.cpp index 7949ed0f..c3c36563 100644 --- a/src/ProfileList.cpp +++ b/src/ProfileList.cpp @@ -1,185 +1,185 @@ /* 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(); foreach(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(Profile::Ptr profile) const +QAction* ProfileList::actionForProfile(const Profile::Ptr &profile) const { foreach(QAction* action, _group->actions()) { if (action->data().value() == profile) { return action; } } return nullptr; // not found } -void ProfileList::profileChanged(Profile::Ptr profile) +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(Profile::Ptr profile, const QKeySequence& sequence) +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) { widget->removeAction(currentAction); } widget->addActions(_group->actions()); } -void ProfileList::addShortcutAction(Profile::Ptr profile) +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) { widget->addAction(action); } emit actionsChanged(_group->actions()); updateEmptyAction(); } -void ProfileList::removeShortcutAction(Profile::Ptr profile) +void ProfileList::removeShortcutAction(const Profile::Ptr &profile) { QAction* action = actionForProfile(profile); if (action != nullptr) { _group->removeAction(action); foreach(QWidget * widget, _registeredWidgets) { widget->removeAction(action); } emit actionsChanged(_group->actions()); } updateEmptyAction(); } -void ProfileList::favoriteChanged(Profile::Ptr profile, bool isFavorite) +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/ProfileList.h b/src/ProfileList.h index 5bb4072b..d301ba18 100644 --- a/src/ProfileList.h +++ b/src/ProfileList.h @@ -1,108 +1,108 @@ /* 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. */ #ifndef PROFILELIST_H #define PROFILELIST_H // Qt #include #include #include // Konsole #include "Profile.h" #include "konsoleprivate_export.h" class QAction; class QActionGroup; class QKeySequence; namespace Konsole { /** * ProfileList provides a list of actions which represent session profiles * that a SessionManager can create a session from. * * These actions can be plugged into a GUI. * * Currently only profiles marked as favorites in the SessionManager are included. * * The user-data associated with each session can be passed to the createProfile() method of the * SessionManager to create a new terminal session. */ class KONSOLEPRIVATE_EXPORT ProfileList : public QObject { Q_OBJECT public: /** * Constructs a new profile list which displays profiles * that can be used to create session * * @param addShortcuts True if the shortcuts associated with profiles * in the session manager should be added to the actions * @param parent The parent GUI object */ ProfileList(bool addShortcuts, QObject *parent); /** * Returns a list of actions representing profiles * * The user-data associated with each action is the corresponding profile */ QList actions(); /** TODO: Document me */ void syncWidgetActions(QWidget *widget, bool sync); Q_SIGNALS: /** * Emitted when the user selects an action from the list. * * @param profile The profile to select */ - void profileSelected(Profile::Ptr profile); + void profileSelected(const Profile::Ptr &profile); /** * Emitted when the list of actions changes. */ void actionsChanged(const QList &actions); private Q_SLOTS: void triggered(QAction *action); - void favoriteChanged(Profile::Ptr profile, bool isFavorite); - void profileChanged(Profile::Ptr profile); - void shortcutChanged(Profile::Ptr profile, const QKeySequence &sequence); - void addShortcutAction(Profile::Ptr profile); - void removeShortcutAction(Profile::Ptr profile); + void favoriteChanged(const Profile::Ptr &profile, bool isFavorite); + void profileChanged(const Profile::Ptr &profile); + void shortcutChanged(const Profile::Ptr &profile, const QKeySequence &sequence); + void addShortcutAction(const Profile::Ptr &profile); + void removeShortcutAction(const Profile::Ptr &profile); private: Q_DISABLE_COPY(ProfileList) - QAction *actionForProfile(Profile::Ptr profile) const; + QAction *actionForProfile(const Profile::Ptr &profile) const; void updateAction(QAction *action, Profile::Ptr profile); void updateEmptyAction(); QActionGroup *_group; bool _addShortcuts; // action to show when the list is empty QAction *_emptyListAction; QSet _registeredWidgets; }; } #endif // PROFILELIST_H diff --git a/src/ProfileManager.cpp b/src/ProfileManager.cpp index c94b779e..c22586c3 100644 --- a/src/ProfileManager.cpp +++ b/src/ProfileManager.cpp @@ -1,707 +1,707 @@ /* 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) { qStableSort(list.begin(), list.end(), profileIndexLessThan); } static void sortByNameProfileList(QList& list) { qStableSort(list.begin(), list.end(), profileNameLessThan); } ProfileManager::ProfileManager() : _profiles(QSet()) , _favorites(QSet()) , _defaultProfile(nullptr) , _fallbackProfile(nullptr) , _loadedAllProfiles(false) , _loadedFavorites(false) , _shortcuts(QMap()) , _profileList(nullptr) { //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, QStringLiteral("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() == QLatin1String(".")) { 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) { 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(); qStableSort(paths.begin(), paths.end(), stringLessThan); return paths; } QStringList ProfileManager::availableProfileNames() const { QStringList names; foreach(Profile::Ptr profile, ProfileManager::instance()->allProfiles()) { if (!profile->isHidden()) { names.push_back(profile->name()); } } qStableSort(names.begin(), names.end(), stringLessThan); return names; } void ProfileManager::loadAllProfiles() { if (_loadedAllProfiles) { return; } const QStringList& paths = availableProfilePaths(); foreach(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(Profile::Ptr profile) +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) { existingProfileNames.append(existingProfile->name()); } int nameSuffix = 1; QString newName; QString newTranslatedName; do { newName = QStringLiteral("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()) { 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) { 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(Profile::Ptr profile) +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(Profile::Ptr profile) +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(Profile::Ptr profile , bool favorite) +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, QStringLiteral("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) { 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) { const QString& path = profile->path(); if (favoriteSet.contains(path)) { _favorites.insert(profile); favoriteSet.remove(path); } } // load any remaining favorites foreach(const QString& favorite, 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(); } ProfileList *ProfileManager::getProfileList() { if (_profileList == nullptr) { _profileList = new ProfileList(true, this); } return _profileList; } diff --git a/src/ProfileManager.h b/src/ProfileManager.h index e85bc7d6..ca8b7fcf 100644 --- a/src/ProfileManager.h +++ b/src/ProfileManager.h @@ -1,327 +1,327 @@ /* 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. */ #ifndef PROFILEMANAGER_H #define PROFILEMANAGER_H // Qt #include #include #include #include #include #include #include // Konsole #include "Profile.h" #include "ProfileList.h" namespace Konsole { /** * Manages profiles which specify various settings for terminal sessions * and their displays. * * Profiles in the manager have a concept of favorite status, which can be used * by widgets and dialogs in the application decide which profiles to list and * how to display them. The favorite status of a profile can be altered using * setFavorite() and retrieved using isFavorite() */ class KONSOLEPRIVATE_EXPORT ProfileManager : public QObject { Q_OBJECT public: /** * Constructs a new profile manager and loads information about the available * profiles. */ ProfileManager(); /** * Destroys the ProfileManager. */ ~ProfileManager() Q_DECL_OVERRIDE; /** * Returns the profile manager instance. */ static ProfileManager *instance(); /** * Returns a list of all available profiles * * Initially only the profile currently set as the default is loaded. * * Favorite profiles are loaded automatically when findFavorites() is called. * * When this method is called, it calls loadAllProfiles() internally to * ensure all available profiles are loaded and usable. */ QList allProfiles(); /** * Returns a list of already loaded profiles */ QList loadedProfiles() const; /** * Loads all available profiles. This involves reading each * profile configuration file from disk and parsing it. * Therefore it should only be done when necessary. */ void loadAllProfiles(); /** * Loads a profile from the specified path and registers * it with the ProfileManager. * * @p path may be relative or absolute. The path may just be the * base name of the profile to load (eg. if the profile's full path * is "/My Profile.profile" then both * "konsole/My Profile.profile" , "My Profile.profile" and * "My Profile" will be accepted) * * @return Pointer to a profile which can be passed to * SessionManager::createSession() to create a new session using * this profile. */ Profile::Ptr loadProfile(const QString &shortPath); /** * Searches for available profiles on-disk and returns a list * of paths of profiles which can be loaded. */ QStringList availableProfilePaths() const; /** * Returns a list of names of all available profiles */ QStringList availableProfileNames() const; /** * Registers a new type of session. * The favorite status of the session ( as returned by isFavorite() ) is set to false by default. */ - void addProfile(Profile::Ptr type); + void addProfile(const Profile::Ptr &type); /** * Deletes the configuration file used to store a profile. * The profile will continue to exist while sessions are still using it. The profile * will be marked as hidden (see Profile::setHidden() ) so that it does not show * up in profile lists and future changes to the profile are not stored to disk. * * Returns true if the profile was successfully deleted or false otherwise. */ bool deleteProfile(Profile::Ptr profile); /** * Updates a @p profile with the changes specified in @p propertyMap. * * All sessions currently using the profile will be updated to reflect the new settings. * * After the profile is updated, the profileChanged() signal will be emitted. * * @param profile The profile to change * @param propertyMap A map between profile properties and values describing the changes * @param persistent If true, the changes are saved to the profile's configuration file, * set this to false if you want to preview possible changes to a profile but do not * wish to make them permanent. */ void changeProfile(Profile::Ptr profile, QHash propertyMap, bool persistent = true); /** * Sets the @p profile as the default profile for creating new sessions */ - void setDefaultProfile(Profile::Ptr profile); + void setDefaultProfile(const Profile::Ptr &profile); /** * Returns a Profile object describing the default profile */ Profile::Ptr defaultProfile() const; /** * Returns a Profile object with hard-coded settings which is always available. * This can be used as a parent for new profiles which provides suitable default settings * for all properties. */ Profile::Ptr fallbackProfile() const; /** * Specifies whether a profile should be included in the user's * list of favorite profiles. */ - void setFavorite(Profile::Ptr profile, bool favorite); + void setFavorite(const Profile::Ptr &profile, bool favorite); /** * Returns the set of the user's favorite profiles. */ QSet findFavorites(); QList sortedFavorites(); /** * Sorts the profile list by menuindex; those without an menuindex, sort by name. * The menuindex list is first and then the non-menuindex list. * * @param list The profile list to sort */ void sortProfiles(QList &list); /** * Associates a shortcut with a particular profile. */ void setShortcut(Profile::Ptr profile, const QKeySequence &shortcut); /** Returns the shortcut associated with a particular profile. */ QKeySequence shortcut(Profile::Ptr profile) const; /** * Returns the list of shortcut key sequences which * can be used to create new sessions based on * existing profiles * * When one of the shortcuts is activated, * use findByShortcut() to load the profile associated * with the shortcut. */ QList shortcuts(); /** * Finds and loads the profile associated with * the specified @p shortcut key sequence and returns a pointer to it. */ Profile::Ptr findByShortcut(const QKeySequence &shortcut); ProfileList *getProfileList(); Q_SIGNALS: /** Emitted when a profile is added to the manager. */ - void profileAdded(Profile::Ptr ptr); + void profileAdded(const Profile::Ptr &ptr); /** Emitted when a profile is removed from the manager. */ - void profileRemoved(Profile::Ptr ptr); + void profileRemoved(const Profile::Ptr &ptr); /** Emitted when a profile's properties are modified. */ - void profileChanged(Profile::Ptr ptr); + void profileChanged(const Profile::Ptr &ptr); /** * Emitted when the favorite status of a profile changes. * * @param profile The profile to change * @param favorite Specifies whether the profile is a favorite or not */ - void favoriteStatusChanged(Profile::Ptr profile, bool favorite); + void favoriteStatusChanged(const Profile::Ptr &profile, bool favorite); /** * Emitted when the shortcut for a profile is changed. * * @param profile The profile whose status was changed * @param newShortcut The new shortcut key sequence for the profile */ - void shortcutChanged(Profile::Ptr profile, const QKeySequence &newShortcut); + void shortcutChanged(const Profile::Ptr &profile, const QKeySequence &newShortcut); public Q_SLOTS: /** Saves settings (favorites, shortcuts, default profile etc.) to disk. */ void saveSettings(); protected Q_SLOTS: private Q_SLOTS: private: Q_DISABLE_COPY(ProfileManager) // loads the mappings between shortcut key sequences and // profile paths void loadShortcuts(); // saves the mappings between shortcut key sequences and // profile paths void saveShortcuts(); //loads the set of favorite profiles void loadFavorites(); //saves the set of favorite profiles void saveFavorites(); // records which profile is set as the default profile // Note: it does not save the profile itself into disk. That is // what saveProfile() does. void saveDefaultProfile(); // saves a profile to a file // returns the path to which the profile was saved, which will // be the same as the path property of profile if valid or a newly generated path // otherwise - QString saveProfile(Profile::Ptr profile); + QString saveProfile(const Profile::Ptr &profile); QSet _profiles; // list of all loaded profiles QSet _favorites; // list of favorite profiles Profile::Ptr _defaultProfile; Profile::Ptr _fallbackProfile; bool _loadedAllProfiles; // set to true after loadAllProfiles has been called bool _loadedFavorites; // set to true after loadFavorites has been called struct ShortcutData { Profile::Ptr profileKey; QString profilePath; }; QMap _shortcuts; // shortcut keys -> profile path // finds out if it's a internal profile or an external one, // fixing the path to point to the correct location for the profile. QString normalizePath(const QString& path) const; ProfileList *_profileList; }; /** * PopStackOnExit is a utility to remove all values from a QStack which are added during * the lifetime of a PopStackOnExit instance. * * When a PopStackOnExit instance is destroyed, elements are removed from the stack * until the stack count is reduced the value when the PopStackOnExit instance was created. */ template class PopStackOnExit { public: explicit PopStackOnExit(QStack &stack) : _stack(stack), _count(stack.count()) { } ~PopStackOnExit() { while (_stack.count() > _count) { _stack.pop(); } } private: Q_DISABLE_COPY(PopStackOnExit) QStack &_stack; int _count; }; } #endif //PROFILEMANAGER_H diff --git a/src/ProfileWriter.cpp b/src/ProfileWriter.cpp index 6e709108..ed1b97f3 100644 --- a/src/ProfileWriter.cpp +++ b/src/ProfileWriter.cpp @@ -1,101 +1,101 @@ /* 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 "ProfileWriter.h" // Qt #include // KDE #include #include // Konsole #include "ShellCommand.h" using namespace Konsole; // FIXME: A dup line from Profile.cpp - redo these static const char GENERAL_GROUP[] = "General"; ProfileWriter::ProfileWriter() = default; ProfileWriter::~ProfileWriter() = default; // All profiles changes are stored under users' local account -QString ProfileWriter::getPath(const Profile::Ptr profile) +QString ProfileWriter::getPath(const Profile::Ptr &profile) { // If any changes are made to this location, check that programs using // the Konsole part can write/save profiles static const QString localDataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/konsole"); return localDataLocation % QLatin1String("/") % profile->untranslatedName() % QLatin1String(".profile"); } void ProfileWriter::writeProperties(KConfig& config, - const Profile::Ptr profile, + const Profile::Ptr &profile, const Profile::PropertyInfo* properties) { const char* groupName = nullptr; KConfigGroup group; while (properties->name != nullptr) { if (properties->group != nullptr) { if (groupName == nullptr || qstrcmp(groupName, properties->group) != 0) { group = config.group(properties->group); groupName = properties->group; } if (profile->isPropertySet(properties->property)) { group.writeEntry(QLatin1String(properties->name), profile->property(properties->property)); } } properties++; } } -bool ProfileWriter::writeProfile(const QString& path , const Profile::Ptr profile) +bool ProfileWriter::writeProfile(const QString& path , const Profile::Ptr &profile) { KConfig config(path, KConfig::NoGlobals); if (!config.isConfigWritable(false)) { return false; } KConfigGroup general = config.group(GENERAL_GROUP); // Parent profile if set, when loading the profile in future, the parent // must be loaded as well if it exists. if (profile->parent()) { general.writeEntry("Parent", profile->parent()->path()); } if (profile->isPropertySet(Profile::Command) || profile->isPropertySet(Profile::Arguments)) { general.writeEntry("Command", ShellCommand(profile->command(), profile->arguments()).fullCommand()); } // Write remaining properties writeProperties(config, profile, Profile::DefaultPropertyNames); return true; } diff --git a/src/ProfileWriter.h b/src/ProfileWriter.h index 9f8db9fc..9340aeab 100644 --- a/src/ProfileWriter.h +++ b/src/ProfileWriter.h @@ -1,56 +1,56 @@ /* This source file is part of Konsole, a terminal emulator. Copyright 2007-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef PROFILEWRITER_H #define PROFILEWRITER_H #include "Profile.h" class KConfig; namespace Konsole { /** Interface for all classes which can write profile settings to a file. */ class KONSOLEPRIVATE_EXPORT ProfileWriter { public: ProfileWriter(); ~ProfileWriter(); /** * Returns a suitable path-name for writing * @p profile to. The path-name should be accepted by * the corresponding ProfileReader class. */ - QString getPath(const Profile::Ptr profile); + QString getPath(const Profile::Ptr &profile); /** * Writes the properties and values from @p profile to the file specified * by @p path. This profile should be readable by the corresponding * ProfileReader class. */ - bool writeProfile(const QString &path, const Profile::Ptr profile); + bool writeProfile(const QString &path, const Profile::Ptr &profile); private: - void writeProperties(KConfig &config, const Profile::Ptr profile, + void writeProperties(KConfig &config, const Profile::Ptr &profile, const Profile::PropertyInfo *properties); }; } #endif // PROFILEWRITER_H diff --git a/src/SearchHistoryTask.cpp b/src/SearchHistoryTask.cpp index ea1e3e64..b8a3deb1 100644 --- a/src/SearchHistoryTask.cpp +++ b/src/SearchHistoryTask.cpp @@ -1,223 +1,223 @@ /* 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 "SearchHistoryTask.h" #include #include #include "TerminalCharacterDecoder.h" #include "Emulation.h" namespace Konsole { void SearchHistoryTask::addScreenWindow(Session* session , ScreenWindow* searchWindow) { _windows.insert(session, searchWindow); } void SearchHistoryTask::execute() { auto iter = QMapIterator,ScreenWindowPtr>(_windows); while (iter.hasNext()) { iter.next(); executeOnScreenWindow(iter.key() , iter.value()); } } -void SearchHistoryTask::executeOnScreenWindow(QPointer session , ScreenWindowPtr window) +void SearchHistoryTask::executeOnScreenWindow(const QPointer &session , ScreenWindowPtr window) { Q_ASSERT(session); Q_ASSERT(window); Emulation* emulation = session->emulation(); if (!_regExp.pattern().isEmpty()) { int pos = -1; const bool forwards = (_direction == Enum::ForwardsSearch); const int lastLine = window->lineCount() - 1; int startLine; if (forwards && (_startLine == lastLine)) { startLine = 0; } else if (!forwards && (_startLine == 0)) { startLine = lastLine; } else { startLine = _startLine + (forwards ? 1 : -1); } QString string; //text stream to read history into string for pattern or regular expression searching QTextStream searchStream(&string); PlainTextDecoder decoder; decoder.setRecordLinePositions(true); //setup first and last lines depending on search direction int line = startLine; //read through and search history in blocks of 10K lines. //this balances the need to retrieve lots of data from the history each time //(for efficient searching) //without using silly amounts of memory if the history is very large. const int maxDelta = qMin(window->lineCount(), 10000); int delta = forwards ? maxDelta : -maxDelta; int endLine = line; bool hasWrapped = false; // set to true when we reach the top/bottom // of the output and continue from the other // end //loop through history in blocks of lines. do { // ensure that application does not appear to hang // if searching through a lengthy output QApplication::processEvents(); // calculate lines to search in this iteration if (hasWrapped) { if (endLine == lastLine) { line = 0; } else if (endLine == 0) { line = lastLine; } endLine += delta; if (forwards) { endLine = qMin(startLine , endLine); } else { endLine = qMax(startLine , endLine); } } else { endLine += delta; if (endLine > lastLine) { hasWrapped = true; endLine = lastLine; } else if (endLine < 0) { hasWrapped = true; endLine = 0; } } decoder.begin(&searchStream); emulation->writeToStream(&decoder, qMin(endLine, line) , qMax(endLine, line)); decoder.end(); // line number search below assumes that the buffer ends with a new-line string.append(QLatin1Char('\n')); if (forwards) { pos = string.indexOf(_regExp); } else { pos = string.lastIndexOf(_regExp); } //if a match is found, position the cursor on that line and update the screen if (pos != -1) { int newLines = 0; QList linePositions = decoder.linePositions(); while (newLines < linePositions.count() && linePositions[newLines] <= pos) { newLines++; } // ignore the new line at the start of the buffer newLines--; int findPos = qMin(line, endLine) + newLines; highlightResult(window, findPos); emit completed(true); return; } //clear the current block of text and move to the next one string.clear(); line = endLine; } while (startLine != endLine); // if no match was found, clear selection to indicate this window->clearSelection(); window->notifyOutputChanged(); } emit completed(false); } void SearchHistoryTask::highlightResult(ScreenWindowPtr window , int findPos) { //work out how many lines into the current block of text the search result was found //- looks a little painful, but it only has to be done once per search. ////qDebug() << "Found result at line " << findPos; //update display to show area of history containing selection if ((findPos < window->currentLine()) || (findPos >= (window->currentLine() + window->windowLines()))) { int centeredScrollPos = findPos - window->windowLines() / 2; if (centeredScrollPos < 0) { centeredScrollPos = 0; } window->scrollTo(centeredScrollPos); } window->setTrackOutput(false); window->notifyOutputChanged(); window->setCurrentResultLine(findPos); } SearchHistoryTask::SearchHistoryTask(QObject* parent) : SessionTask(parent) , _direction(Enum::BackwardsSearch) , _startLine(0) { } void SearchHistoryTask::setSearchDirection(Enum::SearchDirection direction) { _direction = direction; } void SearchHistoryTask::setStartLine(int line) { _startLine = line; } Enum::SearchDirection SearchHistoryTask::searchDirection() const { return _direction; } void SearchHistoryTask::setRegExp(const QRegularExpression &expression) { _regExp = expression; } QRegularExpression SearchHistoryTask::regExp() const { return _regExp; } } diff --git a/src/SearchHistoryTask.h b/src/SearchHistoryTask.h index a976ba99..b31f6aa7 100644 --- a/src/SearchHistoryTask.h +++ b/src/SearchHistoryTask.h @@ -1,102 +1,102 @@ /* Copyright 2006-2008 by Robert Knight Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef SEARCHHISTORYTASK_H #define SEARCHHISTORYTASK_H #include #include #include #include "SessionTask.h" #include "Enumeration.h" #include "ScreenWindow.h" #include "Session.h" namespace Konsole { //class SearchHistoryThread; /** * A task which searches through the output of sessions for matches for a given regular expression. * SearchHistoryTask operates on ScreenWindow instances rather than sessions added by addSession(). * A screen window can be added to the list to search using addScreenWindow() * * When execute() is called, the search begins in the direction specified by searchDirection(), * starting at the position of the current selection. * * FIXME - This is not a proper implementation of SessionTask, in that it ignores sessions specified * with addSession() * * TODO - Implementation requirements: * May provide progress feedback to the user when searching very large output logs. */ class SearchHistoryTask : public SessionTask { Q_OBJECT public: /** * Constructs a new search task. */ explicit SearchHistoryTask(QObject *parent = nullptr); /** Adds a screen window to the list to search when execute() is called. */ void addScreenWindow(Session *session, ScreenWindow *searchWindow); /** Sets the regular expression which is searched for when execute() is called */ void setRegExp(const QRegularExpression &expression); /** Returns the regular expression which is searched for when execute() is called */ QRegularExpression regExp() const; /** Specifies the direction to search in when execute() is called. */ void setSearchDirection(Enum::SearchDirection direction); /** Returns the current search direction. See setSearchDirection(). */ Enum::SearchDirection searchDirection() const; /** The line from which the search will be done **/ void setStartLine(int line); /** * Performs a search through the session's history, starting at the position * of the current selection, in the direction specified by setSearchDirection(). * * If it finds a match, the ScreenWindow specified in the constructor is * scrolled to the position where the match occurred and the selection * is set to the matching text. execute() then returns immediately. * * To continue the search looking for further matches, call execute() again. */ void execute() Q_DECL_OVERRIDE; private: using ScreenWindowPtr = QPointer; - void executeOnScreenWindow(QPointer session, ScreenWindowPtr window); + void executeOnScreenWindow(const QPointer &session, ScreenWindowPtr window); void highlightResult(ScreenWindowPtr window, int findPos); QMap< QPointer, ScreenWindowPtr > _windows; QRegularExpression _regExp; Enum::SearchDirection _direction; int _startLine; }; } #endif diff --git a/src/SessionController.cpp b/src/SessionController.cpp index 79c37c5f..724a2526 100644 --- a/src/SessionController.cpp +++ b/src/SessionController.cpp @@ -1,1795 +1,1795 @@ /* 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) , _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()) { 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); _profileList = ProfileManager::instance()->getProfileList(); connect(_profileList, &ProfileList::actionsChanged, this, &Konsole::SessionController::prepareSwitchProfileMenu); prepareSwitchProfileMenu(); } SessionController::~SessionController() { if (!_view.isNull()) { _view->setScreenWindow(nullptr); } _allControllers.remove(this); if (!_editProfileDialog.isNull()) { delete _editProfileDialog.data(); } } void SessionController::trackOutput(QKeyEvent* event) { Q_ASSERT(_view->screenWindow()); // Only jump to the bottom if the user actually typed something in, // not if the user e. g. just pressed a modifier. if (event->text().isEmpty() && (event->modifiers() != 0u)) { return; } _view->screenWindow()->setTrackOutput(true); } void SessionController::interactionHandler() { // This flag is used to make sure those special icons indicating interest // events (activity/silence/bell?) remain in the tab until user interaction // happens. Otherwise, those special icons will quickly be replaced by // normal icon when ::snapshot() is triggered _keepIconUntilInteraction = false; _interactionTimer->start(); } void SessionController::snapshot() { Q_ASSERT(!_session.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) { 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().value(); _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); disconnect(_profileList, &Konsole::ProfileList::profileSelected, nullptr, nullptr); connect(_profileList, &Konsole::ProfileList::profileSelected, this, &Konsole::SessionController::switchProfile); 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())); if (isKonsolePart()) { action->setText(i18n("&Close Session")); } else { action->setText(i18n("&Close Tab")); } action->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_W); // Open Browser action = collection->addAction(QStringLiteral("open-browser"), this, SLOT(openBrowser())); action->setText(i18n("Open File Manager")); action->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager"))); // Copy and Paste action = KStandardAction::copy(this, SLOT(copy()), collection); #ifdef Q_OS_MACOS // Don't use the Konsole::ACCEL const here, we really want the Command key (Qt::META) // TODO: check what happens if we leave it to Qt to assign the default? collection->setDefaultShortcut(action, Qt::META + Qt::Key_C); #else collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_C); #endif // disabled at first, since nothing has been selected now action->setEnabled(false); action = KStandardAction::paste(this, SLOT(paste()), collection); QList pasteShortcut; #ifdef Q_OS_MACOS pasteShortcut.append(QKeySequence(Qt::META + Qt::Key_V)); // No Insert key on Mac keyboards #else pasteShortcut.append(QKeySequence(Konsole::ACCEL + Qt::SHIFT + Qt::Key_V)); pasteShortcut.append(QKeySequence(Qt::SHIFT + Qt::Key_Insert)); #endif collection->setDefaultShortcuts(action, pasteShortcut); action = collection->addAction(QStringLiteral("paste-selection"), this, SLOT(pasteFromX11Selection())); action->setText(i18n("Paste Selection")); #ifdef Q_OS_MACOS collection->setDefaultShortcut(action, Qt::META + Qt::SHIFT + Qt::Key_V); #else collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Insert); #endif _webSearchMenu = new KActionMenu(i18n("Web Search"), this); _webSearchMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts"))); _webSearchMenu->setVisible(false); collection->addAction(QStringLiteral("web-search"), _webSearchMenu); action = collection->addAction(QStringLiteral("select-all"), this, SLOT(selectAll())); action->setText(i18n("&Select All")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all"))); action = collection->addAction(QStringLiteral("select-line"), this, SLOT(selectLine())); action->setText(i18n("Select &Line")); action = KStandardAction::saveAs(this, SLOT(saveHistory()), collection); action->setText(i18n("Save Output &As...")); #ifdef Q_OS_MACOS action->setShortcut(QKeySequence(Qt::META + Qt::Key_S)); #endif action = KStandardAction::print(this, SLOT(print_screen()), collection); action->setText(i18n("&Print Screen...")); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_P); action = collection->addAction(QStringLiteral("adjust-history"), this, SLOT(showHistoryOptions())); action->setText(i18n("Adjust Scrollback...")); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); action = collection->addAction(QStringLiteral("clear-history"), this, SLOT(clearHistory())); action->setText(i18n("Clear Scrollback")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); action = collection->addAction(QStringLiteral("clear-history-and-reset"), this, SLOT(clearHistoryAndReset())); action->setText(i18n("Clear Scrollback and Reset")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_K); // Profile Options action = collection->addAction(QStringLiteral("edit-current-profile"), this, SLOT(editCurrentProfile())); action->setText(i18n("Edit Current Profile...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); _switchProfileMenu = new KActionMenu(i18n("Switch Profile"), this); collection->addAction(QStringLiteral("switch-profile"), _switchProfileMenu); // History _findAction = KStandardAction::find(this, SLOT(searchBarEvent()), collection); collection->setDefaultShortcut(_findAction, QKeySequence()); _findNextAction = KStandardAction::findNext(this, SLOT(findNextInHistory()), collection); collection->setDefaultShortcut(_findNextAction, QKeySequence()); _findNextAction->setEnabled(false); _findPreviousAction = KStandardAction::findPrev(this, SLOT(findPreviousInHistory()), collection); collection->setDefaultShortcut(_findPreviousAction, QKeySequence()); _findPreviousAction->setEnabled(false); // Character Encoding _codecAction = new KCodecAction(i18n("Set &Encoding"), this); _codecAction->setIcon(QIcon::fromTheme(QStringLiteral("character-set"))); collection->addAction(QStringLiteral("set-encoding"), _codecAction); connect(_codecAction->menu(), &QMenu::aboutToShow, this, &Konsole::SessionController::updateCodecAction); connect(_codecAction, static_cast(&KCodecAction::triggered), this, &Konsole::SessionController::changeCodec); // 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, static_cast(&KSelectAction::triggered), this, &Konsole::SessionController::copyInputActionsTriggered); action = collection->addAction(QStringLiteral("zmodem-upload"), this, SLOT(zmodemUpload())); action->setText(i18n("&ZModem Upload...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::ALT + Qt::Key_U); // Monitor KToggleAction* toggleAction = new KToggleAction(i18n("Monitor for &Activity"), this); collection->setDefaultShortcut(toggleAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_A); action = collection->addAction(QStringLiteral("monitor-activity"), toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorActivity); toggleAction = new KToggleAction(i18n("Monitor for &Silence"), this); collection->setDefaultShortcut(toggleAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_I); action = collection->addAction(QStringLiteral("monitor-silence"), toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorSilence); // Text Size action = collection->addAction(QStringLiteral("enlarge-font"), this, SLOT(increaseFontSize())); action->setText(i18n("Enlarge Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-more"))); QList enlargeFontShortcut; enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL + Qt::Key_Plus)); enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL + Qt::Key_Equal)); collection->setDefaultShortcuts(action, enlargeFontShortcut); action = collection->addAction(QStringLiteral("shrink-font"), this, SLOT(decreaseFontSize())); action->setText(i18n("Shrink Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-less"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::Key_Minus); 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, static_cast(&KSelectAction::triggered), this, &Konsole::SessionController::sendSignal); action = collection->addAction(QStringLiteral("sigstop-signal")); action->setText(i18n("&Suspend Task") + QStringLiteral(" (STOP)")); action->setData(SIGSTOP); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigcont-signal")); action->setText(i18n("&Continue Task") + QStringLiteral(" (CONT)")); action->setData(SIGCONT); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sighup-signal")); action->setText(i18n("&Hangup") + QStringLiteral(" (HUP)")); action->setData(SIGHUP); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigint-signal")); action->setText(i18n("&Interrupt Task") + QStringLiteral(" (INT)")); action->setData(SIGINT); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigterm-signal")); action->setText(i18n("&Terminate Task") + QStringLiteral(" (TERM)")); action->setData(SIGTERM); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigkill-signal")); action->setText(i18n("&Kill Task") + QStringLiteral(" (KILL)")); action->setData(SIGKILL); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigusr1-signal")); action->setText(i18n("User Signal &1") + QStringLiteral(" (USR1)")); action->setData(SIGUSR1); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigusr2-signal")); action->setText(i18n("User Signal &2") + QStringLiteral(" (USR2)")); action->setData(SIGUSR2); sendSignalActions->addAction(action); #ifdef Q_OS_MACOS collection->setDefaultShortcut(_findAction, Qt::META + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::META + Qt::Key_G); collection->setDefaultShortcut(_findPreviousAction, Qt::META + Qt::SHIFT + Qt::Key_G); #else collection->setDefaultShortcut(_findAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::Key_F3); collection->setDefaultShortcut(_findPreviousAction, Qt::SHIFT + Qt::Key_F3); #endif } -void SessionController::switchProfile(Profile::Ptr profile) +void SessionController::switchProfile(const Profile::Ptr &profile) { if (isReadOnly()) { return; } SessionManager::instance()->setSessionProfile(_session, profile); updateFilterList(profile); } void SessionController::prepareSwitchProfileMenu() { _switchProfileMenu->menu()->clear(); _switchProfileMenu->menu()->addActions(_profileList->actions()); } void SessionController::updateCodecAction() { _codecAction->setCurrentCodec(QString::fromUtf8(_session->codec())); } void SessionController::changeCodec(QTextCodec* codec) { _session->setCodec(codec); } EditProfileDialog* SessionController::profileDialogPointer() { return _editProfileDialog.data(); } void SessionController::editCurrentProfile() { // Searching for Edit profile dialog opened with the same profile const QList allSessionsControllers = _allControllers.values(); foreach (SessionController* session, allSessionsControllers) { if ((session->profileDialogPointer() != nullptr) && session->profileDialogPointer()->isVisible() && session->profileDialogPointer()->lookupProfile() == SessionManager::instance()->sessionProfile(_session)) { session->profileDialogPointer()->close(); } } // NOTE bug311270: For to prevent the crash, the profile must be reset. if (!_editProfileDialog.isNull()) { // exists but not visible delete _editProfileDialog.data(); } _editProfileDialog = new EditProfileDialog(QApplication::activeWindow()); _editProfileDialog.data()->setProfile(SessionManager::instance()->sessionProfile(_session)); _editProfileDialog.data()->show(); } void SessionController::renameSession() { 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(); } } } bool SessionController::confirmClose() const { if (_session->isForegroundProcessActive()) { QString title = _session->foregroundProcessName(); // hard coded for now. In future make it possible for the user to specify which programs // are ignored when considering whether to display a confirmation QStringList ignoreList; ignoreList << QString::fromUtf8(qgetenv("SHELL")).section(QLatin1Char('/'), -1); if (ignoreList.contains(title)) { return true; } QString question; if (title.isEmpty()) { question = i18n("A program is currently running in this session." " Are you sure you want to close it?"); } else { question = i18n("The program '%1' is currently running in this session." " Are you sure you want to close it?", title); } int result = KMessageBox::warningYesNo(_view->window(), question, i18n("Confirm Close")); return result == KMessageBox::Yes; } return true; } bool SessionController::confirmForceClose() const { if (_session->isRunning()) { QString title = _session->program(); // hard coded for now. In future make it possible for the user to specify which programs // are ignored when considering whether to display a confirmation QStringList ignoreList; ignoreList << QString::fromUtf8(qgetenv("SHELL")).section(QLatin1Char('/'), -1); if (ignoreList.contains(title)) { return true; } QString question; if (title.isEmpty()) { question = i18n("A program in this session would not die." " Are you sure you want to kill it by force?"); } else { question = i18n("The program '%1' is in this session would not die." " Are you sure you want to kill it by force?", title); } int result = KMessageBox::warningYesNo(_view->window(), question, i18n("Confirm Close")); return result == KMessageBox::Yes; } return true; } void SessionController::closeSession() { if (_preventClose) { return; } if (confirmClose()) { if (_session->closeInNormalWay()) { return; } else if (confirmForceClose()) { if (_session->closeInForceWay()) { return; } else { qCDebug(KonsoleDebug) << "Konsole failed to close a session in any way."; } } } } // Trying to open a remote Url may produce unexpected results. // Therefore, if a remote url, open the user's home path. // TODO consider: 1) disable menu upon remote session // 2) transform url to get the desired result (ssh -> sftp, etc) void SessionController::openBrowser() { const QUrl currentUrl = url(); if (currentUrl.isLocalFile()) { new KRun(currentUrl, QApplication::activeWindow(), true); } else { new KRun(QUrl::fromLocalFile(QDir::homePath()), QApplication::activeWindow(), true); } } void SessionController::copy() { _view->copyToClipboard(); } void SessionController::paste() { _view->pasteFromClipboard(); } void SessionController::pasteFromX11Selection() { _view->pasteFromX11Selection(); } void SessionController::selectAll() { _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()) { // ... 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().value(); switch (mode) { case CopyInputToAllTabsMode: copyInputToAllTabs(); break; case CopyInputToSelectedTabsMode: copyInputToSelectedTabs(); break; case CopyInputToNoneMode: copyInputToNone(); break; default: Q_ASSERT(false); } } void SessionController::copyInputToAllTabs() { if (_copyToGroup == nullptr) { _copyToGroup = new SessionGroup(this); } // Find our window ... const KXmlGuiWindow* myWindow = findWindow(_view); QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (auto session : group) { // First, ensure that the session is removed // (necessary to avoid duplicates on addSession()!) _copyToGroup->removeSession(session); // Add current session if it is displayed our window if (hasTerminalDisplayInSameWindow(session, myWindow)) { _copyToGroup->addSession(session); } } _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); snapshot(); } void SessionController::copyInputToSelectedTabs() { if (_copyToGroup == nullptr) { _copyToGroup = new SessionGroup(this); _copyToGroup->addSession(_session); _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); } QPointer dialog = new CopyInputDialog(_view); dialog->setMasterSession(_session); QSet currentGroup = QSet::fromList(_copyToGroup->sessions()); currentGroup.remove(_session); dialog->setChosenSessions(currentGroup); QPointer guard(_session); int result = dialog->exec(); if (guard.isNull()) { return; } if (result == QDialog::Accepted) { QSet newGroup = dialog->chosenSessions(); newGroup.remove(_session); QSet completeGroup = newGroup | currentGroup; foreach(Session * session, completeGroup) { if (newGroup.contains(session) && !currentGroup.contains(session)) { _copyToGroup->addSession(session); } else if (!newGroup.contains(session) && currentGroup.contains(session)) { _copyToGroup->removeSession(session); } } _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); snapshot(); } } void SessionController::copyInputToNone() { if (_copyToGroup == nullptr) { // No 'Copy To' is active return; } QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (auto iterator : group) { Session* session = iterator; if (session != _session) { _copyToGroup->removeSession(iterator); } } delete _copyToGroup; _copyToGroup = nullptr; snapshot(); } void SessionController::searchClosed() { _isSearchBarEnabled = false; searchHistory(false); } void SessionController::updateFilterList(Profile::Ptr profile) { if (profile != SessionManager::instance()->sessionProfile(_session)) { return; } bool underlineFiles = profile->underlineFilesEnabled(); if (!underlineFiles && (_fileFilter != nullptr)) { _view->filterChain()->removeFilter(_fileFilter); delete _fileFilter; _fileFilter = nullptr; } else if (underlineFiles && (_fileFilter == nullptr)) { _fileFilter = new FileFilter(_session); _view->filterChain()->addFilter(_fileFilter); } bool underlineLinks = profile->underlineLinksEnabled(); if (!underlineLinks && (_urlFilter != nullptr)) { _view->filterChain()->removeFilter(_urlFilter); delete _urlFilter; _urlFilter = nullptr; } else if (underlineLinks && (_urlFilter == nullptr)) { _urlFilter = new UrlFilter(); _view->filterChain()->addFilter(_urlFilter); } } void SessionController::setSearchStartToWindowCurrentLine() { setSearchStartTo(-1); } void SessionController::setSearchStartTo(int line) { _searchStartLine = line; _prevSearchResultLine = line; } void SessionController::listenForScreenWindowUpdates() { if (_listenForScreenWindowUpdates) { return; } connect(_view->screenWindow(), &Konsole::ScreenWindow::outputChanged, this, &Konsole::SessionController::updateSearchFilter); connect(_view->screenWindow(), &Konsole::ScreenWindow::scrolled, this, &Konsole::SessionController::updateSearchFilter); connect(_view->screenWindow(), &Konsole::ScreenWindow::currentResultLineChanged, _view.data(), static_cast(&Konsole::TerminalDisplay::update)); _listenForScreenWindowUpdates = true; } void SessionController::updateSearchFilter() { if ((_searchFilter != nullptr) && (!_searchBar.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(), static_cast(&QPrintDialog::accepted), options, &Konsole::PrintOptions::saveSettings); if (dialog->exec() != QDialog::Accepted) { return; } QPainter painter; painter.begin(&printer); KConfigGroup configGroup(KSharedConfig::openConfig(), "PrintOptions"); if (configGroup.readEntry("ScaleOutput", true)) { double scale = qMin(printer.pageRect().width() / static_cast(_view->width()), printer.pageRect().height() / static_cast(_view->height())); painter.scale(scale, scale); } _view->printContent(painter, configGroup.readEntry("PrinterFriendly", true)); } void SessionController::saveHistory() { SessionTask* task = new SaveHistoryTask(this); task->setAutoDelete(true); task->addSession(_session); task->execute(); } void SessionController::clearHistory() { _session->clearHistory(); _view->updateImage(); // To reset view scrollbar _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() == QStringLiteral("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()); } } } 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()) { // Remove content-specific actions // // If the close action was chosen, the popup menu will be partially // destroyed at this point, and the rest will be destroyed later by // 'chosen->trigger()' foreach(QAction * action, contentActions) { popup->removeAction(action); } delete contentSeparator; } _preventClose = false; if ((chosen != nullptr) && chosen->objectName() == QLatin1String("close-session")) { chosen->trigger(); } } else { qCDebug(KonsoleDebug) << "Unable to display popup menu for session" << _session->title(Session::NameRole) << ", no GUI factory available to build the popup."; } } void SessionController::movementKeyFromSearchBarReceived(QKeyEvent *event) { QCoreApplication::sendEvent(_view, event); setSearchStartToWindowCurrentLine(); } void SessionController::sessionStateChanged(int state) { if (state == _previousState) { return; } if (state == NOTIFYACTIVITY) { setIcon(*_activityIcon); _keepIconUntilInteraction = true; } else if (state == NOTIFYSILENCE) { setIcon(*_silenceIcon); _keepIconUntilInteraction = true; } else if (state == 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; } _session->setZModemBusy(true); QString zmodem = QStandardPaths::findExecutable(QStringLiteral("sz")); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable(QStringLiteral("lsz")); } if (zmodem.isEmpty()) { KMessageBox::sorry(_view, i18n("

No suitable ZModem software was found on this system.

" "

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

")); return; } QStringList files = QFileDialog::getOpenFileNames(_view, i18n("Select Files for ZModem Upload"), QDir::homePath()); if (!files.isEmpty()) { _session->startZModem(zmodem, QString(), files); } } bool SessionController::isKonsolePart() const { // Check to see if we are being called from Konsole or a KPart return !(qApp->applicationName() == QLatin1String("konsole")); } QString SessionController::userTitle() const { if (!_session.isNull()) { return _session->userTitle(); } else { return QString(); } } diff --git a/src/SessionController.h b/src/SessionController.h index 2fdfe126..01d22662 100644 --- a/src/SessionController.h +++ b/src/SessionController.h @@ -1,371 +1,371 @@ /* Copyright 2006-2008 by Robert Knight Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef SESSIONCONTROLLER_H #define SESSIONCONTROLLER_H // Qt #include #include #include #include // KDE #include // Konsole #include "ViewProperties.h" #include "Profile.h" #include "Enumeration.h" namespace KIO { class Job; } class QAction; class QTextCodec; class QKeyEvent; class QTimer; class QUrl; class KCodecAction; class KJob; class QAction; class KActionMenu; namespace Konsole { class Session; class SessionGroup; class ScreenWindow; class TerminalDisplay; class IncrementalSearchBar; class ProfileList; class RegExpFilter; class UrlFilter; class FileFilter; class EditProfileDialog; using SessionPtr = QPointer; /** * Provides the menu actions to manipulate a single terminal session and view pair. * The actions provided by this class are defined in the sessionui.rc XML file. * * SessionController monitors the session and provides access to basic information * about the session such as title(), icon() and currentDir(). SessionController * provides notifications of activity in the session via the activity() signal. * * When the controlled view receives the focus, the focused() signal is emitted * with a pointer to the controller. This can be used by main application window * which contains the view to plug the controller's actions into the menu when * the view is focused. */ class KONSOLEPRIVATE_EXPORT SessionController : public ViewProperties, public KXMLGUIClient { Q_OBJECT public: enum CopyInputToEnum { /** Copy keyboard input to all the other tabs in current window */ CopyInputToAllTabsMode = 0, /** Copy keyboard input to user selected tabs in current window */ CopyInputToSelectedTabsMode = 1, /** Do not copy keyboard input to other tabs */ CopyInputToNoneMode = 2 }; /** * Constructs a new SessionController which operates on @p session and @p view. */ SessionController(Session *session, TerminalDisplay *view, QObject *parent); ~SessionController() Q_DECL_OVERRIDE; /** Returns the session associated with this controller */ QPointer session() { return _session; } /** Returns the view associated with this controller */ QPointer view() { return _view; } /** * Returns the "window title" of the associated session. */ QString userTitle() const; /** * Returns true if the controller is valid. * A valid controller is one which has a non-null session() and view(). * * Equivalent to "!session().isNull() && !view().isNull()" */ bool isValid() const; /** Set the start line from which the next search will be done **/ void setSearchStartTo(int line); /** set start line to the first or last line (depending on the reverse search * setting) in the terminal display **/ void setSearchStartToWindowCurrentLine(); /** * Sets the action displayed in the session's context menu to hide or * show the menu bar. */ void setShowMenuAction(QAction *action); EditProfileDialog *profileDialogPointer(); // reimplemented QUrl url() const Q_DECL_OVERRIDE; QString currentDir() const Q_DECL_OVERRIDE; void rename() Q_DECL_OVERRIDE; bool confirmClose() const Q_DECL_OVERRIDE; virtual bool confirmForceClose() const; // Reimplemented to watch for events happening to the view bool eventFilter(QObject *watched, QEvent *event) Q_DECL_OVERRIDE; /** Returns the set of all controllers that exist. */ static QSet allControllers() { return _allControllers; } /* Returns true if called within a KPart; false if called within Konsole. */ bool isKonsolePart() const; bool isReadOnly() const; Q_SIGNALS: /** * Emitted when the view associated with the controller is focused. * This can be used by other classes to plug the controller's actions into a window's * menus. */ void focused(SessionController *controller); void rawTitleChanged(); /** * Emitted when the current working directory of the session associated with * the controller is changed. */ void currentDirectoryChanged(const QString &dir); /** * Emitted when the user changes the tab title. */ void tabRenamedByUser(bool renamed) const; public Q_SLOTS: /** * Issues a command to the session to navigate to the specified URL. * This may not succeed if the foreground program does not understand * the command sent to it ( 'cd path' for local URLs ) or is not * responding to input. * * openUrl() currently supports urls for local paths and those * using the 'ssh' protocol ( eg. "ssh://joebloggs@hostname" ) */ void openUrl(const QUrl &url); /** * update actions which are meaningful only when primary screen is in use. */ void setupPrimaryScreenSpecificActions(bool use); /** * update actions which are closely related with the selected text. */ void selectionChanged(const QString &selectedText); /** * close the associated session. This might involve user interaction for * confirmation. */ void closeSession(); /** Increase font size */ void increaseFontSize(); /** Decrease font size */ void decreaseFontSize(); /** Reset font size */ void resetFontSize(); /** Close the incremental search */ void searchClosed(); // called when the user clicks on the private Q_SLOTS: // menu item handlers void openBrowser(); void copy(); void paste(); void selectAll(); void selectLine(); void pasteFromX11Selection(); // shortcut only void copyInputActionsTriggered(QAction *action); void copyInputToAllTabs(); void copyInputToSelectedTabs(); void copyInputToNone(); void editCurrentProfile(); void changeCodec(QTextCodec *codec); void enableSearchBar(bool showSearchBar); void searchHistory(bool showSearchBar); void searchBarEvent(); void searchFrom(); void findNextInHistory(); void findPreviousInHistory(); void changeSearchMatch(); void print_screen(); void saveHistory(); void showHistoryOptions(); void clearHistory(); void clearHistoryAndReset(); void monitorActivity(bool monitor); void monitorSilence(bool monitor); void renameSession(); - void switchProfile(Profile::Ptr profile); + void switchProfile(const Profile::Ptr &profile); void handleWebShortcutAction(); void configureWebShortcuts(); void sendSignal(QAction *action); void sendBackgroundColor(); void toggleReadOnly(); // other void setupSearchBar(); void prepareSwitchProfileMenu(); void updateCodecAction(); void showDisplayContextMenu(const QPoint &position); void movementKeyFromSearchBarReceived(QKeyEvent *event); void sessionStateChanged(int state); void sessionAttributeChanged(); void sessionReadOnlyChanged(); void searchTextChanged(const QString &text); void searchCompleted(bool success); void updateFilterList(Profile::Ptr profile); // Called when the profile has changed, so we might need to change the list of filters void interactionHandler(); void snapshot(); // called periodically as the user types // to take a snapshot of the state of the // foreground process in the terminal void highlightMatches(bool highlight); void scrollBackOptionsChanged(int mode, int lines); void sessionResizeRequest(const QSize &size); void trackOutput(QKeyEvent *event); // move view to end of current output // when a key press occurs in the // display area void updateSearchFilter(); void zmodemDownload(); void zmodemUpload(); // update actions related with selected text void updateCopyAction(const QString &selectedText); void updateWebSearchMenu(); private: Q_DISABLE_COPY(SessionController) // begins the search // text - pattern to search for // direction - value from SearchHistoryTask::SearchDirection enum to specify // the search direction void beginSearch(const QString &text, Enum::SearchDirection direction); QRegularExpression regexpFromSearchBarOptions() const; bool reverseSearchChecked() const; void setupCommonActions(); void setupExtraActions(); void removeSearchFilter(); // remove and delete the current search filter if set void setFindNextPrevEnabled(bool enabled); void listenForScreenWindowUpdates(); private: void updateSessionIcon(); void updateReadOnlyActionStates(); QPointer _session; QPointer _view; SessionGroup *_copyToGroup; ProfileList *_profileList; QIcon _sessionIcon; QString _sessionIconName; int _previousState; RegExpFilter *_searchFilter; UrlFilter *_urlFilter; FileFilter *_fileFilter; QAction *_copyInputToAllTabsAction; QAction *_findAction; QAction *_findNextAction; QAction *_findPreviousAction; QTimer *_interactionTimer; int _searchStartLine; int _prevSearchResultLine; KCodecAction *_codecAction; KActionMenu *_switchProfileMenu; KActionMenu *_webSearchMenu; bool _listenForScreenWindowUpdates; bool _preventClose; bool _keepIconUntilInteraction; QString _selectedText; QAction *_showMenuAction; static QSet _allControllers; static int _lastControllerId; QStringList _bookmarkValidProgramsToClear; bool _isSearchBarEnabled; QPointer _editProfileDialog; QString _searchText; QPointer _searchBar; }; inline bool SessionController::isValid() const { return !_session.isNull() && !_view.isNull(); } } #endif //SESSIONCONTROLLER_H diff --git a/src/SessionManager.cpp b/src/SessionManager.cpp index 5bc299b2..770a187f 100644 --- a/src/SessionManager.cpp +++ b/src/SessionManager.cpp @@ -1,389 +1,389 @@ /* 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()) { 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 foreach (Session *session, _sessions) { disconnect(session, nullptr, this, nullptr); } } } Q_GLOBAL_STATIC(SessionManager, theSessionManager) SessionManager* SessionManager::instance() { return theSessionManager; } void SessionManager::closeAllSessions() { // close remaining sessions foreach (Session *session, _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(Profile::Ptr profile) +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(Profile::Ptr profile, bool modifiedPropertiesOnly) +void SessionManager::applyProfile(const Profile::Ptr &profile, bool modifiedPropertiesOnly) { foreach (Session *session, _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); if (_sessionProfiles[session] == profile) { return; } _sessionProfiles[session] = profile; applyProfile(session, profile, false); emit sessionUpdated(session); } -void SessionManager::applyProfile(Session *session, const Profile::Ptr profile, +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_PROFILE_NAME=%1").arg(profile->name()); 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; foreach (TerminalDisplay *view, session->views()) { 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(); foreach (Session *session, _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) { foreach (Session *session, _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/SessionManager.h b/src/SessionManager.h index 49398cfa..ee62d374 100644 --- a/src/SessionManager.h +++ b/src/SessionManager.h @@ -1,157 +1,157 @@ /* 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. */ #ifndef SESSIONMANAGER_H #define SESSIONMANAGER_H // Qt #include #include // Konsole #include "Profile.h" class KConfig; namespace Konsole { class Session; /** * Manages running terminal sessions. */ class KONSOLEPRIVATE_EXPORT SessionManager : public QObject { Q_OBJECT public: /** * Constructs a new session manager and loads information about the available * profiles. */ SessionManager(); /** * Destroys the SessionManager. All running sessions should be closed * (via closeAllSessions()) before the SessionManager is destroyed. */ ~SessionManager() Q_DECL_OVERRIDE; /** * Returns the session manager instance. */ static SessionManager *instance(); /** Kill all running sessions. */ void closeAllSessions(); /** * Creates a new session using the settings specified by the specified * profile. * * The new session has no views associated with it. A new TerminalDisplay view * must be created in order to display the output from the terminal session and * send keyboard or mouse input to it. * * @param profile A profile containing the settings for the new session. If @p profile * is null the default profile (see ProfileManager::defaultProfile()) will be used. */ Session *createSession(Profile::Ptr profile = Profile::Ptr()); /** Sets the profile associated with a session. */ void setSessionProfile(Session *session, Profile::Ptr profile); /** Returns the profile associated with a session. */ Profile::Ptr sessionProfile(Session *session) const; /** * Returns a list of active sessions. */ const QList sessions() const; // System session management void saveSessions(KConfig *config); void restoreSessions(KConfig *config); int getRestoreId(Session *session); Session *idToSession(int id); Q_SIGNALS: /** * Emitted when a session's settings are updated to match * its current profile. */ void sessionUpdated(Session *session); protected Q_SLOTS: /** * Called to inform the manager that a session has finished executing. * * @param session The Session which has finished executing. */ void sessionTerminated(Session *session); private Q_SLOTS: void sessionProfileCommandReceived(const QString &text); - void profileChanged(Profile::Ptr profile); + void profileChanged(const Profile::Ptr &profile); private: Q_DISABLE_COPY(SessionManager) // applies updates to a profile // to all sessions currently using that profile // if modifiedPropertiesOnly is true, only properties which // are set in the profile @p key are updated - void applyProfile(Profile::Ptr profile, bool modifiedPropertiesOnly); + void applyProfile(const Profile::Ptr &profile, bool modifiedPropertiesOnly); // applies updates to the profile @p profile to the session @p session // if modifiedPropertiesOnly is true, only properties which // are set in @p profile are update ( ie. properties for which profile->isPropertySet() // returns true ) - void applyProfile(Session *session, const Profile::Ptr profile, bool modifiedPropertiesOnly); + void applyProfile(Session *session, const Profile::Ptr &profile, bool modifiedPropertiesOnly); QList _sessions; // list of running sessions QHash _sessionProfiles; QHash _sessionRuntimeProfiles; QHash _restoreMapping; }; /** Utility class to simplify code in SessionManager::applyProfile(). */ class ShouldApplyProperty { public: - ShouldApplyProperty(const Profile::Ptr profile, bool modifiedOnly) : + ShouldApplyProperty(const Profile::Ptr &profile, bool modifiedOnly) : _profile(profile), _modifiedPropertiesOnly(modifiedOnly) { } bool shouldApply(Profile::Property property) const { return !_modifiedPropertiesOnly || _profile->isPropertySet(property); } private: const Profile::Ptr _profile; bool _modifiedPropertiesOnly; }; } #endif //SESSIONMANAGER_H diff --git a/src/TerminalDisplay.cpp b/src/TerminalDisplay.cpp index e8c8ce1c..37ccb71c 100644 --- a/src/TerminalDisplay.cpp +++ b/src/TerminalDisplay.cpp @@ -1,4201 +1,4201 @@ /* This file is part of Konsole, a terminal emulator for KDE. Copyright 2006-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "TerminalDisplay.h" // Config #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include // Konsole #include "Filter.h" #include "konsoledebug.h" #include "TerminalCharacterDecoder.h" #include "Screen.h" #include "LineFont.h" #include "SessionController.h" #include "ExtendedCharTable.h" #include "TerminalDisplayAccessible.h" #include "SessionManager.h" #include "Session.h" #include "WindowSystemInfo.h" #include "IncrementalSearchBar.h" using namespace Konsole; #define REPCHAR "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "abcdefgjijklmnopqrstuvwxyz" \ "0123456789./+@" // we use this to force QPainter to display text in LTR mode // more information can be found in: http://unicode.org/reports/tr9/ const QChar LTR_OVERRIDE_CHAR(0x202D); inline int TerminalDisplay::loc(int x, int y) const { Q_ASSERT(y >= 0 && y < _lines); Q_ASSERT(x >= 0 && x < _columns); x = qBound(0, x, _columns - 1); y = qBound(0, y, _lines - 1); return y * _columns + x; } /* ------------------------------------------------------------------------- */ /* */ /* Colors */ /* */ /* ------------------------------------------------------------------------- */ /* Note that we use ANSI color order (bgr), while IBMPC color order is (rgb) Code 0 1 2 3 4 5 6 7 ----------- ------- ------- ------- ------- ------- ------- ------- ------- ANSI (bgr) Black Red Green Yellow Blue Magenta Cyan White IBMPC (rgb) Black Blue Green Cyan Red Magenta Yellow White */ ScreenWindow* TerminalDisplay::screenWindow() const { return _screenWindow; } void TerminalDisplay::setScreenWindow(ScreenWindow* window) { // disconnect existing screen window if any if (!_screenWindow.isNull()) { disconnect(_screenWindow , nullptr , this , nullptr); } _screenWindow = window; if (!_screenWindow.isNull()) { connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateLineProperties); connect(_screenWindow.data() , &Konsole::ScreenWindow::outputChanged , this , &Konsole::TerminalDisplay::updateImage); connect(_screenWindow.data() , &Konsole::ScreenWindow::currentResultLineChanged , this , &Konsole::TerminalDisplay::updateImage); connect(_screenWindow.data(), &Konsole::ScreenWindow::outputChanged, this, [this]() { _filterUpdateRequired = true; }); connect(_screenWindow.data(), &Konsole::ScreenWindow::scrolled, this, [this]() { _filterUpdateRequired = true; }); _screenWindow->setWindowLines(_lines); } } const ColorEntry* TerminalDisplay::colorTable() const { return _colorTable; } void TerminalDisplay::onColorsChanged() { // Mostly just fix the scrollbar // this is a workaround to add some readability to old themes like Fusion // changing the light value for button a bit makes themes like fusion, windows and oxygen way more readable and pleasing QPalette p = QApplication::palette(); QColor buttonTextColor = _colorTable[DEFAULT_FORE_COLOR]; QColor backgroundColor = _colorTable[DEFAULT_BACK_COLOR]; backgroundColor.setAlphaF(_opacity); QColor buttonColor = backgroundColor.toHsv(); if (buttonColor.valueF() < 0.5) { buttonColor = buttonColor.lighter(); } else { buttonColor = buttonColor.darker(); } p.setColor(QPalette::Button, buttonColor); p.setColor(QPalette::Window, backgroundColor); p.setColor(QPalette::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 isSupportedLineChar(string.at(0).unicode()); } void TerminalDisplay::fontChange(const QFont&) { QFontMetrics fm(font()); _fontHeight = fm.height() + _lineSpacing; Q_ASSERT(_fontHeight > 0); // waba TerminalDisplay 1.123: // "Base character width on widest ASCII character. This prevents too wide // characters in the presence of double wide (e.g. Japanese) characters." // Get the width from representative normal width characters _fontWidth = qRound((static_cast(fm.width(QStringLiteral(REPCHAR))) / static_cast(qstrlen(REPCHAR)))); _fixedFont = true; const int fw = fm.width(QLatin1Char(REPCHAR[0])); for (unsigned int i = 1; i < qstrlen(REPCHAR); i++) { if (fw != fm.width(QLatin1Char(REPCHAR[i]))) { _fixedFont = false; break; } } if (_fontWidth < 1) { _fontWidth = 1; } _fontAscent = fm.ascent(); emit changedFontMetricSignal(_fontHeight, _fontWidth); propagateSize(); update(); } void TerminalDisplay::setVTFont(const QFont& f) { QFont newFont(f); int strategy = 0; QFontMetrics fontMetrics(newFont); // This check seems extreme and semi-random // TODO: research if these checks are still needed to prevent // enormous fonts from being used; consider usage on big TV // screens. if ((fontMetrics.height() > height()) || (fontMetrics.maxWidth() > width())) { // return here will cause the "general" non-fixed width font // to be selected return; } // 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())) % comma % QString::number(fontInfo.weight()) % comma % QString::number(static_cast(fontInfo.style())) % comma % QString::number(static_cast(fontInfo.underline())) % comma % QString::number(static_cast(fontInfo.strikeOut())) % comma % QString::number(static_cast(fontInfo.fixedPitch())) % comma % QString::number(static_cast(fontInfo.rawMode())); qCDebug(KonsoleDebug) << "The font to use in the terminal can not be matched exactly on your system."; qCDebug(KonsoleDebug)<<" Selected: "<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; setVTFont(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)) , _searchResultRect(QRect()) { // 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); 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->addStretch(); _verticalLayout->setSpacing(0); setLayout(_verticalLayout); // Take the scrollbar into account and add a margin to the layout. Without the timer the scrollbar width // is garbage. QTimer::singleShot(0, this, [this]() { const int scrollBarWidth = _scrollBar->isVisible() ? geometry().intersected(_scrollBar->geometry()).width() : 0; _verticalLayout->setContentsMargins(0, 0, scrollBarWidth, 0); }); new AutoScrollHandler(this); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(Konsole::accessibleInterfaceFactory); #endif } TerminalDisplay::~TerminalDisplay() { disconnect(_blinkTextTimer); disconnect(_blinkCursorTimer); delete _readOnlyMessageWidget; delete _outputSuspendedMessageWidget; delete[] _image; delete _filterChain; _readOnlyMessageWidget = nullptr; _outputSuspendedMessageWidget = nullptr; } /* ------------------------------------------------------------------------- */ /* */ /* Display Operations */ /* */ /* ------------------------------------------------------------------------- */ /** A table for emulating the simple (single width) unicode drawing chars. It represents the 250x - 257x glyphs. If it's zero, we can't use it. if it's not, it's encoded as follows: imagine a 5x5 grid where the points are numbered 0 to 24 left to top, top to bottom. Each point is represented by the corresponding bit. Then, the pixels basically have the following interpretation: _|||_ -...- -...- -...- _|||_ where _ = none | = vertical line. - = horizontal line. */ enum LineEncode { TopL = (1 << 1), TopC = (1 << 2), TopR = (1 << 3), LeftT = (1 << 5), Int11 = (1 << 6), Int12 = (1 << 7), Int13 = (1 << 8), RightT = (1 << 9), LeftC = (1 << 10), Int21 = (1 << 11), Int22 = (1 << 12), Int23 = (1 << 13), RightC = (1 << 14), LeftB = (1 << 15), Int31 = (1 << 16), Int32 = (1 << 17), Int33 = (1 << 18), RightB = (1 << 19), BotL = (1 << 21), BotC = (1 << 22), BotR = (1 << 23) }; static void drawLineChar(QPainter& paint, int x, int y, int w, int h, uchar code) { //Calculate cell midpoints, end points. const int cx = x + w / 2; const int cy = y + h / 2. - 0.5; const int ex = x + w - 1; const int ey = y + h - 1; const quint32 toDraw = LineChars[code]; //Top _lines: if ((toDraw & TopL) != 0u) { paint.drawLine(cx - 1, y, cx - 1, cy - 2); } if ((toDraw & TopC) != 0u) { paint.drawLine(cx, y, cx, cy - 2); } if ((toDraw & TopR) != 0u) { paint.drawLine(cx + 1, y, cx + 1, cy - 2); } //Bot _lines: if ((toDraw & BotL) != 0u) { paint.drawLine(cx - 1, cy + 2, cx - 1, ey); } if ((toDraw & BotC) != 0u) { paint.drawLine(cx, cy + 2, cx, ey); } if ((toDraw & BotR) != 0u) { paint.drawLine(cx + 1, cy + 2, cx + 1, ey); } //Left _lines: if ((toDraw & LeftT) != 0u) { paint.drawLine(x, cy - 1, cx - 2, cy - 1); } if ((toDraw & LeftC) != 0u) { paint.drawLine(x, cy, cx - 2, cy); } if ((toDraw & LeftB) != 0u) { paint.drawLine(x, cy + 1, cx - 2, cy + 1); } //Right _lines: if ((toDraw & RightT) != 0u) { paint.drawLine(cx + 2, cy - 1, ex, cy - 1); } if ((toDraw & RightC) != 0u) { paint.drawLine(cx + 2, cy, ex, cy); } if ((toDraw & RightB) != 0u) { paint.drawLine(cx + 2, cy + 1, ex, cy + 1); } //Intersection points. if ((toDraw & Int11) != 0u) { paint.drawPoint(cx - 2, cy - 2); } if ((toDraw & Int12) != 0u) { paint.drawPoint(cx - 1, cy - 2); } if ((toDraw & Int13) != 0u) { paint.drawPoint(cx - 0, cy - 2); } if ((toDraw & Int21) != 0u) { paint.drawPoint(cx - 2, cy - 1); } if ((toDraw & Int22) != 0u) { paint.drawPoint(cx - 1, cy - 1); } if ((toDraw & Int23) != 0u) { paint.drawPoint(cx - 0, cy - 1); } if ((toDraw & Int31) != 0u) { paint.drawPoint(cx - 2, cy); } if ((toDraw & Int32) != 0u) { paint.drawPoint(cx - 1, cy); } if ((toDraw & Int33) != 0u) { paint.drawPoint(cx - 0, cy); } } static void drawOtherChar(QPainter& paint, int x, int y, int w, int h, uchar code) { //Calculate cell midpoints, end points. const int cx = x + w / 2; const int cy = y + h / 2. - 0.5; // Compensate for the translation, to match fonts const int ex = x + w - 1; const int ey = y + h - 1; // Double dashes if (0x4C <= code && code <= 0x4F) { const int xHalfGap = qMax(w / 15, 1); const int yHalfGap = qMax(h / 15, 1); switch (code) { case 0x4D: // BOX DRAWINGS HEAVY DOUBLE DASH HORIZONTAL paint.drawLine(x, cy - 1, cx - xHalfGap - 1, cy - 1); paint.drawLine(x, cy + 1, cx - xHalfGap - 1, cy + 1); paint.drawLine(cx + xHalfGap, cy - 1, ex, cy - 1); paint.drawLine(cx + xHalfGap, cy + 1, ex, cy + 1); // No break! Q_FALLTHROUGH(); case 0x4C: // BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL paint.drawLine(x, cy, cx - xHalfGap - 1, cy); paint.drawLine(cx + xHalfGap, cy, ex, cy); break; case 0x4F: // BOX DRAWINGS HEAVY DOUBLE DASH VERTICAL paint.drawLine(cx - 1, y, cx - 1, cy - yHalfGap - 1); paint.drawLine(cx + 1, y, cx + 1, cy - yHalfGap - 1); paint.drawLine(cx - 1, cy + yHalfGap, cx - 1, ey); paint.drawLine(cx + 1, cy + yHalfGap, cx + 1, ey); // No break! Q_FALLTHROUGH(); case 0x4E: // BOX DRAWINGS LIGHT DOUBLE DASH VERTICAL paint.drawLine(cx, y, cx, cy - yHalfGap - 1); paint.drawLine(cx, cy + yHalfGap, cx, ey); break; } } // Rounded corner characters else if (0x6D <= code && code <= 0x70) { const int r = w * 3 / 8; const int d = 2 * r; switch (code) { case 0x6D: // BOX DRAWINGS LIGHT ARC DOWN AND RIGHT paint.drawLine(cx, cy + r, cx, ey); paint.drawLine(cx + r, cy, ex, cy); paint.drawArc(cx, cy, d, d, 90 * 16, 90 * 16); break; case 0x6E: // BOX DRAWINGS LIGHT ARC DOWN AND LEFT paint.drawLine(cx, cy + r, cx, ey); paint.drawLine(x, cy, cx - r, cy); paint.drawArc(cx - d, cy, d, d, 0 * 16, 90 * 16); break; case 0x6F: // BOX DRAWINGS LIGHT ARC UP AND LEFT paint.drawLine(cx, y, cx, cy - r); paint.drawLine(x, cy, cx - r, cy); paint.drawArc(cx - d, cy - d, d, d, 270 * 16, 90 * 16); break; case 0x70: // BOX DRAWINGS LIGHT ARC UP AND RIGHT paint.drawLine(cx, y, cx, cy - r); paint.drawLine(cx + r, cy, ex, cy); paint.drawArc(cx, cy - d, d, d, 180 * 16, 90 * 16); break; } } // Diagonals else if (0x71 <= code && code <= 0x73) { switch (code) { case 0x71: // BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT paint.drawLine(ex, y, x, ey); break; case 0x72: // BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT paint.drawLine(x, y, ex, ey); break; case 0x73: // BOX DRAWINGS LIGHT DIAGONAL CROSS paint.drawLine(ex, y, x, ey); paint.drawLine(x, y, ex, ey); break; } } } static void drawBlockChar(QPainter& paint, int x, int y, int w, int h, uchar code) { const QColor color = paint.pen().color(); const float left = x - 0.5; const float top = y - 0.5; const float cx = left + w / 2; const float cy = top + h / 2; const float right = x + w - 0.5; const float bottom = y + h - 0.5; // Default rect fills entire cell QRectF rect(left, top, w, h); // LOWER ONE EIGHTH BLOCK to LEFT ONE EIGHTH BLOCK if (code >= 0x81 && code <= 0x8f) { if (code < 0x88) { // Horizontal const int height = h * (0x88 - code) / 8; rect.setY(top + height); rect.setHeight(h - height); } else if (code > 0x88) { // Vertical const int width = w * (0x90 - code) / 8; rect.setWidth(width); } paint.fillRect(rect, color); return; } // Combinations of quarter squares // LEFT ONE EIGHTH BLOCK to QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT if (code >= 0x96 && code <= 0x9F) { bool upperLeft = false, upperRight = false, lowerLeft = false, lowerRight = false; switch(code) { case 0x96: lowerLeft = true; break; case 0x97: lowerRight = true; break; case 0x98: upperLeft = true; break; case 0x99: upperLeft = true; lowerLeft = true; lowerRight = true; break; case 0x9a: upperLeft = true; lowerRight = true; break; case 0x9b: upperLeft = true; upperRight = true; lowerLeft = true; break; case 0x9c: upperLeft = true; upperRight = true; lowerRight = true; break; case 0x9d: upperRight = true; break; case 0x9e: upperRight = true; lowerLeft = true; break; case 0x9f: upperRight = true; lowerLeft = true; lowerRight = true; break; default: break; } if (upperLeft) { paint.fillRect(QRectF(QPointF(left, top), QPointF(cx, cy)), color); } if (upperRight) { paint.fillRect(QRectF(QPointF(cx, top), QPointF(right, cy)), color); } if (lowerLeft) { paint.fillRect(QRectF(QPointF(left, cy), QPointF(cx, bottom)), color); } if (lowerRight) { paint.fillRect(QRectF(QPointF(cx, cy), QPointF(right, bottom)), color); } return; } // And the random stuff switch(code) { case 0x80: // Top half block rect.setHeight(h / 2); paint.fillRect(rect, color); return; case 0x90: // Right half block paint.fillRect(QRectF(QPointF(cx, top), QPointF(right, bottom)), color); return; case 0x94: // Top one eighth block rect.setHeight(h / 8); paint.fillRect(rect, color); return; case 0x95: { // Right one eighth block const float width = 7 * w / 8; rect.setX(left + width); rect.setWidth(w - width); paint.fillRect(rect, color); return; } case 0x91: // Light shade paint.fillRect(rect, QBrush(color, Qt::Dense6Pattern)); return; case 0x92: // Medium shade paint.fillRect(rect, QBrush(color, Qt::Dense4Pattern)); return; case 0x93: // Dark shade paint.fillRect(rect, QBrush(color, Qt::Dense2Pattern)); return; default: break; } } void TerminalDisplay::drawLineCharString(QPainter& painter, int x, int y, const QString& str, const Character* attributes) { painter.save(); // For antialiasing, we need to shift it so the single pixel width is in the middle painter.translate(0.5, 0.5); if (((attributes->rendition & RE_BOLD) != 0) && _boldIntense) { QPen boldPen(painter.pen()); boldPen.setWidth(4); painter.setPen(boldPen); } for (int i = 0 ; i < str.length(); i++) { const uchar code = str[i].cell(); if (code >= 0x80 && code <= 0x9F) { // UPPER HALF BLOCK to QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT drawBlockChar(painter, x + (_fontWidth * i), y, _fontWidth, _fontHeight, code); } else if (LineChars[code] != 0u) { drawLineChar(painter, x + (_fontWidth * i), y, _fontWidth, _fontHeight, code); } else { drawOtherChar(painter, x + (_fontWidth * i), y, _fontWidth, _fontHeight, code); } } painter.restore(); } 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; // enable automatic background filling to prevent the display // flickering if there is no transparency /*if ( color.alpha() == 255 ) { setAutoFillBackground(true); } else { setAutoFillBackground(false); }*/ _blendColor = color.rgba(); onColorsChanged(); } -void TerminalDisplay::setWallpaper(ColorSchemeWallpaper::Ptr p) +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(penWidth / 2 + 0.5, penWidth / 2 + 0.5, - penWidth / 2 - penWidth % 2 + 0.5, - 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) { painter.drawLine(cursorRect.left(), cursorRect.bottom(), cursorRect.right(), cursorRect.bottom()); } else if (_cursorShape == Enum::IBeamCursor) { painter.drawLine(cursorRect.left(), cursorRect.top(), cursorRect.left(), cursorRect.bottom()); } } void TerminalDisplay::drawCharacters(QPainter& painter, const QRect& rect, const QString& text, const Character* style, bool invertCharacterColor) { // don't draw text which is currently blinking if (_textBlinking && ((style->rendition & RE_BLINK) != 0)) { return; } // don't draw concealed characters if ((style->rendition & RE_CONCEAL) != 0) { return; } // setup bold and underline bool useBold = (((style->rendition & RE_BOLD) != 0) && _boldIntense) || font().bold(); const bool useUnderline = ((style->rendition & RE_UNDERLINE) != 0) || font().underline(); const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || font().italic(); const bool useStrikeOut = ((style->rendition & RE_STRIKEOUT) != 0) || font().strikeOut(); const bool useOverline = ((style->rendition & RE_OVERLINE) != 0) || font().overline(); QFont font = painter.font(); if (font.bold() != useBold || font.underline() != useUnderline || font.italic() != useItalic || font.strikeOut() != useStrikeOut || font.overline() != useOverline) { font.setBold(useBold); font.setUnderline(useUnderline); font.setItalic(useItalic); font.setStrikeOut(useStrikeOut); font.setOverline(useOverline); painter.setFont(font); } // setup pen const CharacterColor& textColor = (invertCharacterColor ? style->backgroundColor : style->foregroundColor); const QColor color = textColor.color(_colorTable); QPen pen = painter.pen(); if (pen.color() != color) { pen.setColor(color); painter.setPen(color); } // draw text if (isLineCharString(text) && !_useFontLineCharacters) { drawLineCharString(painter, rect.x(), rect.y(), text, style); } else { // Force using LTR as the document layout for the terminal area, because // there is no use cases for RTL emulator and RTL terminal application. // // This still allows RTL characters to be rendered in the RTL way. painter.setLayoutDirection(Qt::LeftToRight); painter.setClipRect(rect); 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.setClipping(false); } } 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); //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); //set region of display to scroll scrollRect.setTop(top); } else { // check that the memory areas that we are going to move are valid Q_ASSERT((char*)firstCharPos + bytesToMove < (char*)(_image + (_lines * _columns))); //scroll internal image up memmove(lastCharPos , firstCharPos , bytesToMove); //set region of the display to scroll scrollRect.setTop(top + abs(lines) * _fontHeight); } scrollRect.setHeight(linesToMove * _fontHeight); Q_ASSERT(scrollRect.isValid() && !scrollRect.isEmpty()); //scroll the display vertically to match internal _image scroll(0 , _fontHeight * (-lines) , scrollRect); } QRegion TerminalDisplay::hotSpotRegion() const { QRegion region; foreach(Filter::HotSpot * hotSpot , _filterChain->hotSpots()) { QRect r; if (hotSpot->startLine() == hotSpot->endLine()) { r.setLeft(hotSpot->startColumn()); r.setTop(hotSpot->startLine()); r.setRight(hotSpot->endColumn()); r.setBottom(hotSpot->endLine()); region |= imageToWidget(r); } else { r.setLeft(hotSpot->startColumn()); r.setTop(hotSpot->startLine()); r.setRight(_columns); r.setBottom(hotSpot->startLine()); region |= imageToWidget(r); for (int line = hotSpot->startLine() + 1 ; line < hotSpot->endLine() ; line++) { r.setLeft(0); r.setTop(line); r.setRight(_columns); r.setBottom(line); region |= imageToWidget(r); } r.setLeft(0); r.setTop(hotSpot->endLine()); r.setRight(hotSpot->endColumn()); r.setBottom(hotSpot->endLine()); region |= imageToWidget(r); } } return region; } void TerminalDisplay::processFilters() { if (_screenWindow.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 = newLine[x + 0].isLineChar(); const bool doubleWidth = (x + 1 == columnsToUpdate) ? false : (newLine[x + 1].character == 0); const RenditionFlags cr = newLine[x].rendition; const CharacterColor clipboard = newLine[x].backgroundColor; if (newLine[x].foregroundColor != cf) { cf = newLine[x].foregroundColor; } const int lln = columnsToUpdate - x; for (len = 1; len < lln; ++len) { const Character& ch = newLine[x + len]; if (ch.character == 0u) { continue; // Skip trailing part of multi-col chars. } const bool nextIsDoubleWidth = (x + len + 1 == columnsToUpdate) ? false : (newLine[x + len + 1].character == 0); if (ch.foregroundColor != cf || ch.backgroundColor != clipboard || (ch.rendition & ~RE_EXTENDED_CHAR) != (cr & ~RE_EXTENDED_CHAR) || (dirtyMask[x + len] == 0) || ch.isLineChar() != lineDraw || nextIsDoubleWidth != doubleWidth) { break; } } const bool saveFixedFont = _fixedFont; if (lineDraw) { _fixedFont = false; } if (doubleWidth) { _fixedFont = false; } updateLine = true; _fixedFont = saveFixedFont; x += len - 1; } } } //both the top and bottom halves of double height _lines must always be redrawn //although both top and bottom halves contain the same characters, only //the top one is actually //drawn. if (_lineProperties.count() > y) { updateLine |= (_lineProperties[y] & LINE_DOUBLEHEIGHT); } // if the characters on the line are different in the old and the new _image // then this line must be repainted. if (updateLine) { dirtyLineCount++; // add the area occupied by this line to the region which needs to be // repainted QRect dirtyRect = QRect(_contentRect.left() + tLx , _contentRect.top() + tLy + _fontHeight * y , _fontWidth * columnsToUpdate , _fontHeight); dirtyRegion |= dirtyRect; } // replace the line of characters in the old _image with the // current line of the new _image memcpy((void*)currentLine, (const void*)newLine, columnsToUpdate * sizeof(Character)); } // if the new _image is smaller than the previous _image, then ensure that the area // outside the new _image is cleared if (linesToUpdate < _usedLines) { dirtyRegion |= QRect(_contentRect.left() + tLx , _contentRect.top() + tLy + _fontHeight * linesToUpdate , _fontWidth * _columns , _fontHeight * (_usedLines - linesToUpdate)); } _usedLines = linesToUpdate; if (columnsToUpdate < _usedColumns) { dirtyRegion |= QRect(_contentRect.left() + tLx + columnsToUpdate * _fontWidth , _contentRect.top() + tLy , _fontWidth * (_usedColumns - columnsToUpdate) , _fontHeight * _lines); } _usedColumns = columnsToUpdate; dirtyRegion |= _inputMethodData.previousPreeditRect; if ((_screenWindow->currentResultLine() != -1) && (_screenWindow->scrollCount() != 0)) { // De-highlight previous result region dirtyRegion |= _searchResultRect; // Highlight new result region dirtyRegion |= QRect(0, _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _fontHeight, _columns * _fontWidth, _fontHeight); } _screenWindow->resetScrollCount(); // update the parts of the display which have changed update(dirtyRegion); if (_allowBlinkingText && _hasTextBlinker && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!_hasTextBlinker && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } delete[] dirtyMask; #ifndef QT_NO_ACCESSIBILITY QAccessibleEvent dataChangeEvent(this, QAccessible::VisibleDataChanged); QAccessible::updateAccessibility(&dataChangeEvent); QAccessibleTextCursorEvent cursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX()); QAccessible::updateAccessibility(&cursorEvent); #endif } void TerminalDisplay::showResizeNotification() { if (_showTerminalSizeHint && isVisible()) { if (_resizeWidget == nullptr) { _resizeWidget = new QLabel(i18n("Size: XXX x XXX"), this); _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().width(i18n("Size: XXX x XXX"))); _resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height()); _resizeWidget->setAlignment(Qt::AlignCenter); _resizeWidget->setStyleSheet(QStringLiteral("background-color:palette(window);border-style:solid;border-width:1px;border-color:palette(dark)")); _resizeTimer = new QTimer(this); _resizeTimer->setInterval(SIZE_HINT_DURATION); _resizeTimer->setSingleShot(true); connect(_resizeTimer, &QTimer::timeout, _resizeWidget, &QLabel::hide); } QString sizeStr = i18n("Size: %1 x %2", _columns, _lines); _resizeWidget->setText(sizeStr); _resizeWidget->move((width() - _resizeWidget->width()) / 2, (height() - _resizeWidget->height()) / 2 + 20); _resizeWidget->show(); _resizeTimer->start(); } } void TerminalDisplay::paintEvent(QPaintEvent* pe) { QPainter paint(this); // Determine which characters should be repainted (1 region unit = 1 character) QRegion dirtyImageRegion; foreach(const QRect & rect, (pe->region() & contentsRect()).rects()) { dirtyImageRegion += widgetToImage(rect); drawBackground(paint, rect, getBackgroundColor(), true /* use opacity setting */); } paint.setRenderHint(QPainter::Antialiasing, _antialiasText); foreach(const QRect & rect, dirtyImageRegion.rects()) { drawContents(paint, rect); } drawCurrentResultRect(paint); drawInputMethodPreeditString(paint, preeditRect()); paintFilters(paint); const bool drawDimmed = _dimWhenInactive && !hasFocus(); const QColor dimColor(0, 0, 0, 128); foreach(const QRect & rect, (pe->region() & contentsRect()).rects()) { if (drawDimmed) { paint.fillRect(rect, dimColor); } } } void TerminalDisplay::printContent(QPainter& painter, bool friendly) { // Reinitialize the font with the printers paint device so the font // measurement calculations will be done correctly QFont savedFont = getVTFont(); QFont font(savedFont, painter.device()); painter.setFont(font); setVTFont(font); QRect rect(0, 0, _usedColumns, _usedLines); _printerFriendly = friendly; if (!friendly) { drawBackground(painter, rect, getBackgroundColor(), true /* use opacity setting */); } drawContents(painter, rect); _printerFriendly = false; setVTFont(savedFont); } QPoint TerminalDisplay::cursorPosition() const { if (!_screenWindow.isNull()) { return _screenWindow->cursorPosition(); } else { return {0, 0}; } } inline bool TerminalDisplay::isCursorOnDisplay() const { return cursorPosition().x() < _columns && cursorPosition().y() < _lines; } FilterChain* TerminalDisplay::filterChain() const { return _filterChain; } void TerminalDisplay::paintFilters(QPainter& painter) { if (_filterUpdateRequired) { return; } // get color of character under mouse and use it to draw // lines for filters QPoint cursorPos = mapFromGlobal(QCursor::pos()); int cursorLine; int cursorColumn; getCharacterPosition(cursorPos, cursorLine, cursorColumn, false); Character cursorCharacter = _image[loc(qMin(cursorColumn, _columns - 1), cursorLine)]; painter.setPen(QPen(cursorCharacter.foregroundColor.color(_colorTable))); // iterate over hotspots identified by the display's currently active filters // and draw appropriate visuals to indicate the presence of the hotspot QList spots = _filterChain->hotSpots(); int urlNumber, urlNumInc; if (_reverseUrlHints) { urlNumber = spots.size() + 1; urlNumInc = -1; } else { urlNumber = 0; urlNumInc = 1; } foreach(Filter::HotSpot* spot, spots) { urlNumber += urlNumInc; QRegion region; if (spot->type() == Filter::HotSpot::Link) { QRect r; if (spot->startLine() == spot->endLine()) { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } else { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) { r.setCoords(0 * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } r.setCoords(0 * _fontWidth + _contentRect.left(), spot->endLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } if (_showUrlHint && urlNumber < 10) { // Position at the beginning of the URL const QVector regionRects = region.rects(); QRect hintRect(regionRects.first()); hintRect.setWidth(r.height()); painter.fillRect(hintRect, QColor(0, 0, 0, 128)); painter.setPen(Qt::white); painter.drawRect(hintRect.adjusted(0, 0, -1, -1)); painter.drawText(hintRect, Qt::AlignCenter, QString::number(urlNumber)); } } for (int line = spot->startLine() ; line <= spot->endLine() ; line++) { int startColumn = 0; int endColumn = _columns - 1; // TODO use number of _columns which are actually // occupied on this line rather than the width of the // display in _columns // Check image size so _image[] is valid (see makeImage) if (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 = _image[loc(x, y)].isLineChar(); 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)]); if(_image[loc(x, y)].character <= 0x7e || rtl) { while (x + len <= rect.right() && _image[loc(x + len, y)].foregroundColor == currentForeground && _image[loc(x + len, y)].backgroundColor == currentBackground && (_image[loc(x + len, y)].rendition & ~RE_EXTENDED_CHAR) == (currentRendition & ~RE_EXTENDED_CHAR) && (_image[qMin(loc(x + len, y) + 1, _imageSize - 1)].character == 0) == doubleWidth && _image[loc(x + len, y)].isLineChar() == lineDraw && (_image[loc(x + len, y)].character <= 0x7e || rtl)) { 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++; } } if ((x + len < _usedColumns) && (_image[loc(x + len, y)].character == 0u)) { len++; // Adjust for trailing part of multi-column character } const bool save__fixedFont = _fixedFont; if (lineDraw) { _fixedFont = false; } if (doubleWidth) { _fixedFont = false; } univec.resize(p); // Create a text scaling matrix for double width and double height lines. QMatrix textScale; if (y < _lineProperties.size()) { if ((_lineProperties[y] & LINE_DOUBLEWIDTH) != 0) { textScale.scale(2, 1); } if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) { textScale.scale(1, 2); } } //Apply text scaling matrix. paint.setWorldMatrix(textScale, true); //calculate the area in which the text will be drawn QRect textArea = QRect(_contentRect.left() + contentsRect().left() + _fontWidth * x, _contentRect.top() + contentsRect().top() + _fontHeight * y, _fontWidth * len, _fontHeight); //move the calculated area to take account of scaling applied to the painter. //the position of the area from the origin (0,0) is scaled //by the opposite of whatever //transformation has been applied to the painter. this ensures that //painting does actually start from textArea.topLeft() //(instead of textArea.topLeft() * painter-scale) textArea.moveTopLeft(textScale.inverted().map(textArea.topLeft())); QString unistr = QString::fromUcs4(univec.data(), univec.length()); //paint text fragment if (_printerFriendly) { drawPrinterFriendlyTextFragment(paint, textArea, unistr, &_image[loc(x, y)]); } else { drawTextFragment(paint, textArea, unistr, &_image[loc(x, y)]); } _fixedFont = save__fixedFont; //reset back to single-width, single-height _lines paint.setWorldMatrix(textScale.inverted(), true); if (y < _lineProperties.size() - 1) { //double-height _lines are represented by two adjacent _lines //containing the same characters //both _lines will have the LINE_DOUBLEHEIGHT attribute. //If the current line has the LINE_DOUBLEHEIGHT attribute, //we can therefore skip the next line if ((_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) { y++; } } x += len - 1; } } } void TerminalDisplay::drawCurrentResultRect(QPainter& painter) { if(_screenWindow->currentResultLine() == -1) { return; } _searchResultRect.setRect(0, _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _fontHeight, _columns * _fontWidth, _fontHeight); painter.fillRect(_searchResultRect, QColor(0, 0, 255, 80)); } QRect TerminalDisplay::imageToWidget(const QRect& imageArea) const { QRect result; result.setLeft(_contentRect.left() + _fontWidth * imageArea.left()); result.setTop(_contentRect.top() + _fontHeight * imageArea.top()); result.setWidth(_fontWidth * imageArea.width()); result.setHeight(_fontHeight * imageArea.height()); return result; } QRect TerminalDisplay::widgetToImage(const QRect &widgetArea) const { QRect result; result.setLeft( qMin(_usedColumns - 1, qMax(0, (widgetArea.left() - contentsRect().left() - _contentRect.left()) / _fontWidth ))); result.setTop( qMin(_usedLines - 1, qMax(0, (widgetArea.top() - contentsRect().top() - _contentRect.top() ) / _fontHeight))); result.setRight( qMin(_usedColumns - 1, qMax(0, (widgetArea.right() - contentsRect().left() - _contentRect.left()) / _fontWidth ))); result.setBottom(qMin(_usedLines - 1, qMax(0, (widgetArea.bottom() - contentsRect().top() - _contentRect.top() ) / _fontHeight))); return result; } /* ------------------------------------------------------------------------- */ /* */ /* Blinking Text & Cursor */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setBlinkingCursorEnabled(bool blink) { _allowBlinkingCursor = blink; if (blink && !_blinkCursorTimer->isActive()) { _blinkCursorTimer->start(); } if (!blink && _blinkCursorTimer->isActive()) { _blinkCursorTimer->stop(); if (_cursorBlinking) { // if cursor is blinking(hidden), blink it again to make it show _cursorBlinking = false; updateCursor(); } Q_ASSERT(!_cursorBlinking); } } void TerminalDisplay::setBlinkingTextEnabled(bool blink) { _allowBlinkingText = blink; if (blink && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!blink && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } } void TerminalDisplay::focusOutEvent(QFocusEvent*) { // trigger a repaint of the cursor so that it is both: // // * visible (in case it was hidden during blinking) // * drawn in a focused out state _cursorBlinking = false; updateCursor(); // suppress further cursor blinking _blinkCursorTimer->stop(); Q_ASSERT(!_cursorBlinking); // if text is blinking (hidden), blink it again to make it shown if (_textBlinking) { blinkTextEvent(); } // suppress further text blinking _blinkTextTimer->stop(); Q_ASSERT(!_textBlinking); _showUrlHint = false; emit focusLost(); } void TerminalDisplay::focusInEvent(QFocusEvent*) { if (_allowBlinkingCursor) { _blinkCursorTimer->start(); } updateCursor(); if (_allowBlinkingText && _hasTextBlinker) { _blinkTextTimer->start(); } emit focusGained(); } void TerminalDisplay::blinkTextEvent() { Q_ASSERT(_allowBlinkingText); _textBlinking = !_textBlinking; // TODO: Optimize to only repaint the areas of the widget where there is // blinking text rather than repainting the whole widget. update(); } void TerminalDisplay::blinkCursorEvent() { Q_ASSERT(_allowBlinkingCursor); _cursorBlinking = !_cursorBlinking; updateCursor(); } void TerminalDisplay::updateCursor() { if (!isCursorOnDisplay()){ return; } const int cursorLocation = loc(cursorPosition().x(), cursorPosition().y()); Q_ASSERT(cursorLocation < _imageSize); int charWidth = _image[cursorLocation].width(); QRect cursorRect = imageToWidget(QRect(cursorPosition(), QSize(charWidth, 1))); update(cursorRect); } /* ------------------------------------------------------------------------- */ /* */ /* Geometry & Resizing */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::resizeEvent(QResizeEvent *event) { const auto width = event->size().width() - _scrollBar->geometry().width(); _searchBar->correctPosition(QSize(width, event->size().height())); if (contentsRect().isValid()) { updateImageSize(); } } void TerminalDisplay::propagateSize() { if (_image != nullptr) { updateImageSize(); } } void TerminalDisplay::updateImageSize() { Character* oldImage = _image; const int oldLines = _lines; const int oldColumns = _columns; makeImage(); if (oldImage != nullptr) { // copy the old image to reduce flicker int lines = qMin(oldLines, _lines); int columns = qMin(oldColumns, _columns); for (int line = 0; line < lines; line++) { memcpy((void*)&_image[_columns * line], (void*)&oldImage[oldColumns * line], columns * sizeof(Character)); } delete[] oldImage; } if (!_screenWindow.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() { _scrollBar->resize(_scrollBar->sizeHint().width(), contentsRect().height()); _contentRect = contentsRect().adjusted(_margin, _margin, -_margin, -_margin); switch (_scrollbarLocation) { case Enum::ScrollBarHidden : break; case Enum::ScrollBarLeft : _contentRect.setLeft(_contentRect.left() + _scrollBar->width()); _scrollBar->move(contentsRect().topLeft()); break; case Enum::ScrollBarRight: _contentRect.setRight(_contentRect.right() - _scrollBar->width()); _scrollBar->move(contentsRect().topRight() - QPoint(_scrollBar->width() - 1, 0)); break; } // ensure that display is always at least one column wide _columns = qMax(1, _contentRect.width() / _fontWidth); _usedColumns = qMin(_usedColumns, _columns); // ensure that display is always at least one line high _lines = qMax(1, _contentRect.height() / _fontHeight); _usedLines = qMin(_usedLines, _lines); if(_centerContents) { QSize unusedPixels = _contentRect.size() - QSize(_columns * _fontWidth, _lines * _fontHeight); _contentRect.adjust(unusedPixels.width() / 2, unusedPixels.height() / 2, 0, 0); } } // calculate the needed size, this must be synced with calcGeometry() void TerminalDisplay::setSize(int columns, int lines) { const int scrollBarWidth = _scrollBar->isHidden() ? 0 : _scrollBar->sizeHint().width(); const int horizontalMargin = _margin * 2; const int verticalMargin = _margin * 2; QSize newSize = QSize(horizontalMargin + scrollBarWidth + (columns * _fontWidth) , verticalMargin + (lines * _fontHeight)); if (newSize != size()) { _size = newSize; updateGeometry(); } } QSize TerminalDisplay::sizeHint() const { return _size; } //showEvent and hideEvent are reimplemented here so that it appears to other classes that the //display has been resized when the display is hidden or shown. // //TODO: Perhaps it would be better to have separate signals for show and hide instead of using //the same signal as the one for a content size change void TerminalDisplay::showEvent(QShowEvent*) { 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; } if (position == Enum::ScrollBarHidden) { _scrollBar->hide(); } else { _scrollBar->show(); } _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, !_wordSelectionMode && !_lineSelectionMode); QPoint here = QPoint(charColumn, charLine); here.rx() = qBound(0, here.x(), _columns - 1); here.ry() = qBound(0, here.y(), _lines - 1); QPoint ohere; QPoint _iPntSelCorr = _iPntSel; _iPntSelCorr.ry() -= _scrollBar->value(); _iPntSelCorr.rx() = qBound(0, _iPntSelCorr.x(), _columns - 1); _iPntSelCorr.ry() = qBound(0, _iPntSelCorr.y(), _lines - 1); QPoint _pntSelCorr = _pntSel; _pntSelCorr.ry() -= _scrollBar->value(); _pntSelCorr.rx() = qBound(0, _pntSelCorr.x(), _columns - 1); _pntSelCorr.ry() = qBound(0, _pntSelCorr.y(), _lines - 1); 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; left = findWordStart(left); right = findWordEnd(right); // Pick which is start (ohere) and which is extension (here) if (left_not_right) { here = left; ohere = right; } else { here = right; ohere = left; } ohere.rx()++; } if (_lineSelectionMode) { // Extend to complete line const bool above_not_below = (here.y() < _iPntSelCorr.y()); if (above_not_below) { ohere = findLineEnd(_iPntSelCorr); here = findLineStart(here); } else { ohere = findLineStart(_iPntSelCorr); here = findLineEnd(here); } swapping = !(_tripleSelBegin == ohere); _tripleSelBegin = ohere; ohere.rx()++; } int offset = 0; if (!_wordSelectionMode && !_lineSelectionMode) { QChar selClass; const bool left_not_right = (here.y() < _iPntSelCorr.y() || (here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x())); const bool old_left_not_right = (_pntSelCorr.y() < _iPntSelCorr.y() || (_pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x())); swapping = left_not_right != old_left_not_right; // Find left (left_not_right ? from here : from start) const QPoint left = left_not_right ? here : _iPntSelCorr; // Find left (left_not_right ? from start : from here) QPoint right = left_not_right ? _iPntSelCorr : here; if (right.x() > 0 && !_columnSelectionMode) { if (right.x() - 1 < _columns && right.y() < _lines) { selClass = charClass(_image[loc(right.x() - 1, right.y())]); } } // Pick which is start (ohere) and which is extension (here) if (left_not_right) { here = left; ohere = right; offset = 0; } else { here = right; ohere = left; offset = -1; } } if ((here == _pntSelCorr) && (scroll == _scrollBar->value())) { return; // not moved } if (here == ohere) { return; // It's not left, it's not right. } if (_actSel < 2 || swapping) { if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) { _screenWindow->setSelectionStart(ohere.x() , ohere.y() , true); } else { _screenWindow->setSelectionStart(ohere.x() - 1 - offset , ohere.y() , false); } } _actSel = 2; // within selection _pntSel = here; _pntSel.ry() += _scrollBar->value(); if (_columnSelectionMode && !_lineSelectionMode && !_wordSelectionMode) { _screenWindow->setSelectionEnd(here.x() , here.y()); } else { _screenWindow->setSelectionEnd(here.x() + offset , here.y()); } } void TerminalDisplay::mouseReleaseEvent(QMouseEvent* ev) { if (_screenWindow.isNull()) { return; } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); if (ev->button() == Qt::LeftButton) { if (_dragInfo.state == diPending) { // We had a drag event pending but never confirmed. Kill selection _screenWindow->clearSelection(); } else { if (_actSel > 1) { copyToX11Selection(); } _actSel = 0; //FIXME: emits a release event even if the mouse is // outside the range. The procedure used in `mouseMoveEvent' // applies here, too. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2); } } _dragInfo.state = diNone; } if (_usesMouseTracking && (ev->button() == Qt::RightButton || ev->button() == Qt::MidButton) && !(ev->modifiers() & Qt::ShiftModifier)) { emit mouseSignal(ev->button() == Qt::MidButton ? 1 : 2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 2); } } void TerminalDisplay::getCharacterPosition(const QPoint& widgetPoint, int& line, int& column, bool edge) const { // the column value returned can be equal to _usedColumns (when edge == true), // which is the position just after the last character displayed in a line. // // this is required so that the user can select characters in the right-most // column (or left-most for right-to-left input) const int columnMax = edge ? _usedColumns : _usedColumns - 1; const int xOffset = edge ? _fontWidth / 2 : 0; column = qBound(0, (widgetPoint.x() + xOffset - contentsRect().left() - _contentRect.left()) / _fontWidth, columnMax); line = qBound(0, (widgetPoint.y() - contentsRect().top() - _contentRect.top()) / _fontHeight, _usedLines - 1); } void TerminalDisplay::updateLineProperties() { if (_screenWindow.isNull()) { return; } _lineProperties = _screenWindow->getLineProperties(); } void TerminalDisplay::processMidButtonClick(QMouseEvent* ev) { if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) { const bool appendEnter = (ev->modifiers() & Qt::ControlModifier) != 0u; if (_middleClickPasteMode == Enum::PasteFromX11Selection) { pasteFromX11Selection(appendEnter); } else if (_middleClickPasteMode == Enum::PasteFromClipboard) { pasteFromClipboard(appendEnter); } else { Q_ASSERT(false); } } else { if(!_readOnly) { int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); emit mouseSignal(1, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } void TerminalDisplay::mouseDoubleClickEvent(QMouseEvent* ev) { // Yes, successive middle click can trigger this event if (ev->button() == Qt::MidButton) { processMidButtonClick(ev); return; } if (ev->button() != Qt::LeftButton) { return; } if (_screenWindow.isNull()) { return; } int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); QPoint pos(qMin(charColumn, _columns - 1), qMin(charLine, _lines - 1)); // pass on double click as two clicks. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { if(!_readOnly) { // Send just _ONE_ click event, since the first click of the double click // was already sent by the click handler emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 0); // left button } return; } _screenWindow->clearSelection(); _iPntSel = pos; _iPntSel.ry() += _scrollBar->value(); _wordSelectionMode = true; _actSel = 2; // within selection // find word boundaries... { // find the start of the word const QPoint bgnSel = findWordStart(pos); const QPoint endSel = findWordEnd(pos); _actSel = 2; // within selection _screenWindow->setSelectionStart(bgnSel.x() , bgnSel.y() , false); _screenWindow->setSelectionEnd(endSel.x() , endSel.y()); copyToX11Selection(); } _possibleTripleClick = true; QTimer::singleShot(QApplication::doubleClickInterval(), this, [this]() { _possibleTripleClick = false; }); } void TerminalDisplay::wheelEvent(QWheelEvent* ev) { // Only vertical scrolling is supported if (ev->orientation() != Qt::Vertical) { return; } const int modifiers = ev->modifiers(); // ctrl+ for zooming, like in konqueror and firefox if (((modifiers & Qt::ControlModifier) != 0u) && mouseWheelZoom()) { _scrollWheelState.addWheelEvent(ev); int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE); for (;steps > 0; --steps) { // wheel-up for increasing font size increaseFontSize(); } for (;steps < 0; ++steps) { // wheel-down for decreasing font size decreaseFontSize(); } } else if (!_usesMouseTracking && (_scrollBar->maximum() > 0)) { // If the program running in the terminal is not interested in Mouse // Tracking events, send the event to the scrollbar if the slider // has room to move _scrollWheelState.addWheelEvent(ev); _scrollBar->event(ev); Q_ASSERT(_sessionController != nullptr); _sessionController->setSearchStartToWindowCurrentLine(); _scrollWheelState.clearAll(); } else if (!_readOnly) { _scrollWheelState.addWheelEvent(ev); Q_ASSERT(!_sessionController->session().isNull()); if(!_usesMouseTracking && !_sessionController->session()->isPrimaryScreen() && _alternateScrolling) { // Send simulated up / down key presses to the terminal program // for the benefit of programs such as 'less' (which use the alternate screen) // assume that each Up / Down key event will cause the terminal application // to scroll by one line. // // to get a reasonable scrolling speed, scroll by one line for every 5 degrees // of mouse wheel rotation. Mouse wheels typically move in steps of 15 degrees, // giving a scroll of 3 lines const int lines = _scrollWheelState.consumeSteps(static_cast(_fontHeight * qApp->devicePixelRatio()), ScrollState::degreesToAngle(5)); const int keyCode = lines > 0 ? Qt::Key_Up : Qt::Key_Down; QKeyEvent keyEvent(QEvent::KeyPress, keyCode, Qt::NoModifier); for (int i = 0; i < abs(lines); i++) { _screenWindow->screen()->setCurrentTerminalDisplay(this); emit keyPressedSignal(&keyEvent); } } else if (_usesMouseTracking) { // terminal program wants notification of mouse activity int charLine; int charColumn; getCharacterPosition(ev->pos() , charLine , charColumn, !_usesMouseTracking); const int steps = _scrollWheelState.consumeLegacySteps(ScrollState::DEFAULT_ANGLE_SCROLL_LINE); const int button = (steps > 0) ? 4 : 5; for (int i = 0; i < abs(steps); ++i) { emit mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } } void TerminalDisplay::viewScrolledByUser() { Q_ASSERT(_sessionController != nullptr); _sessionController->setSearchStartToWindowCurrentLine(); } /* Moving left/up from the line containing pnt, return the starting offset point which the given line is continuously wrapped (top left corner = 0,0; previous line not visible = 0,-1). */ QPoint TerminalDisplay::findLineStart(const QPoint &pnt) { const int visibleScreenLines = _lineProperties.size(); const int topVisibleLine = _screenWindow->currentLine(); Screen *screen = _screenWindow->screen(); int line = pnt.y(); int lineInHistory= line + topVisibleLine; QVector lineProperties = _lineProperties; while (lineInHistory > 0) { for (; line > 0; line--, lineInHistory--) { // Does previous line wrap around? if ((lineProperties[line - 1] & LINE_WRAPPED) == 0) { return {0, lineInHistory - topVisibleLine}; } } if (lineInHistory < 1) { break; } // _lineProperties is only for the visible screen, so grab new data int newRegionStart = qMax(0, lineInHistory - visibleScreenLines); lineProperties = screen->getLineProperties(newRegionStart, lineInHistory - 1); line = lineInHistory - newRegionStart; } return {0, lineInHistory - topVisibleLine}; } /* Moving right/down from the line containing pnt, return the ending offset point which the given line is continuously wrapped. */ QPoint TerminalDisplay::findLineEnd(const QPoint &pnt) { const int visibleScreenLines = _lineProperties.size(); const int topVisibleLine = _screenWindow->currentLine(); const int maxY = _screenWindow->lineCount() - 1; Screen *screen = _screenWindow->screen(); int line = pnt.y(); int lineInHistory= line + topVisibleLine; QVector lineProperties = _lineProperties; while (lineInHistory < maxY) { for (; line < lineProperties.count() && lineInHistory < maxY; line++, lineInHistory++) { // Does current line wrap around? if ((lineProperties[line] & LINE_WRAPPED) == 0) { return {_columns - 1, lineInHistory - topVisibleLine}; } } line = 0; lineProperties = screen->getLineProperties(lineInHistory, qMin(lineInHistory + visibleScreenLines, maxY)); } return {_columns - 1, lineInHistory - topVisibleLine}; } QPoint TerminalDisplay::findWordStart(const QPoint &pnt) { const int regSize = qMax(_screenWindow->windowLines(), 10); const int firstVisibleLine = _screenWindow->currentLine(); Screen *screen = _screenWindow->screen(); Character *image = _image; Character *tmp_image = nullptr; int imgLine = pnt.y(); int x = pnt.x(); int y = imgLine + firstVisibleLine; int imgLoc = loc(x, imgLine); QVector lineProperties = _lineProperties; const QChar selClass = charClass(image[imgLoc]); const int imageSize = regSize * _columns; while (true) { for (;;imgLoc--, x--) { if (imgLoc < 1) { // no more chars in this region break; } if (x > 0) { // has previous char on this line if (charClass(image[imgLoc - 1]) == selClass) { continue; } goto out; } else if (imgLine > 0) { // not the first line in the session if ((lineProperties[imgLine - 1] & LINE_WRAPPED) != 0) { // have continuation on prev line if (charClass(image[imgLoc - 1]) == selClass) { x = _columns; imgLine--; y--; continue; } } goto out; } else if (y > 0) { // want more data, but need to fetch new region break; } else { goto out; } } if (y <= 0) { // No more data goto out; } int newRegStart = qMax(0, y - regSize + 1); lineProperties = screen->getLineProperties(newRegStart, y - 1); imgLine = y - newRegStart; delete[] tmp_image; tmp_image = new Character[imageSize]; image = tmp_image; screen->getImage(tmp_image, imageSize, newRegStart, y - 1); imgLoc = loc(x, imgLine); if (imgLoc < 1) { // Reached the start of the session break; } } out: delete[] tmp_image; return {x, y - firstVisibleLine}; } QPoint TerminalDisplay::findWordEnd(const QPoint &pnt) { const int regSize = qMax(_screenWindow->windowLines(), 10); const int curLine = _screenWindow->currentLine(); int i = pnt.y(); int x = pnt.x(); int y = i + curLine; int j = loc(x, i); QVector lineProperties = _lineProperties; Screen *screen = _screenWindow->screen(); Character *image = _image; Character *tmp_image = nullptr; const QChar selClass = charClass(image[j]); const int imageSize = regSize * _columns; const int maxY = _screenWindow->lineCount() - 1; const int maxX = _columns - 1; while (true) { const int lineCount = lineProperties.count(); for (;;j++, x++) { if (x < maxX) { if (charClass(image[j + 1]) == selClass && // A colon right before whitespace is never part of a word ! (image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) { continue; } goto out; } else if (i < lineCount - 1) { if (((lineProperties[i] & LINE_WRAPPED) != 0) && charClass(image[j + 1]) == selClass && // A colon right before whitespace is never part of a word ! (image[j + 1].character == ':' && charClass(image[j + 2]) == QLatin1Char(' '))) { x = -1; i++; y++; continue; } goto out; } else if (y < maxY) { if (i < lineCount && ((lineProperties[i] & LINE_WRAPPED) == 0)) { goto out; } break; } else { goto out; } } int newRegEnd = qMin(y + regSize - 1, maxY); lineProperties = screen->getLineProperties(y, newRegEnd); i = 0; if (tmp_image == nullptr) { tmp_image = new Character[imageSize]; image = tmp_image; } screen->getImage(tmp_image, imageSize, y, newRegEnd); x--; j = loc(x, i); } out: y -= curLine; // In word selection mode don't select @ (64) if at end of word. if (((image[j].rendition & RE_EXTENDED_CHAR) == 0) && (QChar(image[j].character) == QLatin1Char('@')) && (y > pnt.y() || x > pnt.x())) { if (x > 0) { x--; } else { y--; } } delete[] tmp_image; return {x, y}; } Screen::DecodingOptions TerminalDisplay::currentDecodingOptions() { Screen::DecodingOptions decodingOptions; if (_preserveLineBreaks) { decodingOptions |= Screen::PreserveLineBreaks; } if (_trimLeadingSpaces) { decodingOptions |= Screen::TrimLeadingWhitespace; } if (_trimTrailingSpaces) { decodingOptions |= Screen::TrimTrailingWhitespace; } return decodingOptions; } void TerminalDisplay::mouseTripleClickEvent(QMouseEvent* ev) { if (_screenWindow.isNull()) { return; } int charLine; int charColumn; getCharacterPosition(ev->pos(), charLine, charColumn, true); selectLine(QPoint(charColumn, charLine), _tripleClickMode == Enum::SelectWholeLine); } void TerminalDisplay::selectLine(QPoint pos, bool entireLine) { _iPntSel = pos; _screenWindow->clearSelection(); _lineSelectionMode = true; _wordSelectionMode = false; _actSel = 2; // within selection if (!entireLine) { // Select from cursor to end of line _tripleSelBegin = findWordStart(_iPntSel); _screenWindow->setSelectionStart(_tripleSelBegin.x(), _tripleSelBegin.y() , false); } else { _tripleSelBegin = findLineStart(_iPntSel); _screenWindow->setSelectionStart(0 , _tripleSelBegin.y() , false); } _iPntSel = findLineEnd(_iPntSel); _screenWindow->setSelectionEnd(_iPntSel.x() , _iPntSel.y()); copyToX11Selection(); _iPntSel.ry() += _scrollBar->value(); } void TerminalDisplay::selectCurrentLine() { if (_screenWindow.isNull()) { return; } selectLine(cursorPosition(), true); } void TerminalDisplay::selectAll() { if (_screenWindow.isNull()) { return; } _preserveLineBreaks = true; _screenWindow->setSelectionByLineRange(0, _screenWindow->lineCount()); copyToX11Selection(); } bool TerminalDisplay::focusNextPrevChild(bool next) { // for 'Tab', always disable focus switching among widgets // for 'Shift+Tab', leave the decision to higher level if (next) { return false; } else { return QWidget::focusNextPrevChild(next); } } QChar TerminalDisplay::charClass(const Character& ch) const { if ((ch.rendition & RE_EXTENDED_CHAR) != 0) { ushort extendedCharLength = 0; const uint* chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength); if ((chars != nullptr) && extendedCharLength > 0) { const QString s = QString::fromUcs4(chars, extendedCharLength); if (_wordCharacters.contains(s, Qt::CaseInsensitive)) { return QLatin1Char('a'); } bool letterOrNumber = false; for (int i = 0; !letterOrNumber && i < s.size(); ++i) { letterOrNumber = s.at(i).isLetterOrNumber(); } return letterOrNumber ? QLatin1Char('a') : s.at(0); } return 0; } else { const QChar qch(ch.character); if (qch.isSpace()) { return QLatin1Char(' '); } if (qch.isLetterOrNumber() || _wordCharacters.contains(qch, Qt::CaseInsensitive)) { return QLatin1Char('a'); } return qch; } } void TerminalDisplay::setWordCharacters(const QString& wc) { _wordCharacters = wc; } void TerminalDisplay::setUsesMouseTracking(bool on) { _usesMouseTracking = on; setCursor(_usesMouseTracking ? Qt::ArrowCursor : Qt::IBeamCursor); } bool TerminalDisplay::usesMouseTracking() const { return _usesMouseTracking; } void TerminalDisplay::setAlternateScrolling(bool enable) { _alternateScrolling = enable; } bool TerminalDisplay::alternateScrolling() const { return _alternateScrolling; } void TerminalDisplay::setBracketedPasteMode(bool on) { _bracketedPasteMode = on; } bool TerminalDisplay::bracketedPasteMode() const { return _bracketedPasteMode; } /* ------------------------------------------------------------------------- */ /* */ /* Clipboard */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::doPaste(QString text, bool appendReturn) { if (_screenWindow.isNull()) { return; } if (_readOnly) { return; } if (appendReturn) { text.append(QLatin1String("\r")); } if (text.length() > 8000) { if (KMessageBox::warningContinueCancel(window(), i18np("Are you sure you want to paste %1 character?", "Are you sure you want to paste %1 characters?", text.length()), i18n("Confirm Paste"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("ShowPasteHugeTextWarning")) == KMessageBox::Cancel) { return; } } QStringList unsafeCharacters; for (const QChar &c : text) { if (!c.isPrint() && c != QLatin1Char('\t') && c != QLatin1Char('\n')) { QString description; switch(c.unicode()) { case '\x03': description = i18n("^C Interrupt: May abort the current process"); break; case '\x04': description = i18n("^D End of transmission: May exit the current process"); break; case '\x07': description = i18n("^G Bell: Will try to emit an audible warning"); break; case '\x08': description = i18n("^H Backspace"); break; case '\x13': description = i18n("^S Scroll lock: Locks terminal output"); break; case '\x1a': description = i18n("^Z Suspend: Stops current process"); break; case '\x1b': description = i18n("ESC: Used for special commands to the current process"); break; default: description = ki18n("Other unprintable character (\\x%1)").subs(c.unicode(), 0, 16).toString(); break; } unsafeCharacters.append(description); } } unsafeCharacters.removeDuplicates(); if (!unsafeCharacters.isEmpty()) { int result = KMessageBox::warningYesNoCancelList(window(), i18n("The text you're trying to paste contains hidden unprintable characters, " "do you want to filter them out?"), unsafeCharacters, i18nc("@title", "Filter"), KGuiItem(i18nc("@action:button", "&Remove unprintable"), QStringLiteral("filter-symbolic")), KGuiItem(i18nc("@action:button", "Confirm &paste"), QStringLiteral("edit-paste")), KGuiItem(i18nc("@action:button", "&Cancel paste"), QStringLiteral("dialog-cancel")), QStringLiteral("ShowPasteUnprintableWarning") ); switch(result){ case KMessageBox::Cancel: return; case KMessageBox::Yes: { QString sanitized; for (const QChar &c : text) { if (c.isPrint() || c == QLatin1Char('\t') || c == QLatin1Char('\n')) { sanitized.append(c); } } text = sanitized; } case KMessageBox::No: break; default: break; } } if (!text.isEmpty()) { text.replace(QLatin1Char('\n'), QLatin1Char('\r')); if (bracketedPasteMode()) { text.remove(QLatin1String("\033")); text.prepend(QLatin1String("\033[200~")); text.append(QLatin1String("\033[201~")); } // perform paste by simulating keypress events QKeyEvent e(QEvent::KeyPress, 0, Qt::NoModifier, text); emit keyPressedSignal(&e); } } void TerminalDisplay::setAutoCopySelectedText(bool enabled) { _autoCopySelectedText = enabled; } void TerminalDisplay::setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode) { _middleClickPasteMode = mode; } void TerminalDisplay::setCopyTextAsHTML(bool enabled) { _copyTextAsHTML = enabled; } void TerminalDisplay::copyToX11Selection() { if (_screenWindow.isNull()) { return; } const QString &text = _screenWindow->selectedText(currentDecodingOptions()); if (text.isEmpty()) { return; } auto mimeData = new QMimeData; mimeData->setText(text); if (_copyTextAsHTML) { mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml)); } if (QApplication::clipboard()->supportsSelection()) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection); } if (_autoCopySelectedText) { QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } } void TerminalDisplay::copyToClipboard() { if (_screenWindow.isNull()) { return; } const QString &text = _screenWindow->selectedText(currentDecodingOptions()); if (text.isEmpty()) { return; } auto mimeData = new QMimeData; mimeData->setText(text); if (_copyTextAsHTML) { mimeData->setHtml(_screenWindow->selectedText(currentDecodingOptions() | Screen::ConvertToHtml)); } QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void TerminalDisplay::pasteFromClipboard(bool appendEnter) { QString text = QApplication::clipboard()->text(QClipboard::Clipboard); doPaste(text, appendEnter); } void TerminalDisplay::pasteFromX11Selection(bool appendEnter) { if (QApplication::clipboard()->supportsSelection()) { QString text = QApplication::clipboard()->text(QClipboard::Selection); doPaste(text, appendEnter); } } /* ------------------------------------------------------------------------- */ /* */ /* Input Method */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::inputMethodEvent(QInputMethodEvent* event) { if (!event->commitString().isEmpty()) { QKeyEvent keyEvent(QEvent::KeyPress, 0, Qt::NoModifier, event->commitString()); emit keyPressedSignal(&keyEvent); } if (!_readOnly && isCursorOnDisplay()) { _inputMethodData.preeditString = event->preeditString(); update(preeditRect() | _inputMethodData.previousPreeditRect); } event->accept(); } QVariant TerminalDisplay::inputMethodQuery(Qt::InputMethodQuery query) const { const QPoint cursorPos = cursorPosition(); switch (query) { case Qt::ImMicroFocus: return imageToWidget(QRect(cursorPos.x(), cursorPos.y(), 1, 1)); case Qt::ImFont: return font(); case Qt::ImCursorPosition: // return the cursor position within the current line return cursorPos.x(); case Qt::ImSurroundingText: { // return the text from the current line QString lineText; QTextStream stream(&lineText); PlainTextDecoder decoder; decoder.begin(&stream); if (isCursorOnDisplay()) { decoder.decodeLine(&_image[loc(0, cursorPos.y())], _usedColumns, LINE_DEFAULT); } decoder.end(); return lineText; } case Qt::ImCurrentSelection: return QString(); default: break; } return QVariant(); } QRect TerminalDisplay::preeditRect() const { const int preeditLength = Character::stringWidth(_inputMethodData.preeditString); if (preeditLength == 0) { return {}; } const QRect stringRect(_contentRect.left() + _fontWidth * cursorPosition().x(), _contentRect.top() + _fontHeight * cursorPosition().y(), _fontWidth * preeditLength, _fontHeight); return stringRect.intersected(_contentRect); } void TerminalDisplay::drawInputMethodPreeditString(QPainter& painter , const QRect& rect) { if (_inputMethodData.preeditString.isEmpty() || !isCursorOnDisplay()) { return; } const QPoint cursorPos = cursorPosition(); bool invertColors = false; const QColor background = _colorTable[DEFAULT_BACK_COLOR]; const QColor foreground = _colorTable[DEFAULT_FORE_COLOR]; const Character* style = &_image[loc(cursorPos.x(), cursorPos.y())]; drawBackground(painter, rect, background, true); drawCursor(painter, rect, foreground, background, invertColors); drawCharacters(painter, rect, _inputMethodData.preeditString, style, invertColors); _inputMethodData.previousPreeditRect = rect; } /* ------------------------------------------------------------------------- */ /* */ /* Keyboard */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setFlowControlWarningEnabled(bool enable) { _flowControlWarningEnabled = enable; // if the dialog is currently visible and the flow control warning has // been disabled then hide the dialog if (!enable) { outputSuspended(false); } } void TerminalDisplay::outputSuspended(bool suspended) { //create the label when this function is first called if (_outputSuspendedMessageWidget == nullptr) { //This label includes a link to an English language website //describing the 'flow control' (Xon/Xoff) feature found in almost //all terminal emulators. //If there isn't a suitable article available in the target language the link //can simply be removed. _outputSuspendedMessageWidget = createMessageWidget(i18n("Output has been " "suspended" " by pressing Ctrl+S." " Press Ctrl+Q to resume.")); connect(_outputSuspendedMessageWidget, &KMessageWidget::linkActivated, this, [](const QString &url) { QDesktopServices::openUrl(QUrl(url)); }); _outputSuspendedMessageWidget->setMessageType(KMessageWidget::Warning); } suspended ? _outputSuspendedMessageWidget->animatedShow() : _outputSuspendedMessageWidget->animatedHide(); } void TerminalDisplay::dismissOutputSuspendedMessage() { outputSuspended(false); } KMessageWidget* TerminalDisplay::createMessageWidget(const QString &text) { auto widget = new KMessageWidget(text); widget->setWordWrap(true); widget->setFocusProxy(this); widget->setCursor(Qt::ArrowCursor); _verticalLayout->insertWidget(0, 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) { // 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(); } } 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()) { for (int i = 0 ; i < urls.count() ; i++) { KIO::StatJob* job = KIO::mostLocalUrl(urls[i], KIO::HideProgressInfo); bool ok = job->exec(); if (!ok) { continue; } QUrl url = job->mostLocalUrl(); QString urlText; if (url.isLocalFile()) { urlText = url.path(); } else { urlText = url.url(); } // in future it may be useful to be able to insert file names with drag-and-drop // without quoting them (this only affects paths with spaces in) urlText = KShell::quoteArg(urlText); dropText += urlText; // Each filename(including the last) should be followed by one space. dropText += QLatin1Char(' '); } // If our target is local we will open a popup - otherwise the fallback kicks // in and the URLs will simply be pasted as text. if (!_dropUrlsAsText && (_sessionController != nullptr) && _sessionController->url().isLocalFile()) { // A standard popup with Copy, Move and Link as options - // plus an additional Paste option. QAction* pasteAction = new QAction(i18n("&Paste Location"), this); pasteAction->setData(dropText); connect(pasteAction, &QAction::triggered, this, &TerminalDisplay::dropMenuPasteActionTriggered); QList additionalActions; additionalActions.append(pasteAction); if (urls.count() == 1) { KIO::StatJob* job = KIO::mostLocalUrl(urls[0], KIO::HideProgressInfo); bool ok = job->exec(); if (ok) { const QUrl url = job->mostLocalUrl(); if (url.isLocalFile()) { const QFileInfo fileInfo(url.path()); if (fileInfo.isDir()) { QAction* cdAction = new QAction(i18n("Change &Directory To"), this); dropText = QLatin1String(" cd ") + dropText + QLatin1Char('\n'); cdAction->setData(dropText); connect(cdAction, &QAction::triggered, this, &TerminalDisplay::dropMenuCdActionTriggered); additionalActions.append(cdAction); } } } } QUrl target = QUrl::fromLocalFile(_sessionController->currentDir()); KIO::DropJob* job = KIO::drop(event, target); KJobWidgets::setWindow(job, this); job->setApplicationActions(additionalActions); return; } } else { dropText = mimeData->text(); } if (mimeData->hasFormat(QStringLiteral("text/plain")) || mimeData->hasFormat(QStringLiteral("text/uri-list"))) { emit sendStringToEmu(dropText.toLocal8Bit()); } } void TerminalDisplay::dropMenuPasteActionTriggered() { if (sender() != nullptr) { const auto* action = qobject_cast(sender()); if (action != nullptr) { emit sendStringToEmu(action->data().toString().toLocal8Bit()); } } } void TerminalDisplay::dropMenuCdActionTriggered() { if (sender() != nullptr) { const auto* action = qobject_cast(sender()); if (action != nullptr) { emit sendStringToEmu(action->data().toString().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; } 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; } diff --git a/src/TerminalDisplay.h b/src/TerminalDisplay.h index 6050205a..4f3a9cfc 100644 --- a/src/TerminalDisplay.h +++ b/src/TerminalDisplay.h @@ -1,1096 +1,1096 @@ /* Copyright 2007-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef TERMINALDISPLAY_H #define TERMINALDISPLAY_H // Qt #include #include #include // Konsole #include "Character.h" #include "konsoleprivate_export.h" #include "ScreenWindow.h" #include "ColorScheme.h" #include "Enumeration.h" #include "ScrollState.h" class QDrag; class QDragEnterEvent; class QDropEvent; class QLabel; class QTimer; class QEvent; class QVBoxLayout; class QKeyEvent; class QScrollBar; class QShowEvent; class QHideEvent; class QTimerEvent; class KMessageWidget; namespace Konsole { class FilterChain; class TerminalImageFilterChain; class SessionController; class IncrementalSearchBar; /** * A widget which displays output from a terminal emulation and sends input keypresses and mouse activity * to the terminal. * * When the terminal emulation receives new output from the program running in the terminal, * it will update the display by calling updateImage(). * * TODO More documentation */ class KONSOLEPRIVATE_EXPORT TerminalDisplay : public QWidget { Q_OBJECT public: /** Constructs a new terminal display widget with the specified parent. */ explicit TerminalDisplay(QWidget *parent = nullptr); ~TerminalDisplay() Q_DECL_OVERRIDE; /** Returns the terminal color palette used by the display. */ const ColorEntry *colorTable() const; /** Sets the terminal color palette used by the display. */ void setColorTable(const ColorEntry table[]); /** * Sets the seed used to generate random colors for the display * (in color schemes that support them). */ void setRandomSeed(uint randomSeed); /** * Returns the seed used to generate random colors for the display * (in color schemes that support them). */ uint randomSeed() const; /** Sets the opacity of the terminal display. */ void setOpacity(qreal opacity); /** Sets the background picture */ - void setWallpaper(ColorSchemeWallpaper::Ptr p); + void setWallpaper(const ColorSchemeWallpaper::Ptr &p); /** * Specifies whether the terminal display has a vertical scroll bar, and if so whether it * is shown on the left or right side of the display. */ void setScrollBarPosition(Enum::ScrollBarPositionEnum position); Enum::ScrollBarPositionEnum scrollBarPosition() const { return _scrollbarLocation; } /** * Sets the current position and range of the display's scroll bar. * * @param cursor The position of the scroll bar's thumb. * @param slines The maximum value of the scroll bar. */ void setScroll(int cursor, int slines); void setScrollFullPage(bool fullPage); bool scrollFullPage() const; /** * Returns the display's filter chain. When the image for the display is updated, * the text is passed through each filter in the chain. Each filter can define * hotspots which correspond to certain strings (such as URLs or particular words). * Depending on the type of the hotspots created by the filter ( returned by Filter::Hotspot::type() ) * the view will draw visual cues such as underlines on mouse-over for links or translucent * rectangles for markers. * * To add a new filter to the view, call: * viewWidget->filterChain()->addFilter( filterObject ); */ FilterChain *filterChain() const; /** * Updates the filters in the display's filter chain. This will cause * the hotspots to be updated to match the current image. * * WARNING: This function can be expensive depending on the * image size and number of filters in the filterChain() * * TODO - This API does not really allow efficient usage. Revise it so * that the processing can be done in a better way. * * eg: * - Area of interest may be known ( eg. mouse cursor hovering * over an area ) */ void processFilters(); /** * Returns a list of menu actions created by the filters for the content * at the given @p position. */ QList filterActions(const QPoint &position); /** Specifies whether or not the cursor can blink. */ void setBlinkingCursorEnabled(bool blink); /** Returns true if the cursor is allowed to blink or false otherwise. */ bool blinkingCursorEnabled() const { return _allowBlinkingCursor; } /** Specifies whether or not text can blink. */ void setBlinkingTextEnabled(bool blink); void setControlDrag(bool enable) { _ctrlRequiredForDrag = enable; } bool ctrlRequiredForDrag() const { return _ctrlRequiredForDrag; } void setDropUrlsAsText(bool enable) { _dropUrlsAsText = enable; } bool getDropUrlsAsText() const { return _dropUrlsAsText; } /** Sets how the text is selected when the user triple clicks within the display. */ void setTripleClickMode(Enum::TripleClickModeEnum mode) { _tripleClickMode = mode; } /** See setTripleClickSelectionMode() */ Enum::TripleClickModeEnum tripleClickMode() const { return _tripleClickMode; } /** * Specifies whether links and email addresses should be opened when * clicked with the mouse. Defaults to false. */ void setOpenLinksByDirectClick(bool value) { _openLinksByDirectClick = value; } /** * Returns true if links and email addresses should be opened when * clicked with the mouse. */ bool getOpenLinksByDirectClick() const { return _openLinksByDirectClick; } /** * Sets whether leading spaces should be trimmed in selected text. */ void setTrimLeadingSpaces(bool enabled) { _trimLeadingSpaces = enabled; } /** * Returns true if leading spaces should be trimmed in selected text. */ bool trimLeadingSpaces() const { return _trimLeadingSpaces; } /** * Sets whether trailing spaces should be trimmed in selected text. */ void setTrimTrailingSpaces(bool enabled) { _trimTrailingSpaces = enabled; } /** * Returns true if trailing spaces should be trimmed in selected text. */ bool trimTrailingSpaces() const { return _trimTrailingSpaces; } void setLineSpacing(uint); uint lineSpacing() const; void setSessionController(SessionController *controller); SessionController *sessionController(); /** * Sets the shape of the keyboard cursor. This is the cursor drawn * at the position in the terminal where keyboard input will appear. * * In addition the terminal display widget also has a cursor for * the mouse pointer, which can be set using the QWidget::setCursor() * method. * * Defaults to BlockCursor */ void setKeyboardCursorShape(Enum::CursorShapeEnum shape); /** * Returns the shape of the keyboard cursor. See setKeyboardCursorShape() */ Enum::CursorShapeEnum keyboardCursorShape() const; /** * Sets the Cursor Style (DECSCUSR) via escape sequences * @p shape cursor shape * @p isBlinking if true, the cursor will be set to blink */ void setCursorStyle(Enum::CursorShapeEnum shape, bool isBlinking); /** * Resets the cursor style to the current profile cursor shape and * blinking settings */ void resetCursorStyle(); /** * Sets the color used to draw the keyboard cursor. * * The keyboard cursor defaults to using the foreground color of the character * underneath it. * * @param color By default, the widget uses the color of the * character under the cursor to draw the cursor, and inverts the * color of that character to make sure it is still readable. If @p * color is a valid QColor, the widget uses that color to draw the * cursor. If @p color is not an valid QColor, the widget falls back * to the default behavior. */ void setKeyboardCursorColor(const QColor &color); /** * Returns the color of the keyboard cursor, or an invalid color if the keyboard * cursor color is set to change according to the foreground color of the character * underneath it. */ QColor keyboardCursorColor() const; /** * Returns the number of lines of text which can be displayed in the widget. * * This will depend upon the height of the widget and the current font. * See fontHeight() */ int lines() const { return _lines; } /** * Returns the number of characters of text which can be displayed on * each line in the widget. * * This will depend upon the width of the widget and the current font. * See fontWidth() */ int columns() const { return _columns; } /** * Returns the height of the characters in the font used to draw the text in the display. */ int fontHeight() const { return _fontHeight; } /** * Returns the width of the characters in the display. * This assumes the use of a fixed-width font. */ int fontWidth() const { return _fontWidth; } void setSize(int columns, int lines); // reimplemented QSize sizeHint() const Q_DECL_OVERRIDE; /** * Sets which characters, in addition to letters and numbers, * are regarded as being part of a word for the purposes * of selecting words in the display by double clicking on them. * * The word boundaries occur at the first and last characters which * are either a letter, number, or a character in @p wc * * @param wc An array of characters which are to be considered parts * of a word ( in addition to letters and numbers ). */ void setWordCharacters(const QString &wc); /** * Returns the characters which are considered part of a word for the * purpose of selecting words in the display with the mouse. * * @see setWordCharacters() */ QString wordCharacters() const { return _wordCharacters; } /** * Sets the type of effect used to alert the user when a 'bell' occurs in the * terminal session. * * The terminal session can trigger the bell effect by calling bell() with * the alert message. */ void setBellMode(int mode); /** * Returns the type of effect used to alert the user when a 'bell' occurs in * the terminal session. * * See setBellMode() */ int bellMode() const; /** Play a visual bell for prompt or warning. */ void visualBell(); /** * Specified whether zoom terminal on Ctrl+mousewheel is enabled or not. * Defaults to enabled. */ void setMouseWheelZoom(bool value) { _mouseWheelZoom = value; } /** * Returns the whether zoom terminal on Ctrl+mousewheel is enabled. * * See setMouseWheelZoom() */ bool mouseWheelZoom() { return _mouseWheelZoom; } /** Returns the font used to draw characters in the display */ QFont getVTFont() { return font(); } /** * Sets the font used to draw the display. Has no effect if @p font * is larger than the size of the display itself. */ void setVTFont(const QFont &f); /** Increases the font size */ void increaseFontSize(); /** Decreases the font size */ void decreaseFontSize(); /** Reset the font size */ void resetFontSize(); /** * Specified whether anti-aliasing of text in the terminal display * is enabled or not. Defaults to enabled. */ void setAntialias(bool value) { _antialiasText = value; } /** * Returns true if anti-aliasing of text in the terminal is enabled. */ bool antialias() const { return _antialiasText; } /** * Specifies whether characters with intense colors should be rendered * as bold. Defaults to true. */ void setBoldIntense(bool value) { _boldIntense = value; } /** * Returns true if characters with intense colors are rendered in bold. */ bool getBoldIntense() const { return _boldIntense; } /** * Specifies whether line characters will be displayed using font instead * of builtin code. * as bold. Defaults to false. */ void setUseFontLineCharacters(bool value) { _useFontLineCharacters = value; } /** * Returns true if font line characters will be used. */ bool getFontLineCharacters() const { return _useFontLineCharacters; } /** * Sets whether or not the current height and width of the * terminal in lines and columns is displayed whilst the widget * is being resized. */ void setShowTerminalSizeHint(bool on) { _showTerminalSizeHint = on; } /** * Returns whether or not the current height and width of * the terminal in lines and columns is displayed whilst the widget * is being resized. */ bool showTerminalSizeHint() const { return _showTerminalSizeHint; } /** * Sets the status of the BiDi rendering inside the terminal display. * Defaults to disabled. */ void setBidiEnabled(bool set) { _bidiEnabled = set; } /** * Returns the status of the BiDi rendering in this widget. */ bool isBidiEnabled() const { return _bidiEnabled; } /** * Sets the modifiers that shows URL hints when they are pressed * Defaults to disabled. */ void setUrlHintsModifiers(int modifiers) { _urlHintsModifiers = Qt::KeyboardModifiers(modifiers); } void setReverseUrlHintsEnabled(bool set) { _reverseUrlHints = set; } bool isReverseUrlHintsEnabled() const { return _reverseUrlHints; } /** * Sets the terminal screen section which is displayed in this widget. * When updateImage() is called, the display fetches the latest character image from the * the associated terminal screen window. * * In terms of the model-view paradigm, the ScreenWindow is the model which is rendered * by the TerminalDisplay. */ void setScreenWindow(ScreenWindow *window); /** Returns the terminal screen section which is displayed in this widget. See setScreenWindow() */ ScreenWindow *screenWindow() const; // Select the current line. void selectCurrentLine(); /** * Selects everything in the terminal */ void selectAll(); void printContent(QPainter &painter, bool friendly); /** * Gets the background of the display * @see setBackgroundColor(), setColorTable(), setForegroundColor() */ QColor getBackgroundColor() const; bool bracketedPasteMode() const; /** * Returns true if the flow control warning box is enabled. * See outputSuspended() and setFlowControlWarningEnabled() */ bool flowControlWarningEnabled() const { return _flowControlWarningEnabled; } /** See setUsesMouseTracking() */ bool usesMouseTracking() const; /** See setAlternateScrolling() */ bool alternateScrolling() const; public Q_SLOTS: /** * Scrolls current ScreenWindow * * it's needed for proper handling scroll commands in the Vt102Emulation class */ void scrollScreenWindow(enum ScreenWindow::RelativeScrollMode mode, int amount); /** * Causes the terminal display to fetch the latest character image from the associated * terminal screen ( see setScreenWindow() ) and redraw the display. */ void updateImage(); /** * Causes the terminal display to fetch the latest line status flags from the * associated terminal screen ( see setScreenWindow() ). */ void updateLineProperties(); void setAutoCopySelectedText(bool enabled); void setCopyTextAsHTML(bool enabled); void setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum mode); /** Copies the selected text to the X11 Selection. */ void copyToX11Selection(); /** Copies the selected text to the system clipboard. */ void copyToClipboard(); /** * Pastes the content of the clipboard into the * display. */ void pasteFromClipboard(bool appendEnter = false); /** * Pastes the content of the X11 selection into the * display. */ void pasteFromX11Selection(bool appendEnter = false); /** * Changes whether the flow control warning box should be shown when the flow control * stop key (Ctrl+S) are pressed. */ void setFlowControlWarningEnabled(bool enable); /** * Causes the widget to display or hide a message informing the user that terminal * output has been suspended (by using the flow control key combination Ctrl+S) * * @param suspended True if terminal output has been suspended and the warning message should * be shown or false to indicate that terminal output has been resumed and that * the warning message should disappear. */ void outputSuspended(bool suspended); /** * Sets whether the program currently running in the terminal is interested * in Mouse Tracking events. * * When set to true, Konsole will send Mouse Tracking events. * * The user interaction needed to create text selections will change * also, and the user will be required to hold down the Shift key to * create a selection or perform other mouse activities inside the view * area, since the program running in the terminal is being allowed * to handle normal mouse events itself. * * @param on Set to true if the program running in the terminal is * interested in Mouse Tracking events or false otherwise. */ void setUsesMouseTracking(bool on); /** * Sets the AlternateScrolling profile property which controls whether * to emulate up/down key presses for mouse scroll wheel events. * For more details, check the documentation of that property in the * Profile header. * Enabled by default. */ void setAlternateScrolling(bool enable); void setBracketedPasteMode(bool on); /** * Shows a notification that a bell event has occurred in the terminal. * TODO: More documentation here */ void bell(const QString &message); /** * Sets the background of the display to the specified color. * @see setColorTable(), getBackgroundColor(), setForegroundColor() */ void setBackgroundColor(const QColor &color); /** * Sets the text of the display to the specified color. * @see setColorTable(), setBackgroundColor(), getBackgroundColor() */ void setForegroundColor(const QColor &color); /** * Sets the display's contents margins. */ void setMargin(int margin); /** * Sets whether the contents are centered between the margins. */ void setCenterContents(bool enable); /** * Sets whether the background should change when the window loses focus */ void setDimWhenInactive(bool shouldDim) { _dimWhenInactive = shouldDim; } // Used to show/hide the message widget void updateReadOnlyState(bool readonly); IncrementalSearchBar *searchBar() const; Q_SIGNALS: /** * Emitted when the user presses a key whilst the terminal widget has focus. */ void keyPressedSignal(QKeyEvent *event); /** * A mouse event occurred. * @param button The mouse button (0 for left button, 1 for middle button, 2 for right button, 3 for release) * @param column The character column where the event occurred * @param line The character row where the event occurred * @param eventType The type of event. 0 for a mouse press / release or 1 for mouse motion */ void mouseSignal(int button, int column, int line, int eventType); void changedFontMetricSignal(int height, int width); void changedContentSizeSignal(int height, int width); /** * Emitted when the user right clicks on the display, or right-clicks * with the Shift key held down if usesMouseTracking() is true. * * This can be used to display a context menu. */ void configureRequest(const QPoint &position); /** * When a shortcut which is also a valid terminal key sequence is pressed while * the terminal widget has focus, this signal is emitted to allow the host to decide * whether the shortcut should be overridden. * When the shortcut is overridden, the key sequence will be sent to the terminal emulation instead * and the action associated with the shortcut will not be triggered. * * @p override is set to false by default and the shortcut will be triggered as normal. */ void overrideShortcutCheck(QKeyEvent *keyEvent, bool &override); void sendStringToEmu(const QByteArray &local8BitString); void focusLost(); void focusGained(); protected: // events bool event(QEvent *event) Q_DECL_OVERRIDE; void paintEvent(QPaintEvent *pe) Q_DECL_OVERRIDE; void showEvent(QShowEvent *event) Q_DECL_OVERRIDE; void hideEvent(QHideEvent *event) Q_DECL_OVERRIDE; void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE; void contextMenuEvent(QContextMenuEvent *event) Q_DECL_OVERRIDE; void focusInEvent(QFocusEvent *event) Q_DECL_OVERRIDE; void focusOutEvent(QFocusEvent *event) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; void keyReleaseEvent(QKeyEvent *event) Q_DECL_OVERRIDE; void leaveEvent(QEvent *event) Q_DECL_OVERRIDE; void mouseDoubleClickEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mousePressEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mouseReleaseEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mouseMoveEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void wheelEvent(QWheelEvent *ev) Q_DECL_OVERRIDE; bool focusNextPrevChild(bool next) Q_DECL_OVERRIDE; void fontChange(const QFont &); void extendSelection(const QPoint &position); // drag and drop void dragEnterEvent(QDragEnterEvent *event) Q_DECL_OVERRIDE; void dropEvent(QDropEvent *event) Q_DECL_OVERRIDE; void doDrag(); enum DragState { diNone, diPending, diDragging }; struct DragInfo { DragState state; QPoint start; QDrag *dragObject; } _dragInfo; // classifies the 'ch' into one of three categories // and returns a character to indicate which category it is in // // - A space (returns ' ') // - Part of a word (returns 'a') // - Other characters (returns the input character) QChar charClass(const Character &ch) const; void clearImage(); void mouseTripleClickEvent(QMouseEvent *ev); void selectLine(QPoint pos, bool entireLine); // reimplemented void inputMethodEvent(QInputMethodEvent *event) Q_DECL_OVERRIDE; QVariant inputMethodQuery(Qt::InputMethodQuery query) const Q_DECL_OVERRIDE; void onColorsChanged(); protected Q_SLOTS: void scrollBarPositionChanged(int value); void blinkTextEvent(); void blinkCursorEvent(); private Q_SLOTS: void swapFGBGColors(); void viewScrolledByUser(); /** * Called from the drag-and-drop popup. Causes the dropped URLs to be pasted as text. */ void dropMenuPasteActionTriggered(); void dropMenuCdActionTriggered(); void dismissOutputSuspendedMessage(); private: Q_DISABLE_COPY(TerminalDisplay) // -- Drawing helpers -- // divides the part of the display specified by 'rect' into // fragments according to their colors and styles and calls // drawTextFragment() or drawPrinterFriendlyTextFragment() // to draw the fragments void drawContents(QPainter &painter, const QRect &rect); // draw a transparent rectangle over the line of the current match void drawCurrentResultRect(QPainter &painter); // draws a section of text, all the text in this section // has a common color and style void drawTextFragment(QPainter &painter, const QRect &rect, const QString &text, const Character *style); void drawPrinterFriendlyTextFragment(QPainter &painter, const QRect &rect, const QString &text, const Character *style); // draws the background for a text fragment // if useOpacitySetting is true then the color's alpha value will be set to // the display's transparency (set with setOpacity()), otherwise the background // will be drawn fully opaque void drawBackground(QPainter &painter, const QRect &rect, const QColor &backgroundColor, bool useOpacitySetting); // draws the cursor character void drawCursor(QPainter &painter, const QRect &rect, const QColor &foregroundColor, const QColor &backgroundColor, bool &invertCharacterColor); // draws the characters or line graphics in a text fragment void drawCharacters(QPainter &painter, const QRect &rect, const QString &text, const Character *style, bool invertCharacterColor); // draws a string of line graphics void drawLineCharString(QPainter &painter, int x, int y, const QString &str, const Character *attributes); // draws the preedit string for input methods void drawInputMethodPreeditString(QPainter &painter, const QRect &rect); // -- // maps an area in the character image to an area on the widget QRect imageToWidget(const QRect &imageArea) const; QRect widgetToImage(const QRect &widgetArea) const; // maps a point on the widget to the position ( ie. line and column ) // of the character at that point. When the edge is true, it maps to // a character which left edge is closest to the point. void getCharacterPosition(const QPoint &widgetPoint, int &line, int &column, bool edge) const; // the area where the preedit string for input methods will be draw QRect preeditRect() const; // shows a notification window in the middle of the widget indicating the terminal's // current size in columns and lines void showResizeNotification(); // scrolls the image by a number of lines. // 'lines' may be positive ( to scroll the image down ) // or negative ( to scroll the image up ) // 'region' is the part of the image to scroll - currently only // the top, bottom and height of 'region' are taken into account, // the left and right are ignored. void scrollImage(int lines, const QRect &screenWindowRegion); void calcGeometry(); void propagateSize(); void updateImageSize(); void makeImage(); void paintFilters(QPainter &painter); // returns a region covering all of the areas of the widget which contain // a hotspot QRegion hotSpotRegion() const; // returns the position of the cursor in columns and lines QPoint cursorPosition() const; // returns true if the cursor's position is on display. bool isCursorOnDisplay() const; // redraws the cursor void updateCursor(); bool handleShortcutOverrideEvent(QKeyEvent *keyEvent); void doPaste(QString text, bool appendReturn); void processMidButtonClick(QMouseEvent *ev); QPoint findLineStart(const QPoint &pnt); QPoint findLineEnd(const QPoint &pnt); QPoint findWordStart(const QPoint &pnt); QPoint findWordEnd(const QPoint &pnt); // Uses the current settings for trimming whitespace and preserving linebreaks to create a proper flag value for Screen Screen::DecodingOptions currentDecodingOptions(); // Boilerplate setup for MessageWidget KMessageWidget* createMessageWidget(const QString &text); int loc(int x, int y) const; // the window onto the terminal screen which this display // is currently showing. QPointer _screenWindow; bool _bellMasked; QVBoxLayout *_verticalLayout; bool _fixedFont; // has fixed pitch int _fontHeight; // height int _fontWidth; // width int _fontAscent; // ascend bool _boldIntense; // Whether intense colors should be rendered with bold font int _lines; // the number of lines that can be displayed in the widget int _columns; // the number of columns that can be displayed in the widget int _usedLines; // the number of lines that are actually being used, this will be less // than 'lines' if the character image provided with setImage() is smaller // than the maximum image size which can be displayed int _usedColumns; // the number of columns that are actually being used, this will be less // than 'columns' if the character image provided with setImage() is smaller // than the maximum image size which can be displayed QRect _contentRect; Character *_image; // [lines][columns] // only the area [usedLines][usedColumns] in the image contains valid data int _imageSize; QVector _lineProperties; ColorEntry _colorTable[TABLE_COLORS]; uint _randomSeed; bool _resizing; bool _showTerminalSizeHint; bool _bidiEnabled; bool _usesMouseTracking; bool _alternateScrolling; bool _bracketedPasteMode; QPoint _iPntSel; // initial selection point QPoint _pntSel; // current selection point QPoint _tripleSelBegin; // help avoid flicker int _actSel; // selection state bool _wordSelectionMode; bool _lineSelectionMode; bool _preserveLineBreaks; bool _columnSelectionMode; bool _autoCopySelectedText; bool _copyTextAsHTML; Enum::MiddleClickPasteModeEnum _middleClickPasteMode; QScrollBar *_scrollBar; Enum::ScrollBarPositionEnum _scrollbarLocation; bool _scrollFullPage; QString _wordCharacters; int _bellMode; bool _allowBlinkingText; // allow text to blink bool _allowBlinkingCursor; // allow cursor to blink bool _textBlinking; // text is blinking, hide it when drawing bool _cursorBlinking; // cursor is blinking, hide it when drawing bool _hasTextBlinker; // has characters to blink QTimer *_blinkTextTimer; QTimer *_blinkCursorTimer; Qt::KeyboardModifiers _urlHintsModifiers; bool _showUrlHint; bool _reverseUrlHints; bool _openLinksByDirectClick; // Open URL and hosts by single mouse click bool _ctrlRequiredForDrag; // require Ctrl key for drag selected text bool _dropUrlsAsText; // always paste URLs as text without showing copy/move menu Enum::TripleClickModeEnum _tripleClickMode; bool _possibleTripleClick; // is set in mouseDoubleClickEvent and deleted // after QApplication::doubleClickInterval() delay QLabel *_resizeWidget; QTimer *_resizeTimer; bool _flowControlWarningEnabled; //widgets related to the warning message that appears when the user presses Ctrl+S to suspend //terminal output - informing them what has happened and how to resume output KMessageWidget *_outputSuspendedMessageWidget; uint _lineSpacing; QSize _size; QRgb _blendColor; ColorSchemeWallpaper::Ptr _wallpaper; // list of filters currently applied to the display. used for links and // search highlight TerminalImageFilterChain *_filterChain; QRegion _mouseOverHotspotArea; bool _filterUpdateRequired; Enum::CursorShapeEnum _cursorShape; // cursor color. If it is invalid (by default) then the foreground // color of the character under the cursor is used QColor _cursorColor; struct InputMethodData { QString preeditString; QRect previousPreeditRect; }; InputMethodData _inputMethodData; bool _antialiasText; // do we anti-alias or not bool _useFontLineCharacters; bool _printerFriendly; // are we currently painting to a printer in black/white mode //the delay in milliseconds between redrawing blinking text static const int TEXT_BLINK_DELAY = 500; //the duration of the size hint in milliseconds static const int SIZE_HINT_DURATION = 1000; SessionController *_sessionController; bool _trimLeadingSpaces; // trim leading spaces in selected text bool _trimTrailingSpaces; // trim trailing spaces in selected text bool _mouseWheelZoom; // enable mouse wheel zooming or not int _margin; // the contents margin bool _centerContents; // center the contents between margins KMessageWidget *_readOnlyMessageWidget; // Message shown at the top when read-only mode gets activated // Needed to know whether the mode really changed between update calls bool _readOnly; qreal _opacity; bool _dimWhenInactive; ScrollState _scrollWheelState; IncrementalSearchBar *_searchBar; QRect _searchResultRect; friend class TerminalDisplayAccessible; }; class AutoScrollHandler : public QObject { Q_OBJECT public: explicit AutoScrollHandler(QWidget *parent); protected: void timerEvent(QTimerEvent *event) Q_DECL_OVERRIDE; bool eventFilter(QObject *watched, QEvent *event) Q_DECL_OVERRIDE; private: QWidget *widget() const { return static_cast(parent()); } int _timerId; }; } #endif // TERMINALDISPLAY_H diff --git a/src/ViewContainer.cpp b/src/ViewContainer.cpp index 16620247..8c473f36 100644 --- a/src/ViewContainer.cpp +++ b/src/ViewContainer.cpp @@ -1,543 +1,543 @@ /* 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 // Qt #include #include #include #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" // 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()), _closeTabButton(new QToolButton()), _contextMenuTabIndex(-1), _navigationVisibility(ViewManager::NavigationVisibility::NavigationNotSet), _tabHistoryIndex(-1) { 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("document-new"))); _newTabButton->setAutoRaise(true); connect(_newTabButton, &QToolButton::clicked, this, [this]{ emit newViewRequest(this); }); _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(this, terminalAt(idx)); }); 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() == QStringLiteral("view-readonly")) { _contextPopupMenu->removeAction(action); break; } } }); #if defined(ENABLE_DETACHING) auto detachAction = _contextPopupMenu->addAction( QIcon::fromTheme(QStringLiteral("tab-detach")), i18nc("@action:inmenu", "&Detach Tab"), this, [this] { emit detachTab(this, terminalAt(_contextMenuTabIndex)); } ); detachAction->setObjectName(QStringLiteral("tab-detach")); #endif 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(); auto profileList = new ProfileList(false, profileMenu); profileList->syncWidgetActions(profileMenu, true); connect(profileList, &Konsole::ProfileList::profileSelected, this, - [this](Profile::Ptr profile) { emit newViewWithProfileRequest(this, profile); }); + [this](const Profile::Ptr &profile) { emit newViewWithProfileRequest(this, profile); }); _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); } } TerminalDisplay *TabbedViewContainer::terminalAt(int index) { return qobject_cast(widget(index)); } void TabbedViewContainer::moveTabToWindow(int index, QWidget *window) { const int id = terminalAt(index)->sessionController()->identifier(); // This one line here will be removed as soon as I finish my new split handling. // it's hacky but it works. const auto widgets = window->findChildren(); const auto currentPos = QCursor::pos(); for(const auto dropWidget : widgets) { if (dropWidget->rect().contains(dropWidget->mapFromGlobal(currentPos))) { emit dropWidget->moveViewRequest(-1, id); removeView(terminalAt(index)); } } } 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::showQuickButtons() ? _newTabButton : nullptr, Qt::TopLeftCorner); setCornerWidget( KonsoleSettings::showQuickButtons() ? _closeTabButton : nullptr, Qt::TopRightCorner); tabBar()->setExpanding(KonsoleSettings::expandTabWidth()); tabBar()->update(); if (isVisible() && KonsoleSettings::showQuickButtons()) { _newTabButton->setVisible(true); _closeTabButton->setVisible(true); } 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 = terminalAt(newIndex); auto currentWidget = terminalAt(currentIndex); auto swappedContext = swappedWidget->sessionController(); auto currentContext = currentWidget->sessionController(); if (newIndex < currentIndex) { insertTab(newIndex, currentWidget, currentContext->icon(), currentContext->title()); insertTab(currentIndex, swappedWidget, swappedContext->icon(), swappedContext->title()); } else { insertTab(currentIndex, swappedWidget, swappedContext->icon(), swappedContext->title()); insertTab(newIndex, currentWidget, currentContext->icon(), currentContext->title()); } setCurrentIndex(newIndex); } void TabbedViewContainer::addView(TerminalDisplay *view, int index) { auto item = view->sessionController(); if (index == -1) { addTab(view, item->icon(), item->title()); } else { insertTab(index, view, item->icon(), item->title()); } _tabHistory.append(view); 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); connect(view, &QWidget::destroyed, this, &Konsole::TabbedViewContainer::viewDestroyed); emit viewAdded(view); } void TabbedViewContainer::viewDestroyed(QObject *view) { auto widget = static_cast(view); const auto idx = indexOf(widget); removeTab(idx); forgetView(widget); } void TabbedViewContainer::forgetView(TerminalDisplay *view) { updateTabHistory(view, true); emit viewRemoved(view); if (count() == 0) { emit empty(this); } } void TabbedViewContainer::removeView(TerminalDisplay *view) { const int idx = indexOf(view); disconnect(view, &QWidget::destroyed, this, &Konsole::TabbedViewContainer::viewDestroyed); removeTab(idx); forgetView(view); } 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::activateLastUsedView(bool reverse) { if (_tabHistory.count() <= 1) { return; } if (_tabHistoryIndex == -1) { _tabHistoryIndex = reverse ? _tabHistory.count() - 1 : 1; } else if (reverse) { if (_tabHistoryIndex == 0) { _tabHistoryIndex = _tabHistory.count() - 1; } else { _tabHistoryIndex--; } } else { if (_tabHistoryIndex >= _tabHistory.count() - 1) { _tabHistoryIndex = 0; } else { _tabHistoryIndex++; } } int index = indexOf(_tabHistory[_tabHistoryIndex]); setCurrentIndex(index); } // Jump to last view - this allows toggling between two views // Using "Last Used Tabs" shortcut will cause "Toggle between two tabs" // shortcut to be incorrect the first time. void TabbedViewContainer::toggleLastUsedView() { if (_tabHistory.count() <= 1) { return; } setCurrentIndex(indexOf(_tabHistory.at(1))); } void TabbedViewContainer::keyReleaseEvent(QKeyEvent* event) { if (_tabHistoryIndex != -1 && event->modifiers() == Qt::NoModifier) { _tabHistoryIndex = -1; auto *active = qobject_cast(currentWidget()); if (active != _tabHistory[0]) { // Update the tab history now that we have ended the walk-through updateTabHistory(active); } } } void TabbedViewContainer::updateTabHistory(TerminalDisplay* view, bool remove) { if (_tabHistoryIndex != -1 && !remove) { // Do not reorder the tab history while we are walking through it return; } for (int i = 0; i < _tabHistory.count(); ++i ) { if (_tabHistory[i] == view) { _tabHistory.removeAt(i); if (!remove) { _tabHistory.prepend(view); } break; } } } void TabbedViewContainer::closeCurrentTab() { if (currentIndex() != -1) { closeTerminalTab(currentIndex()); } } void TabbedViewContainer::tabDoubleClicked(int index) { if (index >= 0) { renameTab(index); } else { emit newViewRequest(this); } } void TabbedViewContainer::renameTab(int index) { if (index != -1) { terminalAt(index)->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. #if defined(ENABLE_DETACHING) for(auto action : _contextPopupMenu->actions()) { if (action->objectName() == QStringLiteral("tab-detach")) { action->setEnabled(count() > 1); } } #endif // Add the read-only action auto controller = terminalAt(_contextMenuTabIndex)->sessionController(); auto sessionController = qobject_cast(controller); 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() == QStringLiteral("edit-rename")) { action->setEnabled(!sessionController->isReadOnly()); break; } } } _contextPopupMenu->exec(tabBar()->mapToGlobal(point)); } void TabbedViewContainer::currentTabChanged(int index) { if (index != -1) { auto *view = terminalAt(index); view->setFocus(); updateTabHistory(view); 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 index = indexOf(controller->view()); if (index != currentIndex()) { setTabActivity(index, true); } } void TabbedViewContainer::updateTitle(ViewProperties *item) { auto controller = qobject_cast(item); const int index = indexOf(controller->view()); 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) { terminalAt(idx)->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); } } diff --git a/src/ViewContainer.h b/src/ViewContainer.h index 2e6acdae..cec7b000 100644 --- a/src/ViewContainer.h +++ b/src/ViewContainer.h @@ -1,216 +1,216 @@ /* 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. */ #ifndef VIEWCONTAINER_H #define VIEWCONTAINER_H // Qt #include #include #include // Konsole #include "Profile.h" #include "ViewManager.h" // Qt class QPoint; class QToolButton; class QMenu; class QDropEvent; class QDragEnterEvent; namespace Konsole { class IncrementalSearchBar; class ViewProperties; class ViewManager; class TabbedViewContainer; /** * An interface for container widgets which can hold one or more views. * * The container widget typically displays a list of the views which * it has and provides a means of switching between them. * * Subclasses should reimplement the addViewWidget() and removeViewWidget() functions * to actually add or remove view widgets from the container widget, as well * as updating any navigation aids. */ class KONSOLEPRIVATE_EXPORT TabbedViewContainer : public QTabWidget { Q_OBJECT public: /** * Constructs a new view container with the specified parent. * * @param connectedViewManager Connect the new view to this manager * @param parent The parent object of the container */ TabbedViewContainer(ViewManager *connectedViewManager, QWidget *parent); /** * Called when the ViewContainer is destroyed. When reimplementing this in * subclasses, use object->deleteLater() to delete any widgets or other objects * instead of 'delete object'. */ ~TabbedViewContainer() Q_DECL_OVERRIDE; /** Adds a new view to the container widget */ void addView(TerminalDisplay *view, int index = -1); /** Removes a view from the container */ void removeView(TerminalDisplay *view); void setTabActivity(int index, bool activity); void updateTitle(ViewProperties *item); void updateIcon(ViewProperties *item); void updateActivity(ViewProperties *item); /** Changes the active view to the next view */ void activateNextView(); /** Changes the active view to the previous view */ void activatePreviousView(); /** Changes the active view to the last view */ void activateLastView(); /** Changes the active view to the last used view */ void activateLastUsedView(bool reverse); /** Toggle between last two views */ void toggleLastUsedView(); void setCss(const QString& styleSheet = QString()); void setCssFromFile(const QUrl& url); /** * This enum describes the directions * in which views can be re-arranged within the container * using the moveActiveView() method. */ enum MoveDirection { /** Moves the view to the left. */ MoveViewLeft, /** Moves the view to the right. */ MoveViewRight }; /** * Moves the active view within the container and * updates the order in which the views are shown * in the container's navigation widget. * * The default implementation does nothing. */ void moveActiveView(MoveDirection direction); /** Sets the menu to be shown when the new view button is clicked. * Only valid if the QuickNewView feature is enabled. * The default implementation does nothing. */ // TODO: Reenable this later. // void setNewViewMenu(QMenu *menu); void renameTab(int index); ViewManager *connectedViewManager(); void currentTabChanged(int index); void closeCurrentTab(); void wheelScrolled(int delta); void tabDoubleClicked(int index); void openTabContextMenu(const QPoint &point); void setNavigationVisibility(ViewManager::NavigationVisibility navigationVisibility); void moveTabToWindow(int index, QWidget *window); /* return the widget(int index) casted to TerminalDisplay* * * The only thing that this class holds are TerminalDisplays, so * this is the only thing that should be used to retrieve widgets. */ TerminalDisplay *terminalAt(int index); Q_SIGNALS: /** Emitted when the container has no more children */ void empty(TabbedViewContainer *container); /** Emitted when the user requests to open a new view */ void newViewRequest(TabbedViewContainer *thisContainer); /** Requests creation of a new view, with the selected profile. */ - void newViewWithProfileRequest(TabbedViewContainer *thisContainer, Profile::Ptr); + void newViewWithProfileRequest(TabbedViewContainer *thisContainer, const Profile::Ptr&); /** * Emitted when the user requests to move a view from another container * into this container. If 'success' is set to true by a connected slot * then the original view will be removed. * * @param index Index at which to insert the new view in the container * or -1 to append it. This index should be passed to addView() when * the new view has been created. * @param sessionControllerId The identifier of the view. */ void moveViewRequest(int index, int sessionControllerId); /** Emitted when the active view changes */ void activeViewChanged(TerminalDisplay *view); /** Emitted when a view is added to the container. */ void viewAdded(TerminalDisplay *view); /** Emitted when a view is removed from the container. */ void viewRemoved(TerminalDisplay *view); /** detach the specific tab */ void detachTab(TabbedViewContainer *self, TerminalDisplay *activeView); protected: /** * Rearranges the order of widgets in the container. * * @param fromIndex Current index of the widget to move * @param toIndex New index for the widget */ void moveViewWidget(int fromIndex, int toIndex); // close tabs and unregister void closeTerminalTab(int idx); void keyReleaseEvent(QKeyEvent *event) Q_DECL_OVERRIDE; private Q_SLOTS: void viewDestroyed(QObject *view); void konsoleConfigChanged(); private: void forgetView(TerminalDisplay *view); void updateTabHistory(TerminalDisplay *view, bool remove = false); QList _tabHistory; ViewManager *_connectedViewManager; QMenu *_contextPopupMenu; QToolButton *_newTabButton; QToolButton *_closeTabButton; int _contextMenuTabIndex; ViewManager::NavigationVisibility _navigationVisibility; int _tabHistoryIndex; }; } #endif //VIEWCONTAINER_H diff --git a/src/ViewManager.cpp b/src/ViewManager.cpp index ef71a444..0e6003de 100644 --- a/src/ViewManager.cpp +++ b/src/ViewManager.cpp @@ -1,1144 +1,1144 @@ /* Copyright 2006-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "ViewManager.h" #include // Qt #include #include #include // KDE #include #include #include #include // Konsole #include #include "ColorScheme.h" #include "ColorSchemeManager.h" #include "Session.h" #include "TerminalDisplay.h" #include "SessionController.h" #include "SessionManager.h" #include "ProfileManager.h" #include "ViewSplitter.h" #include "Enumeration.h" #include "ViewContainer.h" using namespace Konsole; int ViewManager::lastManagerId = 0; ViewManager::ViewManager(QObject *parent, KActionCollection *collection) : QObject(parent), _viewSplitter(nullptr), _pluggedController(nullptr), _sessionMap(QHash()), _actionCollection(collection), _navigationMethod(NoNavigation), _navigationVisibility(NavigationNotSet), _newTabBehavior(PutNewTabAtTheEnd), _managerId(0) { // create main view area _viewSplitter = new ViewSplitter(nullptr); KAcceleratorManager::setNoAccel(_viewSplitter); // the ViewSplitter class supports both recursive and non-recursive splitting, // in non-recursive mode, all containers are inserted into the same top-level splitter // widget, and all the divider lines between the containers have the same orientation // // the ViewManager class is not currently able to handle a ViewSplitter in recursive-splitting // mode _viewSplitter->setRecursiveSplitting(false); _viewSplitter->setFocusPolicy(Qt::NoFocus); // setup actions which are related to the views setupActions(); // emit a signal when all of the views held by this view manager are destroyed connect(_viewSplitter.data(), &Konsole::ViewSplitter::allContainersEmpty, this, &Konsole::ViewManager::empty); connect(_viewSplitter.data(), &Konsole::ViewSplitter::empty, this, &Konsole::ViewManager::empty); // listen for 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); _viewSplitter->addContainer(createContainer(), Qt::Vertical); } ViewManager::~ViewManager() = default; int ViewManager::managerId() const { return _managerId; } QWidget *ViewManager::activeView() const { TabbedViewContainer *container = _viewSplitter->activeContainer(); if (container != nullptr) { return container->currentWidget(); } else { return nullptr; } } QWidget *ViewManager::widget() const { return _viewSplitter; } void ViewManager::setupActions() { Q_ASSERT(_actionCollection); if (_actionCollection == nullptr) { return; } KActionCollection *collection = _actionCollection; QAction *nextViewAction = new QAction(i18nc("@action Shortcut entry", "Next Tab"), this); QAction *previousViewAction = new QAction(i18nc("@action Shortcut entry", "Previous Tab"), this); QAction *lastViewAction = new QAction(i18nc("@action Shortcut entry", "Switch to Last Tab"), this); QAction *lastUsedViewAction = new QAction(i18nc("@action Shortcut entry", "Last Used Tabs"), this); QAction *lastUsedViewReverseAction = new QAction(i18nc("@action Shortcut entry", "Last Used Tabs (Reverse)"), this); QAction *toggleTwoViewsAction = new QAction(i18nc("@action Shortcut entry", "Toggle Between Two Tabs"), this); QAction *nextContainerAction = new QAction(i18nc("@action Shortcut entry", "Next View Container"), this); QAction *moveViewLeftAction = new QAction(i18nc("@action Shortcut entry", "Move Tab Left"), this); QAction *moveViewRightAction = new QAction(i18nc("@action Shortcut entry", "Move Tab Right"), this); // list of actions that should only be enabled when there are multiple view // containers open QList multiViewOnlyActions; multiViewOnlyActions << nextContainerAction; QAction *splitLeftRightAction = new QAction(QIcon::fromTheme(QStringLiteral("view-split-left-right")), i18nc("@action:inmenu", "Split View Left/Right"), this); collection->setDefaultShortcut(splitLeftRightAction, Konsole::ACCEL + Qt::Key_ParenLeft); collection->addAction(QStringLiteral("split-view-left-right"), splitLeftRightAction); connect(splitLeftRightAction, &QAction::triggered, this, &Konsole::ViewManager::splitLeftRight); QAction *splitTopBottomAction = new QAction(QIcon::fromTheme(QStringLiteral("view-split-top-bottom")), i18nc("@action:inmenu", "Split View Top/Bottom"), this); collection->setDefaultShortcut(splitTopBottomAction, Konsole::ACCEL + Qt::Key_ParenRight); collection->addAction(QStringLiteral("split-view-top-bottom"), splitTopBottomAction); connect(splitTopBottomAction, &QAction::triggered, this, &Konsole::ViewManager::splitTopBottom); QAction *closeActiveAction = new QAction(i18nc("@action:inmenu Close Active View", "Close Active"), this); closeActiveAction->setIcon(QIcon::fromTheme(QStringLiteral("view-close"))); collection->setDefaultShortcut(closeActiveAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_X); closeActiveAction->setEnabled(false); collection->addAction(QStringLiteral("close-active-view"), closeActiveAction); connect(closeActiveAction, &QAction::triggered, this, &Konsole::ViewManager::closeActiveContainer); multiViewOnlyActions << closeActiveAction; QAction *closeOtherAction = new QAction(i18nc("@action:inmenu Close Other Views", "Close Others"), this); collection->setDefaultShortcut(closeOtherAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_O); closeOtherAction->setEnabled(false); collection->addAction(QStringLiteral("close-other-views"), closeOtherAction); connect(closeOtherAction, &QAction::triggered, this, &Konsole::ViewManager::closeOtherContainers); multiViewOnlyActions << closeOtherAction; // Expand & Shrink Active View QAction *expandActiveAction = new QAction(i18nc("@action:inmenu", "Expand View"), this); collection->setDefaultShortcut(expandActiveAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_BracketRight); expandActiveAction->setEnabled(false); collection->addAction(QStringLiteral("expand-active-view"), expandActiveAction); connect(expandActiveAction, &QAction::triggered, this, &Konsole::ViewManager::expandActiveContainer); multiViewOnlyActions << expandActiveAction; QAction *shrinkActiveAction = new QAction(i18nc("@action:inmenu", "Shrink View"), this); collection->setDefaultShortcut(shrinkActiveAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_BracketLeft); shrinkActiveAction->setEnabled(false); collection->addAction(QStringLiteral("shrink-active-view"), shrinkActiveAction); connect(shrinkActiveAction, &QAction::triggered, this, &Konsole::ViewManager::shrinkActiveContainer); multiViewOnlyActions << shrinkActiveAction; // Crashes on Mac. #if defined(ENABLE_DETACHING) QAction *detachViewAction = collection->addAction(QStringLiteral("detach-view")); detachViewAction->setEnabled(true); detachViewAction->setIcon(QIcon::fromTheme(QStringLiteral("tab-detach"))); detachViewAction->setText(i18nc("@action:inmenu", "D&etach Current Tab")); // Ctrl+Shift+D is not used as a shortcut by default because it is too close // to Ctrl+D - which will terminate the session in many cases collection->setDefaultShortcut(detachViewAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_H); connect(this, &Konsole::ViewManager::splitViewToggle, this, &Konsole::ViewManager::updateDetachViewState); connect(detachViewAction, &QAction::triggered, this, &Konsole::ViewManager::detachActiveView); #endif // Next / Previous View , Next Container collection->addAction(QStringLiteral("next-view"), nextViewAction); collection->addAction(QStringLiteral("previous-view"), previousViewAction); collection->addAction(QStringLiteral("last-tab"), lastViewAction); collection->addAction(QStringLiteral("last-used-tab"), lastUsedViewAction); collection->addAction(QStringLiteral("last-used-tab-reverse"), lastUsedViewReverseAction); collection->addAction(QStringLiteral("toggle-two-tabs"), toggleTwoViewsAction); collection->addAction(QStringLiteral("next-container"), nextContainerAction); collection->addAction(QStringLiteral("move-view-left"), moveViewLeftAction); collection->addAction(QStringLiteral("move-view-right"), moveViewRightAction); // Switch to tab N shortcuts const int SWITCH_TO_TAB_COUNT = 19; for (int i = 0; i < SWITCH_TO_TAB_COUNT; i++) { QAction *switchToTabAction = new QAction(i18nc("@action Shortcut entry", "Switch to Tab %1", i + 1), this); connect(switchToTabAction, &QAction::triggered, this, [this, i]() { switchToView(i); }); collection->addAction(QStringLiteral("switch-to-tab-%1").arg(i), switchToTabAction); } foreach (QAction *action, multiViewOnlyActions) { connect(this, &Konsole::ViewManager::splitViewToggle, action, &QAction::setEnabled); } // keyboard shortcut only actions const QList nextViewActionKeys{Qt::SHIFT + Qt::Key_Right, Qt::CTRL + Qt::Key_PageDown}; collection->setDefaultShortcuts(nextViewAction, nextViewActionKeys); connect(nextViewAction, &QAction::triggered, this, &Konsole::ViewManager::nextView); _viewSplitter->addAction(nextViewAction); const QList previousViewActionKeys{Qt::SHIFT + Qt::Key_Left, Qt::CTRL + Qt::Key_PageUp}; collection->setDefaultShortcuts(previousViewAction, previousViewActionKeys); connect(previousViewAction, &QAction::triggered, this, &Konsole::ViewManager::previousView); _viewSplitter->addAction(previousViewAction); collection->setDefaultShortcut(nextContainerAction, Qt::SHIFT + Qt::Key_Tab); connect(nextContainerAction, &QAction::triggered, this, &Konsole::ViewManager::nextContainer); _viewSplitter->addAction(nextContainerAction); #ifdef Q_OS_MACOS collection->setDefaultShortcut(moveViewLeftAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_BracketLeft); #else collection->setDefaultShortcut(moveViewLeftAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Left); #endif connect(moveViewLeftAction, &QAction::triggered, this, &Konsole::ViewManager::moveActiveViewLeft); _viewSplitter->addAction(moveViewLeftAction); #ifdef Q_OS_MACOS collection->setDefaultShortcut(moveViewRightAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_BracketRight); #else collection->setDefaultShortcut(moveViewRightAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Right); #endif connect(moveViewRightAction, &QAction::triggered, this, &Konsole::ViewManager::moveActiveViewRight); _viewSplitter->addAction(moveViewRightAction); connect(lastViewAction, &QAction::triggered, this, &Konsole::ViewManager::lastView); _viewSplitter->addAction(lastViewAction); collection->setDefaultShortcut(lastUsedViewAction, Qt::CTRL + Qt::Key_Tab); connect(lastUsedViewAction, &QAction::triggered, this, &Konsole::ViewManager::lastUsedView); _viewSplitter->addAction(lastUsedViewAction); collection->setDefaultShortcut(lastUsedViewReverseAction, Qt::CTRL + Qt::SHIFT + Qt::Key_Tab); connect(lastUsedViewReverseAction, &QAction::triggered, this, &Konsole::ViewManager::lastUsedViewReverse); _viewSplitter->addAction(lastUsedViewReverseAction); connect(toggleTwoViewsAction, &QAction::triggered, this, &Konsole::ViewManager::toggleTwoViews); _viewSplitter->addAction(toggleTwoViewsAction); } void ViewManager::switchToView(int index) { _viewSplitter->activeContainer()->setCurrentIndex(index); } void ViewManager::updateDetachViewState() { Q_ASSERT(_actionCollection); if (_actionCollection == nullptr) { return; } const bool splitView = _viewSplitter->containers().count() >= 2; auto activeContainer = _viewSplitter->activeContainer(); const bool shouldEnable = splitView || ((activeContainer != nullptr) && activeContainer->count() >= 2); QAction *detachAction = _actionCollection->action(QStringLiteral("detach-view")); if ((detachAction != nullptr) && shouldEnable != detachAction->isEnabled()) { detachAction->setEnabled(shouldEnable); } } void ViewManager::moveActiveViewLeft() { TabbedViewContainer *container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->moveActiveView(TabbedViewContainer::MoveViewLeft); } void ViewManager::moveActiveViewRight() { TabbedViewContainer *container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->moveActiveView(TabbedViewContainer::MoveViewRight); } void ViewManager::nextContainer() { _viewSplitter->activateNextContainer(); } void ViewManager::nextView() { TabbedViewContainer *container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->activateNextView(); } void ViewManager::previousView() { TabbedViewContainer *container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->activatePreviousView(); } void ViewManager::lastView() { TabbedViewContainer *container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->activateLastView(); } void ViewManager::lastUsedView() { TabbedViewContainer *container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->activateLastUsedView(false); } void ViewManager::lastUsedViewReverse() { TabbedViewContainer *container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->activateLastUsedView(true); } void ViewManager::toggleTwoViews() { TabbedViewContainer *container = _viewSplitter->activeContainer(); Q_ASSERT(container); container->toggleLastUsedView(); } void ViewManager::detachActiveView() { // find the currently active view and remove it from its container TabbedViewContainer *container = _viewSplitter->activeContainer(); detachView(container, container->currentWidget()); } void ViewManager::detachView(TabbedViewContainer *container, QWidget *view) { #if !defined(ENABLE_DETACHING) return; #endif auto *viewToDetach = qobject_cast(view); if (viewToDetach == nullptr) { return; } // BR390736 - some instances are sending invalid session to viewDetached() Session *sessionToDetach = _sessionMap[viewToDetach]; if (sessionToDetach == nullptr) { return; } emit viewDetached(sessionToDetach); _sessionMap.remove(viewToDetach); // remove the view from this window container->removeView(viewToDetach); viewToDetach->deleteLater(); // if the container from which the view was removed is now empty then it can be deleted, // unless it is the only container in the window, in which case it is left empty // so that there is always an active container if (_viewSplitter->containers().count() > 1 && container->count() == 0) { removeContainer(container); } } void ViewManager::sessionFinished() { // if this slot is called after the view manager's main widget // has been destroyed, do nothing if (_viewSplitter.isNull()) { return; } auto *session = qobject_cast(sender()); Q_ASSERT(session); // close attached views QList children = _viewSplitter->findChildren(); foreach (TerminalDisplay *view, children) { if (_sessionMap[view] == session) { _sessionMap.remove(view); view->deleteLater(); } } // 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); } } void ViewManager::viewActivated(QWidget *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) { TabbedViewContainer *container = createContainer(); if (_viewSplitter->activeContainer()->count()) { // get the currently applied profile and use it to create the new tab. auto *activeContainer= _viewSplitter->activeContainer(); auto *currentDisplay = qobject_cast(activeContainer->currentWidget()); auto profile = SessionManager::instance()->sessionProfile(_sessionMap[currentDisplay]); // Create a new session with the selected profile. auto *session = SessionManager::instance()->createSession(profile); session->addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_WINDOW=/Windows/%1").arg(managerId())); createView(session, container, 0); } _viewSplitter->addContainer(container, orientation); emit splitViewToggle(_viewSplitter->containers().count() > 0); // focus the new container container->currentWidget()->setFocus(); // ensure that the active view is focused after the split / unsplit TabbedViewContainer *activeContainer = _viewSplitter->activeContainer(); QWidget *activeView = activeContainer != nullptr ? activeContainer->currentWidget() : nullptr; if (activeView != nullptr) { activeView->setFocus(Qt::OtherFocusReason); } } void ViewManager::removeContainer(TabbedViewContainer *container) { // remove session map entries for views in this container for(int i = 0, end = container->count(); i < end; i++) { auto view = container->widget(i); auto *display = qobject_cast(view); Q_ASSERT(display); _sessionMap.remove(display); } _viewSplitter->removeContainer(container); container->deleteLater(); emit splitViewToggle(_viewSplitter->containers().count() > 1); } void ViewManager::expandActiveContainer() { _viewSplitter->adjustContainerSize(_viewSplitter->activeContainer(), 10); } void ViewManager::shrinkActiveContainer() { _viewSplitter->adjustContainerSize(_viewSplitter->activeContainer(), -10); } void ViewManager::closeActiveContainer() { // only do something if there is more than one container active if (_viewSplitter->containers().count() > 1) { TabbedViewContainer *container = _viewSplitter->activeContainer(); removeContainer(container); // focus next container so that user can continue typing // without having to manually focus it themselves nextContainer(); } } void ViewManager::closeOtherContainers() { TabbedViewContainer *active = _viewSplitter->activeContainer(); foreach (TabbedViewContainer *container, _viewSplitter->containers()) { if (container != active) { removeContainer(container); } } } SessionController *ViewManager::createController(Session *session, TerminalDisplay *view) { // create a new controller for the session, and ensure that this view manager // is notified when the view gains the focus 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; } void ViewManager::controllerChanged(SessionController *controller) { if (controller == _pluggedController) { return; } _viewSplitter->setFocusProxy(controller->view()); _pluggedController = controller; emit activeViewChanged(controller); } SessionController *ViewManager::activeViewController() const { return _pluggedController; } void ViewManager::createView(Session *session, TabbedViewContainer *container, int index) { // notify this view manager when the session finishes so that its view // can be deleted // // Use Qt::UniqueConnection to avoid duplicate connection connect(session, &Konsole::Session::finished, this, &Konsole::ViewManager::sessionFinished, Qt::UniqueConnection); TerminalDisplay *display = createTerminalDisplay(session); const Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); applyProfileToView(display, profile); // set initial size const QSize &preferredSize = session->preferredSize(); display->setSize(preferredSize.width(), preferredSize.height()); createController(session, display); _sessionMap[display] = session; container->addView(display, index); session->addView(display); // tell the session whether it has a light or dark background session->setDarkBackground(colorSchemeForProfile(profile)->hasDarkBackground()); container->setCurrentWidget(display); display->setFocus(Qt::OtherFocusReason); updateDetachViewState(); } void ViewManager::createView(TabbedViewContainer *tabWidget, Session *session) { const int index = _newTabBehavior == PutNewTabAfterCurrentTab ? _viewSplitter->activeContainer()->currentIndex() + 1 : -1; createView(session, tabWidget, index); } TabbedViewContainer *ViewManager::createContainer() { auto *container = new TabbedViewContainer(this, _viewSplitter); container->setNavigationVisibility(_navigationVisibility); //TODO: Fix Detaching. connect(container, &TabbedViewContainer::detachTab, this, &ViewManager::detachView); // 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::moveViewRequest, this, &Konsole::ViewManager::containerMoveViewRequest); connect(container, &Konsole::TabbedViewContainer::viewRemoved, this, &Konsole::ViewManager::viewDestroyed); connect(container, &Konsole::TabbedViewContainer::activeViewChanged, this, &Konsole::ViewManager::viewActivated); return container; } void ViewManager::containerMoveViewRequest(int index, int id) { auto *container = qobject_cast(sender()); auto *controller = qobject_cast(ViewProperties::propertiesById(id)); Q_ASSERT(container); Q_ASSERT(controller); createView(controller->session(), container, index); controller->session()->refresh(); container->currentWidget()->setFocus(); } 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) { if ((!_viewSplitter.isNull()) && container == _viewSplitter->activeContainer()) { emit viewPropertiesChanged(viewProperties()); } } void ViewManager::viewDestroyed(QWidget *view) { // Note: the received QWidget has already been destroyed, so // using dynamic_cast<> or qobject_cast<> does not work here // 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 if (!_viewSplitter.isNull()) { updateDetachViewState(); } // The below causes the menus to be messed up // Only happens when using the tab bar close button // if (_pluggedController) // emit unplugController(_pluggedController); } TerminalDisplay *ViewManager::createTerminalDisplay(Session *session) { auto display = new TerminalDisplay(nullptr); display->setRandomSeed(session->sessionId() * 31); return display; } -const ColorScheme *ViewManager::colorSchemeForProfile(const Profile::Ptr profile) +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) +bool ViewManager::profileHasBlurEnabled(const Profile::Ptr &profile) { return colorSchemeForProfile(profile)->blur(); } -void ViewManager::applyProfileToView(TerminalDisplay *view, const Profile::Ptr profile) +void ViewManager::applyProfileToView(TerminalDisplay *view, const Profile::Ptr &profile) { Q_ASSERT(profile); emit updateWindowIcon(); // load color scheme ColorEntry table[TABLE_COLORS]; const ColorScheme *colorScheme = colorSchemeForProfile(profile); colorScheme->getColorTable(table, view->randomSeed()); view->setColorTable(table); view->setOpacity(colorScheme->opacity()); view->setWallpaper(colorScheme->wallpaper()); emit blurSettingChanged(colorScheme->blur()); // load font view->setAntialias(profile->antiAliasFonts()); view->setBoldIntense(profile->boldIntense()); view->setUseFontLineCharacters(profile->useFontLineCharacters()); view->setVTFont(profile->font()); // set scroll-bar position view->setScrollBarPosition(Enum::ScrollBarPositionEnum(profile->property(Profile::ScrollBarPosition))); view->setScrollFullPage(profile->property(Profile::ScrollFullPage)); // show hint about terminal size after resizing view->setShowTerminalSizeHint(profile->showTerminalSizeHint()); view->setDimWhenInactive(profile->dimWhenInactive()); // terminal features view->setBlinkingCursorEnabled(profile->blinkingCursorEnabled()); view->setBlinkingTextEnabled(profile->blinkingTextEnabled()); view->setTripleClickMode(Enum::TripleClickModeEnum(profile->property(Profile::TripleClickMode))); view->setAutoCopySelectedText(profile->autoCopySelectedText()); view->setControlDrag(profile->property(Profile::CtrlRequiredForDrag)); view->setDropUrlsAsText(profile->property(Profile::DropUrlsAsText)); view->setBidiEnabled(profile->bidiRenderingEnabled()); view->setLineSpacing(profile->lineSpacing()); view->setTrimLeadingSpaces(profile->property(Profile::TrimLeadingSpacesInSelectedText)); view->setTrimTrailingSpaces(profile->property(Profile::TrimTrailingSpacesInSelectedText)); view->setOpenLinksByDirectClick(profile->property(Profile::OpenLinksByDirectClickEnabled)); view->setUrlHintsModifiers(profile->property(Profile::UrlHintsModifiers)); view->setReverseUrlHintsEnabled(profile->property(Profile::ReverseUrlHints)); view->setMiddleClickPasteMode(Enum::MiddleClickPasteModeEnum(profile->property(Profile::MiddleClickPasteMode))); view->setCopyTextAsHTML(profile->property(Profile::CopyTextAsHTML)); // margin/center view->setMargin(profile->property(Profile::TerminalMargin)); view->setCenterContents(profile->property(Profile::TerminalCenter)); // cursor shape view->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) view->setKeyboardCursorColor(profile->useCustomCursorColor() ? profile->customCursorColor() : QColor()); // word characters view->setWordCharacters(profile->wordCharacters()); // bell mode view->setBellMode(profile->property(Profile::BellMode)); // mouse wheel zoom view->setMouseWheelZoom(profile->mouseWheelZoomEnabled()); view->setAlternateScrolling(profile->property(Profile::AlternateScrolling)); } void ViewManager::updateViewsForSession(Session *session) { const Profile::Ptr profile = SessionManager::instance()->sessionProfile(session); const QList sessionMapKeys = _sessionMap.keys(session); foreach (TerminalDisplay *view, sessionMapKeys) { applyProfileToView(view, profile); } } -void ViewManager::profileChanged(Profile::Ptr 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 = _viewSplitter->activeContainer(); if (container == nullptr) { return {}; } list.reserve(container->count()); for(int i = 0, end = container->count(); i < end; i++) { auto view = container->terminalAt(i); ViewProperties *properties = view->sessionController(); Q_ASSERT(properties); list << properties; } return list; } void ViewManager::saveSessions(KConfigGroup &group) { // find all unique session restore IDs QList ids; QSet unique; int tab = 1; TabbedViewContainer *container = _viewSplitter->activeContainer(); // first: sessions in the active container, preserving the order Q_ASSERT(container); if (container == nullptr) { return; } ids.reserve(container->count()); auto *activeview = qobject_cast(container->currentWidget()); for (int i = 0, end = container->count(); i < end; i++) { auto *view = qobject_cast(container->widget(i)); Q_ASSERT(view); Session *session = _sessionMap[view]; ids << SessionManager::instance()->getRestoreId(session); unique.insert(session); if (view == activeview) { group.writeEntry("Active", tab); } tab++; } // second: all other sessions, in random order // we don't want to have sessions restored that are not connected foreach (Session *session, _sessionMap) { if (!unique.contains(session)) { ids << SessionManager::instance()->getRestoreId(session); unique.insert(session); } } group.writeEntry("Sessions", ids); } TabbedViewContainer *ViewManager::activeContainer() { return _viewSplitter->activeContainer(); } void ViewManager::restoreSessions(const KConfigGroup &group) { QList ids = group.readEntry("Sessions", QList()); int activeTab = group.readEntry("Active", 0); TerminalDisplay *display = nullptr; int tab = 1; foreach (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(); break; } createView(activeContainer(), session); if (!session->isRunning()) { session->run(); } if (tab++ == activeTab) { display = qobject_cast(activeView()); } } if (display != nullptr) { _viewSplitter->activeContainer()->setCurrentWidget(display); display->setFocus(Qt::OtherFocusReason); } if (ids.isEmpty()) { // Session file is unusable, start default Profile Profile::Ptr profile = ProfileManager::instance()->defaultProfile(); Session *session = SessionManager::instance()->createSession(profile); createView(activeContainer(), session); if (!session->isRunning()) { session->run(); } } } 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() { QHash::const_iterator i; for (i = _sessionMap.constBegin(); i != _sessionMap.constEnd(); ++i) { if (i.key()->isVisible()) { return i.value()->sessionId(); } } return -1; } void ViewManager::setCurrentSession(int sessionId) { QHash::const_iterator i; for (i = _sessionMap.constBegin(); i != _sessionMap.constEnd(); ++i) { if (i.value()->sessionId() == sessionId) { TabbedViewContainer *container = _viewSplitter->activeContainer(); if (container != nullptr) { container->setCurrentWidget(i.key()); } } } } int ViewManager::newSession() { Profile::Ptr profile = ProfileManager::instance()->defaultProfile(); Session *session = SessionManager::instance()->createSession(profile); session->addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_WINDOW=/Windows/%1").arg(managerId())); createView(activeContainer(), session); session->run(); return session->sessionId(); } int ViewManager::newSession(const QString &profile) { const QList profilelist = ProfileManager::instance()->allProfiles(); Profile::Ptr profileptr = ProfileManager::instance()->defaultProfile(); for (const auto &i : profilelist) { if (i->name() == profile) { profileptr = i; break; } } Session *session = SessionManager::instance()->createSession(profileptr); session->addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_WINDOW=/Windows/%1").arg(managerId())); createView(activeContainer(), session); session->run(); return session->sessionId(); } int ViewManager::newSession(const QString &profile, const QString &directory) { const QList profilelist = ProfileManager::instance()->allProfiles(); Profile::Ptr profileptr = ProfileManager::instance()->defaultProfile(); for (const auto &i : profilelist) { if (i->name() == profile) { profileptr = i; break; } } Session *session = SessionManager::instance()->createSession(profileptr); session->setInitialWorkingDirectory(directory); session->addEnvironmentEntry(QStringLiteral("KONSOLE_DBUS_WINDOW=/Windows/%1").arg(managerId())); createView(activeContainer(), session); 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) { for(auto container : _viewSplitter->containers()) { container->tabBar()->setExpanding(!setTabWidthToText); container->tabBar()->update(); } } void ViewManager::setNavigationVisibility(NavigationVisibility navigationVisibility) { if (_navigationVisibility != navigationVisibility) { _navigationVisibility = navigationVisibility; for(auto *container : _viewSplitter->containers()) { container->setNavigationVisibility(navigationVisibility); } } } void ViewManager::setNavigationBehavior(int behavior) { _newTabBehavior = static_cast(behavior); } diff --git a/src/ViewManager.h b/src/ViewManager.h index 54afd4b9..40fea353 100644 --- a/src/ViewManager.h +++ b/src/ViewManager.h @@ -1,435 +1,435 @@ /* 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. */ #ifndef VIEWMANAGER_H #define VIEWMANAGER_H // Qt #include #include #include // Konsole #include "Profile.h" class KActionCollection; class KConfigGroup; namespace Konsole { class ColorScheme; class Session; class TerminalDisplay; class TabbedViewContainer; class SessionController; class ViewProperties; class ViewSplitter; class TabbedViewContainer; /** * Manages the terminal display widgets in a Konsole window or part. * * When a view manager is created, it constructs a splitter widget ( accessed via * widget() ) to hold one or more view containers. Each view container holds * one or more terminal displays and a navigation widget ( eg. tabs or a list ) * to allow the user to navigate between the displays in that container. * * The view manager provides menu actions ( defined in the 'konsoleui.rc' XML file ) * to manipulate the views and view containers - for example, actions to split the view * left/right or top/bottom, detach a view from the current window and navigate between * views and containers. These actions are added to the collection specified in the * ViewManager's constructor. * * The view manager provides facilities to construct display widgets for a terminal * session and also to construct the SessionController which provides the menus and other * user interface elements specific to each display/session pair. * */ class KONSOLEPRIVATE_EXPORT ViewManager : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.konsole.Window") public: /** * Constructs a new view manager with the specified @p parent. * View-related actions defined in 'konsoleui.rc' are created * and added to the specified @p collection. */ ViewManager(QObject *parent, KActionCollection *collection); ~ViewManager() Q_DECL_OVERRIDE; /** * Creates a new view to display the output from and deliver input to @p session. * Constructs a new container to hold the views if no container has yet been created. */ void createView(TabbedViewContainer *tabWidget, Session *session); /** * Applies the view-specific settings associated with specified @p profile * to the terminal display @p view. */ - void applyProfileToView(TerminalDisplay *view, const Profile::Ptr profile); + void applyProfileToView(TerminalDisplay *view, const Profile::Ptr &profile); /** * Return the main widget for the view manager which * holds all of the views managed by this ViewManager instance. */ QWidget *widget() const; /** * Returns the view manager's active view. */ QWidget *activeView() const; /** * Returns the list of view properties for views in the active container. * Each view widget is associated with a ViewProperties instance which * provides access to basic information about the session being * displayed in the view, such as title, current directory and * associated icon. */ QList viewProperties() const; /** * This enum describes the available types of navigation widget * which newly created containers can provide to allow navigation * between open sessions. */ enum NavigationMethod { /** * Each container has a row of tabs (one per session) which the user * can click on to navigate between open sessions. */ TabbedNavigation, /** The container has no navigation widget. */ NoNavigation }; /** * This enum describes where newly created tab should be placed. */ enum NewTabBehavior { /** Put newly created tab at the end. */ PutNewTabAtTheEnd = 0, /** Put newly created tab right after current tab. */ PutNewTabAfterCurrentTab = 1 }; /** * Describes the options for showing or hiding the container's navigation widget. */ enum NavigationVisibility { NavigationNotSet, // Don't rely on this information, Only use the settings. AlwaysShowNavigation, ShowNavigationAsNeeded, AlwaysHideNavigation }; /** * Sets the visibility of the view container's navigation widget. * The ViewContainer subclass is responsible for ensuring that this * setting is respected as views are dded or removed from the container */ void setNavigationVisibility(NavigationVisibility navigationVisibility); /** Returns the current mode for controlling the visibility of the * view container's navigation widget. */ NavigationVisibility navigationVisibility() const; /** * Sets the type of widget provided to navigate between open sessions * in a container. The changes will only apply to newly created containers. * * The default method is TabbedNavigation. To disable navigation widgets, call * setNavigationMethod(ViewManager::NoNavigation) before creating any sessions. */ void setNavigationMethod(NavigationMethod method); /** * Returns the type of navigation widget created in new containers. * See setNavigationMethod() */ NavigationMethod navigationMethod() const; /** * Returns the controller for the active view. activeViewChanged() is * emitted when this changes. */ SessionController *activeViewController() const; /** * Session management */ void saveSessions(KConfigGroup &group); void restoreSessions(const KConfigGroup &group); void setNavigationBehavior(int behavior); int managerId() const; /** Returns a list of sessions in this ViewManager */ QList sessions() { return _sessionMap.values(); } /** * Returns whether the @p profile has the blur setting enabled */ - static bool profileHasBlurEnabled(const Profile::Ptr profile); + static bool profileHasBlurEnabled(const Profile::Ptr &profile); /** returns the active tab from the view */ TabbedViewContainer *activeContainer(); Q_SIGNALS: /** Emitted when the last view is removed from the view manager */ void empty(); /** Emitted when a session is detached from a view owned by this ViewManager */ void viewDetached(Session *session); /** * Emitted when the active view changes. * @param controller The controller associated with the active view */ void activeViewChanged(SessionController *controller); /** * Emitted when the current session needs unplugged from factory(). * @param controller The controller associated with the active view */ void unplugController(SessionController *controller); /** * Emitted when the list of view properties ( as returned by viewProperties() ) changes. * This occurs when views are added to or removed from the active container, or * if the active container is changed. */ void viewPropertiesChanged(const QList &propertiesList); /** * Emitted when the number of views containers changes. This is used to disable or * enable menu items which can only be used when there are one or multiple containers * visible. * * @param multipleViews True if there are multiple view containers open or false if there is * just a single view. */ void splitViewToggle(bool multipleViews); /** * Emitted when menu bar visibility changes because a profile that requires so is * activated. */ void setMenuBarVisibleRequest(bool); void updateWindowIcon(); void blurSettingChanged(bool); /** Requests creation of a new view with the default profile. */ void newViewRequest(TabbedViewContainer *tabWidget); /** Requests creation of a new view, with the selected profile. */ - void newViewWithProfileRequest(TabbedViewContainer *tabWidget, Profile::Ptr); + void newViewWithProfileRequest(TabbedViewContainer *tabWidget, const Profile::Ptr&); public Q_SLOTS: /** DBus slot that returns the number of sessions in the current view. */ Q_SCRIPTABLE int sessionCount(); /** * DBus slot that returns the unique ids of the sessions in the * current view. The returned list is not sorted. * QList is not printable by qdbus so we use QStringList */ Q_SCRIPTABLE QStringList sessionList(); /** DBus slot that returns the current (active) session window */ Q_SCRIPTABLE int currentSession(); /** DBus slot that sets the current (active) session window */ Q_SCRIPTABLE void setCurrentSession(int sessionId); /** DBus slot that creates a new session in the current view. * @param profile the name of the profile to be used * started. */ Q_SCRIPTABLE int newSession(const QString &profile); /** DBus slot that creates a new session in the current view. * @param profile the name of the profile to be used * @param directory the working directory where the session is * started. */ Q_SCRIPTABLE int newSession(const QString &profile, const QString &directory); // TODO: its semantic is application-wide. Move it to more appropriate place // DBus slot that returns the name of default profile Q_SCRIPTABLE QString defaultProfile(); // TODO: its semantic is application-wide. Move it to more appropriate place // DBus slot that returns a string list of defined (known) profiles Q_SCRIPTABLE QStringList profileList(); /** DBus slot that creates a new session in the current view with the associated * default profile and the default working directory */ Q_SCRIPTABLE int newSession(); /** DBus slot that changes the view port to the next session */ Q_SCRIPTABLE void nextSession(); /** DBus slot that changes the view port to the previous session */ Q_SCRIPTABLE void prevSession(); /** DBus slot that switches the current session (as returned by * currentSession()) with the left (or previous) one in the * navigation tab. */ Q_SCRIPTABLE void moveSessionLeft(); /** DBus slot that Switches the current session (as returned by * currentSession()) with the right (or next) one in the navigation * tab. */ Q_SCRIPTABLE void moveSessionRight(); /** DBus slot that sets ALL tabs' width to match their text */ Q_SCRIPTABLE void setTabWidthToText(bool); private Q_SLOTS: // called when the "Split View Left/Right" menu item is selected void splitLeftRight(); void splitTopBottom(); void closeActiveContainer(); void closeOtherContainers(); void expandActiveContainer(); void shrinkActiveContainer(); // called when the "Detach View" menu item is selected void detachActiveView(); void updateDetachViewState(); // called when a session terminates - the view manager will delete any // views associated with the session void sessionFinished(); // called when one view has been destroyed void viewDestroyed(QWidget *view); // controller detects when an associated view is given the focus // and emits a signal. ViewManager listens for that signal // and then plugs the action into the UI //void viewFocused( SessionController* controller ); // called when the active view in a ViewContainer changes, so // that we can plug the appropriate actions into the UI void viewActivated(QWidget *view); // called when "Next View" shortcut is activated void nextView(); // called when "Previous View" shortcut is activated void previousView(); // called when "Switch to last tab" shortcut is activated void lastView(); // called when "Switch to last used tab" shortcut is activated void lastUsedView(); // called when "Switch to last used tab (reverse)" shortcut is activated void lastUsedViewReverse(); // called when "Next View Container" shortcut is activated void nextContainer(); // called when "Toggle Two tabs" shortcut is activated void toggleTwoViews(); // called when the views in a container owned by this view manager // changes void containerViewsChanged(TabbedViewContainer *container); // called when a profile changes - void profileChanged(Profile::Ptr profile); + void profileChanged(const Profile::Ptr &profile); void updateViewsForSession(Session *session); // moves active view to the left void moveActiveViewLeft(); // moves active view to the right void moveActiveViewRight(); // switches to the view at visual position 'index' // in the current container void switchToView(int index); // called when a SessionController gains focus void controllerChanged(SessionController *controller); // called when a ViewContainer requests a view be // moved void containerMoveViewRequest(int index, int sessionControllerId); void detachView(TabbedViewContainer *container, QWidget *view); private: Q_DISABLE_COPY(ViewManager) void createView(Session *session, TabbedViewContainer *container, int index); - static const ColorScheme *colorSchemeForProfile(const Profile::Ptr profile); + static const ColorScheme *colorSchemeForProfile(const Profile::Ptr &profile); void setupActions(); // takes a view from a view container owned by a different manager and places it in // newContainer owned by this manager void takeView(ViewManager *otherManager, TabbedViewContainer *otherContainer, TabbedViewContainer *newContainer, TerminalDisplay *view); void splitView(Qt::Orientation orientation); // creates a new container which can hold terminal displays TabbedViewContainer *createContainer(); // removes a container and emits appropriate signals void removeContainer(TabbedViewContainer *container); // creates a new terminal display // the 'session' is used so that the terminal display's random seed // can be set to something which depends uniquely on that session TerminalDisplay *createTerminalDisplay(Session *session = nullptr); // creates a new controller for a session/display pair which provides the menu // actions associated with that view, and exposes basic information // about the session ( such as title and associated icon ) to the display. SessionController *createController(Session *session, TerminalDisplay *view); private: QPointer _viewSplitter; QPointer _pluggedController; QHash _sessionMap; KActionCollection *_actionCollection; NavigationMethod _navigationMethod; NavigationVisibility _navigationVisibility; NewTabBehavior _newTabBehavior; int _managerId; static int lastManagerId; }; } #endif diff --git a/src/autotests/TerminalCharacterDecoderTest.cpp b/src/autotests/TerminalCharacterDecoderTest.cpp index b1839f5d..ce5db61d 100644 --- a/src/autotests/TerminalCharacterDecoderTest.cpp +++ b/src/autotests/TerminalCharacterDecoderTest.cpp @@ -1,123 +1,123 @@ /* Copyright 2008 by Robert Knight Copyright 2013, 2018 by Kurt Hindenburg 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 "TerminalCharacterDecoderTest.h" // Qt #include #include #include #include // KDE #include using namespace Konsole; -Character* TerminalCharacterDecoderTest::convertToCharacter(QString text, QVector renditions) +Character* TerminalCharacterDecoderTest::convertToCharacter(const QString &text, QVector renditions) { auto charResult = new Character[text.size()]; // Force renditions size to match that of text; default will be DEFAULT_RENDITION. if (renditions.size() < text.size()) { renditions.resize(text.size()); } for (int i = 0; i < text.size(); ++i) { charResult[i] = Character(text.at(i).unicode()); charResult[i].rendition = renditions.at(i); } return charResult; } void TerminalCharacterDecoderTest::testPlainTextDecoder_data() { QTest::addColumn("text"); QTest::addColumn>("renditions"); QTest::addColumn("result"); /* Notes: * - rendition has no effect on plain decoded text * * TODO: need to add foregroundColor, backgroundColor, and isRealCharacter */ QTest::newRow("simple text with default rendition") << "hello" << QVector(6).fill(DEFAULT_RENDITION) << "hello"; QTest::newRow("simple text with bold rendition") << "hello" << QVector(6).fill(RE_BOLD) << "hello"; QTest::newRow("simple text with underline and italic rendition") << "hello" << QVector(6).fill(RE_UNDERLINE|RE_ITALIC) << "hello"; QTest::newRow("simple text with default rendition (shorten)") << "hello" << QVector(4).fill(DEFAULT_RENDITION) << "hello"; QTest::newRow("simple text with underline rendition (shorten)") << "hello" << QVector(4).fill(RE_UNDERLINE) << "hello"; } void TerminalCharacterDecoderTest::testPlainTextDecoder() { QFETCH(QString, text); QFETCH(QVector, renditions); QFETCH(QString, result); TerminalCharacterDecoder *decoder = new PlainTextDecoder(); auto testCharacters = convertToCharacter(text, renditions); QString outputString; QTextStream outputStream(&outputString); decoder->begin(&outputStream); decoder->decodeLine(testCharacters, text.size(), /* ignored */ LINE_DEFAULT); decoder->end(); QCOMPARE(outputString, result); delete[] testCharacters; delete decoder; } void TerminalCharacterDecoderTest::testHTMLDecoder_data() { QTest::addColumn("text"); QTest::addColumn>("renditions"); QTest::addColumn("result"); /* Notes: * TODO: need to add foregroundColor, backgroundColor, and isRealCharacter */ QTest::newRow("simple text with default rendition") << "hello" << QVector(6).fill(DEFAULT_RENDITION) << R"(hello
)"; QTest::newRow("simple text with bold rendition") << "hello" << QVector(6).fill(RE_BOLD) << R"(hello
)"; // The below is wrong; only the first rendition is used (eg ignores the |) QTest::newRow("simple text with underline and italic rendition") << "hello" << QVector(6).fill(RE_UNDERLINE|RE_ITALIC) << R"(hello
)"; QTest::newRow("text with &") << "hello &there" << QVector(6).fill(DEFAULT_RENDITION) << R"(hello &there
)"; } void TerminalCharacterDecoderTest::testHTMLDecoder() { QFETCH(QString, text); QFETCH(QVector, renditions); QFETCH(QString, result); TerminalCharacterDecoder *decoder = new HTMLDecoder(); auto testCharacters = convertToCharacter(text, renditions); QString outputString; QTextStream outputStream(&outputString); decoder->begin(&outputStream); decoder->decodeLine(testCharacters, text.size(), /* ignored */ LINE_DEFAULT); decoder->end(); QCOMPARE(outputString, result); delete[] testCharacters; delete decoder; } QTEST_GUILESS_MAIN(TerminalCharacterDecoderTest) diff --git a/src/autotests/TerminalCharacterDecoderTest.h b/src/autotests/TerminalCharacterDecoderTest.h index d2a498b8..97af5879 100644 --- a/src/autotests/TerminalCharacterDecoderTest.h +++ b/src/autotests/TerminalCharacterDecoderTest.h @@ -1,46 +1,46 @@ /* Copyright 2008 by Robert Knight Copyright 2018 by Kurt Hindenburg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef TERMINALCHARACTERDECODERTEST_H #define TERMINALCHARACTERDECODERTEST_H #include #include "../TerminalCharacterDecoder.h" namespace Konsole { class TerminalCharacterDecoderTest : public QObject { Q_OBJECT private Q_SLOTS: - Character* convertToCharacter(QString text, QVector renditions); + Character* convertToCharacter(const QString &text, QVector renditions); void testPlainTextDecoder(); void testPlainTextDecoder_data(); void testHTMLDecoder(); void testHTMLDecoder_data(); }; } #endif // TERMINALCHARACTERDECODERTEST_H diff --git a/src/settings/ProfileSettings.cpp b/src/settings/ProfileSettings.cpp index 12b4dab4..489fd4e9 100644 --- a/src/settings/ProfileSettings.cpp +++ b/src/settings/ProfileSettings.cpp @@ -1,505 +1,505 @@ /* 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 // KDE #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); // hide vertical header sessionTable->verticalHeader()->hide(); sessionTable->setShowGrid(false); sessionTable->setItemDelegateForColumn(FavoriteStatusColumn, new FavoriteItemDelegate(this)); sessionTable->setItemDelegateForColumn(ShortcutColumn, new ShortcutItemDelegate(this)); sessionTable->setEditTriggers(sessionTable->editTriggers() | QAbstractItemView::SelectedClicked); // double clicking the profile name opens the profile edit dialog connect(sessionTable, &QTableView::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); // resize the session table to the full width of the table sessionTable->horizontalHeader()->setHighlightSections(false); sessionTable->horizontalHeader()->setStretchLastSection(true); sessionTable->resizeColumnsToContents(); // 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()); ProfileManager::instance()->setShortcut(item->data(ShortcutRole).value(), sequence); } } -int ProfileSettings::rowForProfile(const Profile::Ptr profile) const +int ProfileSettings::rowForProfile(const Profile::Ptr &profile) const { const int rowCount = _sessionModel->rowCount(); for (int i = 0; i < rowCount; i++) { if (_sessionModel->item(i, ProfileNameColumn)->data(ProfileKeyRole) .value() == profile) { return i; } } return -1; } -void ProfileSettings::removeItems(const Profile::Ptr profile) +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) +void ProfileSettings::updateItems(const Profile::Ptr &profile) { const int row = rowForProfile(profile); if (row < 0) { return; } const auto items = QList { _sessionModel->item(row, ProfileNameColumn), _sessionModel->item(row, FavoriteStatusColumn), _sessionModel->item(row, ShortcutColumn) }; updateItemsForProfile(profile, items); } -void ProfileSettings::updateItemsForProfile(const Profile::Ptr profile, const QList& items) const +void ProfileSettings::updateItemsForProfile(const Profile::Ptr &profile, const QList& items) const { // Profile Name items[ProfileNameColumn]->setText(profile->name()); if (!profile->icon().isEmpty()) { items[ProfileNameColumn]->setIcon(QIcon::fromTheme(profile->icon())); } items[ProfileNameColumn]->setData(QVariant::fromValue(profile), ProfileKeyRole); // only allow renaming the profile from the edit profile dialog // so as to use ProfileManager::checkProfileName() items[ProfileNameColumn]->setEditable(false); // Favorite Status const auto isFavorite = ProfileManager::instance()->findFavorites().contains(profile); const auto icon = isFavorite ? QIcon::fromTheme(QStringLiteral("dialog-ok-apply")) : QIcon(); items[FavoriteStatusColumn]->setData(icon, Qt::DecorationRole); items[FavoriteStatusColumn]->setData(QVariant::fromValue(profile), ProfileKeyRole); items[FavoriteStatusColumn]->setToolTip(i18nc("@info:tooltip", "Click to toggle status")); // Shortcut const auto shortcut = ProfileManager::instance()->shortcut(profile).toString(); items[ShortcutColumn]->setText(shortcut); items[ShortcutColumn]->setData(QVariant::fromValue(profile), ShortcutRole); items[ShortcutColumn]->setToolTip(i18nc("@info:tooltip", "Double click to change shortcut")); } void ProfileSettings::doubleClicked(const QModelIndex &index) { QStandardItem *item = _sessionModel->itemFromIndex(index); if (item->column() == ProfileNameColumn) { editSelected(); } } -void ProfileSettings::addItems(const Profile::Ptr profile) +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() }; updateItemsForProfile(profile, items); _sessionModel->appendRow(items); } void ProfileSettings::populateTable() { Q_ASSERT(!sessionTable->model()); sessionTable->setModel(_sessionModel); _sessionModel->clear(); // setup session table _sessionModel->setHorizontalHeaderLabels({i18nc("@title:column Profile label", "Name"), i18nc("@title:column Display profile in file menu", "Show"), i18nc("@title:column Profile shortcut text", "Shortcut")}); QList profiles = ProfileManager::instance()->allProfiles(); ProfileManager::instance()->sortProfiles(profiles); foreach(const Profile::Ptr& profile, profiles) { addItems(profile); } updateDefaultItem(); connect(_sessionModel, &QStandardItemModel::itemChanged, this, &Konsole::ProfileSettings::itemDataChanged); // listen for changes in the table selection and update the state of the form's buttons // accordingly. // // it appears that the selection model is changed when the model itself is replaced, // so the signals need to be reconnected each time the model is updated. connect(sessionTable->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Konsole::ProfileSettings::tableSelectionChanged); sessionTable->selectRow(0); } void ProfileSettings::updateDefaultItem() { Profile::Ptr defaultProfile = ProfileManager::instance()->defaultProfile(); const int rowCount = _sessionModel->rowCount(); for (int i = 0; i < rowCount; i++) { QStandardItem* item = _sessionModel->item(i); QFont itemFont = item->font(); bool isDefault = (defaultProfile == item->data().value()); if (isDefault && !itemFont.bold()) { QIcon icon(KIconLoader::global()->loadIcon(defaultProfile->icon(), KIconLoader::Small, 0, KIconLoader::DefaultState, QStringList(QStringLiteral("emblem-favorite")))); item->setIcon(icon); itemFont.setBold(true); item->setFont(itemFont); } else if (!isDefault && itemFont.bold()) { item->setIcon(QIcon(defaultProfile->icon())); itemFont.setBold(false); item->setFont(itemFont); } } } void ProfileSettings::tableSelectionChanged(const QItemSelection&) { const int selectedRows = sessionTable->selectionModel()->selectedRows().count(); const ProfileManager* manager = ProfileManager::instance(); const bool isNotDefault = (selectedRows > 0) && currentProfile() != manager->defaultProfile(); const bool isDeletable = (selectedRows > 1) || (selectedRows == 1 && isProfileDeletable(currentProfile())); newProfileButton->setEnabled(selectedRows < 2); // FIXME: At some point editing 2+ profiles no longer works editProfileButton->setEnabled(selectedRows == 1); // do not allow the default session type to be removed deleteProfileButton->setEnabled(isDeletable && isNotDefault); setAsDefaultButton->setEnabled(isNotDefault && (selectedRows < 2)); } void ProfileSettings::deleteSelected() { foreach(const Profile::Ptr & profile, selectedProfiles()) { if (profile != ProfileManager::instance()->defaultProfile()) { ProfileManager::instance()->deleteProfile(profile); } } } void ProfileSettings::setSelectedAsDefault() { ProfileManager::instance()->setDefaultProfile(currentProfile()); // do not allow the new default session type to be removed deleteProfileButton->setEnabled(false); setAsDefaultButton->setEnabled(false); // update font of new default item updateDefaultItem(); } void ProfileSettings::moveUpSelected() { Q_ASSERT(_sessionModel); const int rowIndex = sessionTable->currentIndex().row(); const QListitems = _sessionModel->takeRow(rowIndex); _sessionModel->insertRow(rowIndex - 1, items); sessionTable->selectRow(rowIndex - 1); } void ProfileSettings::moveDownSelected() { Q_ASSERT(_sessionModel); const int rowIndex = sessionTable->currentIndex().row(); const QListitems = _sessionModel->takeRow(rowIndex); _sessionModel->insertRow(rowIndex + 1, items); sessionTable->selectRow(rowIndex + 1); } 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); newProfile->setProperty(Profile::Name, i18nc("@item This will be used as part of the file name", "New Profile")); newProfile->setProperty(Profile::UntranslatedName, QStringLiteral("New Profile")); newProfile->setProperty(Profile::MenuIndex, QStringLiteral("0")); // Consider https://blogs.kde.org/2009/03/26/how-crash-almost-every-qtkde-application-and-how-fix-it-0 before changing the below QPointer dialog = new EditProfileDialog(this); dialog.data()->setProfile(newProfile); dialog.data()->selectProfileName(); if (dialog.data()->exec() == QDialog::Accepted) { ProfileManager::instance()->addProfile(newProfile); ProfileManager::instance()->setFavorite(newProfile, true); ProfileManager::instance()->changeProfile(newProfile, newProfile->setProperties()); } delete dialog.data(); } void ProfileSettings::editSelected() { QList profiles(selectedProfiles()); foreach (Session* session, SessionManager::instance()->sessions()) { foreach (TerminalDisplay* terminal, session->views()) { // Searching for opened profiles if (terminal->sessionController()->profileDialogPointer() != nullptr) { foreach (const Profile::Ptr & profile, profiles) { if (profile->name() == terminal->sessionController()->profileDialogPointer()->lookupProfile()->name() && terminal->sessionController()->profileDialogPointer()->isVisible()) { // close opened edit dialog terminal->sessionController()->profileDialogPointer()->close(); } } } } } EditProfileDialog dialog(this); // the dialog will delete the profile group when it is destroyed ProfileGroup* group = new ProfileGroup; foreach (const Profile::Ptr & profile, profiles) { group->addProfile(profile); } group->updateValues(); dialog.setProfile(Profile::Ptr(group)); dialog.exec(); } QList ProfileSettings::selectedProfiles() const { QList list; QItemSelectionModel* selection = sessionTable->selectionModel(); if (selection == nullptr) { return list; } foreach(const QModelIndex & index, selection->selectedIndexes()) { if (index.column() == ProfileNameColumn) { list << index.data(ProfileKeyRole).value(); } } return list; } Profile::Ptr ProfileSettings::currentProfile() const { QItemSelectionModel* selection = sessionTable->selectionModel(); if ((selection == nullptr) || selection->selectedRows().count() != 1) { return Profile::Ptr(); } return selection-> selectedIndexes().first().data(ProfileKeyRole).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(Profile::Ptr profile, bool favorite) +void ProfileSettings::updateFavoriteStatus(const Profile::Ptr &profile, bool favorite) { Q_ASSERT(_sessionModel); const int rowCount = _sessionModel->rowCount(); for (int i = 0; i < rowCount; i++) { QModelIndex index = _sessionModel->index(i, FavoriteStatusColumn); if (index.data(ProfileKeyRole).value() == profile) { const QIcon icon = favorite ? QIcon::fromTheme(QStringLiteral("dialog-ok-apply")) : QIcon(); _sessionModel->setData(index, icon, Qt::DecorationRole); } } } void ProfileSettings::setShortcutEditorVisible(bool visible) { sessionTable->setColumnHidden(ShortcutColumn, !visible); } void StyledBackgroundPainter::drawBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex&) { const auto* v3option = qstyleoption_cast(&option); const QWidget* widget = v3option != nullptr ? v3option->widget : nullptr; QStyle* style = widget != nullptr ? widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, widget); } FavoriteItemDelegate::FavoriteItemDelegate(QObject* aParent) : QStyledItemDelegate(aParent) { } void FavoriteItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { // See implementation of QStyledItemDelegate::paint() QStyleOptionViewItemV4 opt = option; initStyleOption(&opt, index); StyledBackgroundPainter::drawBackground(painter, opt, index); int margin = (opt.rect.height() - opt.decorationSize.height()) / 2; margin++; opt.rect.setTop(opt.rect.top() + margin); opt.rect.setBottom(opt.rect.bottom() - margin); QIcon icon = index.data(Qt::DecorationRole).value(); icon.paint(painter, opt.rect, Qt::AlignCenter); } bool FavoriteItemDelegate::editorEvent(QEvent* aEvent, QAbstractItemModel*, const QStyleOptionViewItem&, const QModelIndex& index) { if (aEvent->type() == QEvent::MouseButtonPress || aEvent->type() == QEvent::KeyPress || aEvent->type() == QEvent::MouseButtonDblClick) { Profile::Ptr profile = index.data(ProfileSettings::ProfileKeyRole).value(); const bool isFavorite = ProfileManager::instance()->findFavorites().contains(profile); ProfileManager::instance()->setFavorite(profile, !isFavorite); } return true; } ShortcutItemDelegate::ShortcutItemDelegate(QObject* aParent) : QStyledItemDelegate(aParent), _modifiedEditors(QSet()), _itemsBeingEdited(QSet()) { } void ShortcutItemDelegate::editorModified(const QKeySequence& keys) { Q_UNUSED(keys); 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 KKeySequenceWidget(aParent); editor->setFocusPolicy(Qt::StrongFocus); editor->setModifierlessAllowed(false); QString shortcutString = index.data(Qt::DisplayRole).toString(); editor->setKeySequence(QKeySequence::fromString(shortcutString)); connect(editor, &KKeySequenceWidget::keySequenceChanged, this, &Konsole::ShortcutItemDelegate::editorModified); editor->captureKeySequence(); 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); } } diff --git a/src/settings/ProfileSettings.h b/src/settings/ProfileSettings.h index 29ce1f06..ce6f7edd 100644 --- a/src/settings/ProfileSettings.h +++ b/src/settings/ProfileSettings.h @@ -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. */ #ifndef PROFILESETTINGS_H #define PROFILESETTINGS_H // Qt #include #include // KDE // Konsole #include "Profile.h" #include "ui_ProfileSettings.h" class QItemSelection; class QShowEvent; class QStandardItem; class QStandardItemModel; namespace Konsole { /** * A dialog which lists the available types of profiles and allows * the user to add new profiles, and remove or edit existing * profile types. */ class ProfileSettings : public QWidget, private Ui::ProfileSettings { Q_OBJECT friend class FavoriteItemDelegate; friend class ShortcutItemDelegate; public: /** Constructs a new profile type with the specified parent. */ explicit ProfileSettings(QWidget *parent = nullptr); ~ProfileSettings() Q_DECL_OVERRIDE; /** * Specifies whether the shortcut editor should be show. * The shortcut editor allows shortcuts to be associated with * profiles. When a shortcut is changed, the dialog will call * SessionManager::instance()->setShortcut() to update the shortcut * associated with the profile. * * By default the editor is visible. */ void setShortcutEditorVisible(bool visible); protected: private Q_SLOTS: void slotAccepted(); void deleteSelected(); void setSelectedAsDefault(); void createProfile(); void editSelected(); void moveUpSelected(); void moveDownSelected(); void itemDataChanged(QStandardItem *item); // enables or disables Edit/Delete/Set as Default buttons when the // selection changes void tableSelectionChanged(const QItemSelection &); - void updateFavoriteStatus(Profile::Ptr profile, bool favorite); + void updateFavoriteStatus(const Profile::Ptr &profile, bool favorite); - void addItems(const Profile::Ptr); - void updateItems(const Profile::Ptr); - void removeItems(const Profile::Ptr); + void addItems(const Profile::Ptr&); + void updateItems(const Profile::Ptr&); + void removeItems(const Profile::Ptr&); // double clicking the profile name opens the edit profile dialog void doubleClicked(const QModelIndex &index); private: Profile::Ptr currentProfile() const; QList selectedProfiles() const; bool isProfileDeletable(Profile::Ptr profile) const; // updates the font of the items to match // their default / non-default profile status void updateDefaultItem(); - void updateItemsForProfile(const Profile::Ptr profile,const QList &items) const; + void updateItemsForProfile(const Profile::Ptr &profile,const QList &items) const; // updates the profile table to be in sync with the // session manager void populateTable(); - int rowForProfile(const Profile::Ptr profile) const; + int rowForProfile(const Profile::Ptr &profile) const; QStandardItemModel *_sessionModel; static const int ProfileNameColumn = 0; static const int FavoriteStatusColumn = 1; static const int ShortcutColumn = 2; static const int ProfileKeyRole = Qt::UserRole + 1; static const int ShortcutRole = Qt::UserRole + 1; }; class StyledBackgroundPainter { public: static void drawBackground(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index); }; class FavoriteItemDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit FavoriteItemDelegate(QObject *parent = nullptr); bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) Q_DECL_OVERRIDE; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; }; class ShortcutItemDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit ShortcutItemDelegate(QObject *parent = nullptr); void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const Q_DECL_OVERRIDE; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; private Q_SLOTS: void editorModified(const QKeySequence &keys); private: mutable QSet _modifiedEditors; mutable QSet _itemsBeingEdited; }; } #endif // MANAGEPROFILESDIALOG_H