diff --git a/desktop/sessionui.rc b/desktop/sessionui.rc index 287e1dea..6e6f54a6 100644 --- a/desktop/sessionui.rc +++ b/desktop/sessionui.rc @@ -1,62 +1,63 @@ - + + diff --git a/doc/manual/index.docbook b/doc/manual/index.docbook index 6671ae5e..854464ad 100644 --- a/doc/manual/index.docbook +++ b/doc/manual/index.docbook @@ -1,1781 +1,1789 @@ AhmadSamir'> a.samirh78@gmail.com'> ]> The &konsole; Handbook &Jonathan.Singer; &Jonathan.Singer.mail; &Kurt.Hindenburg; &Kurt.Hindenburg.mail; &Ahmad.Samir; &Ahmad.Samir.mail; &Robert.Knight; &Robert.Knight.mail; &Kurt.Hindenburg; &Kurt.Hindenburg.mail; &Waldo.Bastian; &Waldo.Bastian.mail; &Mike.McBride; &Mike.McBride.mail; 200020012002 &Jonathan.Singer; 200520082009201020112014201620172018 &Kurt.Hindenburg; 2018 &Ahmad.Samir; &FDLNotice; 2018-04-03 Applications 18.08 &konsole; is &kde;'s terminal emulator. KDE konsole kdebase command line terminal cli Introduction What is a terminal? &konsole; is an X terminal emulator, often referred to as a terminal or a shell. It emulates a command line interface in a text only window. &konsole; typically runs a command shell, an application that executes commands that you type. The shell the &konsole; runs depends on your account settings. Consult your operating system documentation to know what the shell is, how to configure it and how to use it. Scrollback &konsole; uses the notion of scrollback to allow users to view previously displayed output. By default, scrollback is on and set to save 1000 lines of output in addition to what is currently displayed on the screen. As lines of text scroll off the top of the screen, they can be reviewed by moving the scroll bar upwards, scrolling with a mouse wheel or through the use of the &Shift;Page Up (to move back), &Shift;Page Down (to move forward), &Shift;Up Arrow (to move up a line) and &Shift;Down Arrow (to move down a line) keys. The amount of scrolling using &Shift;Page Up/Down can be switched between half and full page in the Scrolling tab of the profile configuration window (use SettingsEdit Current Profile... to open this window). Profiles Profiles allow the user to quickly and easily automate the running of common commands. Examples could include: ssh into another machine starting an irc session use tail to watch a file All new and changed profiles are saved in the user's local home folder in $XDG_DATA_HOME/konsole. Procedure to create a new profile: Click on the menu entry SettingsManage Profiles... Click on the button New Profile.... Fill in the first entry with a name. This is the name that will show in the menu, and will be the default label instead of Shell when you start a session of this type. Enter a command just as you normally would if you opened a new shell and were going to issue that command. For our first example above, you might type ssh administration. On the other tabs of the dialog, configure this session's appearance. You can configure a different font, color scheme, $TERM type and many other settings for each session. Press the OK button. The new session is now available in the Manage Profiles... dialog. Any profiles which have Show in Menu checked will be listed by their name in the FileNew Tab menu. There will be no submenu if only the default profile is to be shown. Mouse Buttons This section details the use of the mouse buttons for the common right handed mouse button order. For the left handed mouse button order, swap left and right in the text below. Left All &LMB; clicks will be sent to a mouse-aware application running in &konsole;. If an application will react on mouse clicks, &konsole; indicates this by showing an arrow cursor. If not, an I-beam (bar) cursor is shown. Holding the &LMB; down and dragging the mouse over the screen with a mouse-unaware application running will mark a region of the text. While dragging the mouse, the marked text is displayed in reversed color for visual feedback. Select Copy from the Edit menu to copy the marked text to the clipboard for further use within &konsole; or another application. The selected text can also be dragged and dropped into compatible applications. Hold the &Ctrl; key and drag the selected text to the desired location. Normally, new-line characters are inserted at the end of each line selected. This is best for cut and paste of source code, or the output of a particular command. For ordinary text, the line breaks are often not important. One might prefer, however, for the text to be a stream of characters that will be automatically re-formatted when pasted into another application. To select in text-stream mode, hold down the &Ctrl; key while selecting normally. Pressing the &Ctrl; and &Alt; keys along with the &LMB; will select text in columns. Double-click with the &LMB; to select a word; triple-click to select an entire line. If the upper or lower edge of the text area is touched while marking, &konsole; scrolls up or down, eventually exposing text within the history buffer. The scrolling stops when the mouse stops moving. After the mouse is released, &konsole; attempts to keep the text in the clipboard visible by holding the marked area reversed. The marked area reverts back to normal as soon as the contents of the clipboard change, the text within the marked area is altered or the &LMB; is clicked. To mark text in a mouse-aware application (Midnight Commander, for example) the &Shift; key has to be pressed when clicking. Middle Pressing the &MMB; pastes text currently in the clipboard. Holding down the &Ctrl; key as you press the &MMB; pastes the text and appends a new-line. That is convenient for executing pasted command quickly, but it can be dangerous so use it with caution. If you have a mouse with only two buttons, pressing both the &LMB; and &RMB; together emulates the &MMB; of a three button mouse. If you have a wheel as the middle button, rolling it in a mouse-unaware program will move &konsole;'s scrollbar. Right These items appear in the menu when the &RMB; is pressed: Copy Paste With a text selection a submenu Search for with a list of the preferred Web Shortcuts and an option to configure web shortcuts. Open File Manager Set Encoding Clear Scrollback Adjust Scrollback... Show Menu Bar, only when the menubar is hidden Switch Profile Edit Current Profile... Close Tab In a mouse aware application, press the &Shift; key along with the &RMB; to get the popup menu. Drag and Drop If you drop a file, folder or &URL; on a &konsole; window, a context menu appears with these actions: Drag and Drop Context Menu Drag and Drop Context Menu &Shift;Move Here Move the dropped item into the current folder. This item only appears in the context menu, if you have the rights to delete the dropped file or folder. &Ctrl;Copy Here Copy the dropped item into the current folder. &Ctrl;&Shift;Link Here Insert a symbolic link to the dropped item. Paste Location Insert the full file path of the dropped item at the cursor. Change Directory To If a folder is dropped, this action appears in the context menu and allows you to change the working folder of the &konsole; session. &Esc;Cancel Break the drag and drop action. If you press the shortcuts before releasing the &LMB; during drag and drop, no context menu appears and the actions will be executed immediately. If you want to use the &Ctrl; key for drag and drop or disable the context menu to insert &URL;s as text by default, enable the corresponding options on the Mouse tab in the profile settings dialog. Command Reference The Menubar The menubar is at the top of the &konsole; window. If the menubar is hidden, Show Menu Bar can be reached by right clicking in the window (as long as no full screen application is running in that window such as vi, minicom, etc.). The default shortcut is listed after each menu item. Alternatively you can use the shortcut &Ctrl;&Shift;M to show or hide the menubar. File Menu &Ctrl;&Shift;N FileNew Window Opens a new separate &konsole; window with the default profile &Ctrl;&Shift;T FileNew Tab Opens a new tab with the default profile &konsole; ships with a default profile. Any new profiles added by the user will be listed in the submenu. There will be no submenu if only the default profile is to be shown. File Clone Tab Attempts to clone the current tab in a new tab &Ctrl;&Shift;S File Save Output As... Saves the current scrollback as a text or html file &Ctrl;&Shift;P File Print Screen ... Print the current screen. By default the output is scaled to fit the size of the paper being printed on with black text color and no background. In the print dialog these options can be changed on the Output Options tab. File Open File Manager Opens &kde;'s file manager at the current directory. By default, that is &dolphin;. &Ctrl;&Shift;W File Close Tab Closes the current tab &Ctrl;&Shift;Q File Close Window Quits &konsole; &konsole; will display a confirmation dialog if there is more than one tab open. This dialog can be disabled by clicking on the Do not ask again checkbox. If you want to get the confirmation dialog get back, delete the entry [Notification Messages] CloseAllTabs=true in $XDG_CONFIG_HOME/konsolerc. Edit Menu &Ctrl;&Shift;C Edit Copy Copies the selected text to the clipboard &Ctrl;&Shift;V EditPaste Pastes text from the clipboard at the cursor location EditSelect All Selects all the text in current window EditCopy Input ToAll Tabs in Current Window Allows input from the current session to be sent simultaneously to all sessions in current window &Ctrl;&Shift;. EditCopy Input ToSelect Tabs... Allows input from the current session to be sent simultaneously to sessions picked by user &Ctrl;&Shift;/ EditCopy Input ToNone Stop sending input from current session into other sessions Edit Send Signal Send the specified signal to the shell process, or other process, that was launched when the new session was started.Currently available signals are: STOP to stop process CONT continue if stopped HUP hangup detected on controlling terminal, or death of controlling process INT interrupt from keyboard TERM termination signal KILL kill signal USR1 user signal 1 USR2 user signal 2 Refer to your system manual pages for further details by giving the command man . &Ctrl;&Alt;S EditRename Tab... Opens a dialog box allowing you to change the name of the current tab (more info) &Ctrl;&Alt;U EditZModem Upload... Opens up a dialog to select a file to be uploaded if the required software is installed &Ctrl;&Shift;F EditFind... Opens a search bar at the bottom of &konsole;'s window This allows for case sensitive, forward or backwards, and regular expressions searches. F3 EditFind Next Moves to the next search instance . If the search bar has the focus, you can use the shortcut &Enter; as well. &Shift;F3 EditFind Previous Moves to the previous search instance . If the search bar has the focus, you can use the shortcut &Shift;&Enter; as well. View Menu &Ctrl;( ViewSplit ViewSplit View Left/Right Splits all the tabs into left and right views Any output on one view is duplicated in the other view. &Ctrl;) ViewSplit ViewSplit View Top/Bottom Splits all the tabs into top and bottom views Any output on one view is duplicated in the other view. &Ctrl;&Shift;S ViewSplit ViewClose Active Closes the current view &Ctrl;&Shift;O ViewSplit ViewClose Others Closes all non-current views &Ctrl;&Shift;] ViewSplit ViewExpand View Makes the current view larger &Ctrl;&Shift;[ ViewSplit ViewShrink View Makes the current view smaller &Ctrl;&Shift;H ViewDetach Current Tab Opens the current tab in a separate window Quiting the previous &konsole; window will not affect the newly created window. &Ctrl;&Shift;I ViewMonitor for Silence Toggles the monitoring of the current tab for lack of activity By default, after 10 seconds of inactivity, an info icon will appear on the session's tab. The type of alerts can be changed through SettingsConfigure NotificationsSilence in monitored session. &Ctrl;&Shift;A ViewMonitor for Activity Toggles the monitoring of the current tab for activity Upon any activity, an info icon will appear on the session's tab. The type of alerts can be changed through SettingsConfigure NotificationsActivity in monitored session. ViewRead-only Toggles the session to be read-only: no input is accepted, drag and drop is disabled. &Ctrl;+ ViewEnlarge Font Increases the text font size + + +&Ctrl;0 +ViewReset Font Size +Reset the text font size to the profile default + + + &Ctrl;- ViewShrink Font Decreases the text font size View Set Encoding Sets the character encoding ViewClear Scrollback Clears the text in the scrollback &Ctrl;&Shift;K ViewClear Scrollback and Reset Clears the text in the current tab and scrollback and resets the terminal Bookmarks Menu &Ctrl;&Shift;B BookmarksAdd Bookmark Adds the current location BookmarksBookmark Tabs as Folder... Adds all tabs to a bookmark folder A dialog will open for the bookmark folder name. BookmarksNew Bookmark Folder... Adds a new folder to the bookmark list A dialog will open for the bookmark folder name. BookmarksEdit Bookmarks Opens the bookmark editor The keditbookmarks program must be installed for this menu item to appear. You can use the bookmark editor to manually add URLs. Currently, &konsole; accepts the following: ssh://user@host:port telnet://user@host:port Settings Menu SettingsEdit Current Profile... Opens a dialog to configure current profile SettingsSwitch Profile Switch current profile to a listed profile SettingsManage Profiles... Opens a editor for managing profiles &Ctrl;&Shift;M SettingsShow Menu Bar Toggles the menubar being visible F11 SettingsFull Screen Mode Toggles &konsole; filling the entire screen SettingsConfigure Shortcuts... Opens the keyboard shortcut editor. More on shortcuts configuration can be found in the &kde; Fundamentals. Additionally &konsole; has a few special shortcuts with no corresponding menu item: Shortcut Description &Shift;Right Next Tab &Shift;Left Previous Tab &Ctrl;&Shift;Left Move Tab Left &Ctrl;&Shift;Right Move Tab Right &Ctrl;&Shift;Ins Paste Selection &Shift; Next View Container SettingsConfigure Notifications... Opens the notifications editor SettingsConfigure &konsole;... Opens the &konsole; settings editor This dialog has options influencing the appearance and behaviour of the TabBar and general options for the &konsole; window. Help Menu &konsole; has the some of the common &kde; Help menu items, for more information read the section about the Help Menu of the &kde; Fundamentals. &konsole; Dialogs Rename Tab Dialog The name of the current tab can be changed from this dialog. The dialog can be displayed via the menu, the shortcut &Ctrl;&Alt;S or by double-clicking on the tab in the tab bar. These changes can be made permanent by editing the current profile. &konsole; will substitute these tokens for local tabs: %n : program name %d : current directory (short) %D : current directory (long) %h : local host (short) %u : user name %w : window title set by shell %# : session number &konsole; will substitute these tokens for remote tabs: %c : current program %h : remote host (short) %H : remote host (long) %u : user name %U : user name@ (if given) %w : window title set by shell %# : session number Examples: %d : %n with /usr/src as current directory and running bash will display src : bash %D : %n with /usr/src as current directory and running top will display /usr/src : top %w (%#) with ~ as current directory and running vim in the first tab will display [No Name] (~) - VIM(1) Copy Input Dialog The text entered in one tab can simultaneously be sent to other tabs. This dialog allows you to select which tabs will get that input. The current tab will be greyed out. Adjust Scrollback Dialog The scrollback options for the history size can be changed in this dialog. Any changes are for the current tab only and will not be saved to the profile. Command-line Options When &konsole; is started from the command line, various options can be specified to modify its behavior. List various options. file Start &konsole; using the specified profile instead of the default profile. Use the internal FALLBACK profile. This option is a shortcut for FALLBACK/. dir Open with dir as the initial working directory. Do not close the initial session automatically when it ends. Create a new tab in an existing window rather than creating a new window. file Create tabs as specified in the given tabs configuration file. The file has one tab per line in the following format: Each line specifies a tab to open using up to 4 fields specifying how it is to open. Fields are delimited with ;; and a field name must have a : appended. Empty lines or lines with # at the beginning are ignored, so you can use line beginning with # to add comments. title: a name for this tab, tab default if blank or not specified workdir: working directory, ~ if blank or not specified profile: a &konsole; profile to use, the default if blank or not specified command: a command to run Each line should contain at least one of command or profile field. Example: title: %n;; command: /usr/bin/top ;; profile: Shell Start &konsole; in the background and bring to the front when &Ctrl;&Shift;F12 (by default) is pressed. Run the new instance of &konsole; in a separate process. Show the menubar, overriding the default behavior. Hide the menubar, overriding the default behavior. Show the tabbar, overriding the default behavior. Hide the tabbar, overriding the default behavior. Start &konsole; in fullscreen mode. Disable transparent backgrounds, even if the system supports them. List all available profiles. List all possible properties with name and type. See option . For more information, please visit &konsole; API Reference. property=value Change the value of a profile property. command Execute command instead of the normal shell. This option will catch all following arguments passed to &konsole;, and execute it as command. So this option should always be used as the last option. &konsole; also accepts generic &Qt; and &kf5-full; options, see man pages qt5options and kf5options. Scripting &konsole; &konsole; does support numerous methods that can be used with &DBus;. There are two ways to use the &DBus; interface: &Qt;'s &GUI; qdbusviewer and the command line qdbus. Examples: % qdbus will display all services available. % qdbus will display the &DBus; interface for &konsole;. % qdbus will display methods for controlling window 1. % qdbus will display methods for controlling the current window. % qdbus will display methods for controlling session 1. % qdbus will display methods for controlling the current session. % qdbus will display methods for controlling the current &konsole;'s session. If any of the above commands outputs: Service 'org.kde.konsole' does not exist, change to one of the following: (will select first pid) (this can be used from the current &konsole;) For more information, please visit &DBus; tutorial. Terminal Key Bindings How &konsole; Uses Key Bindings Introduction &konsole; uses *.keytab files to translate key combinations into control characters and escape sequences that are sent to the shell or to interactive programs (typically programs that use the Alternate Screen buffer, ⪚ vim, less, screen) running in the shell. Users can customize the key bindings settings in &konsole; using the Key Bindings Editor. A key combination can be configured to send a specific control or escape sequence to the terminal. You can open the Key Bindings Editor from the menu entry SettingsEdit Current Profile, and going to the Keyboard tab. Listed there are the Key Bindings schemas that come by default with &konsole;. Key Combinations and Modes Key combinations follow the pattern: Key (+|-) Modes for example: Up+Shift+AppScreen Down+Shift-AppScreen Space+Ctrl Key names are defined in the qnamespace.h header file, with the Qt::Key_ prefix removed, for a list of key names check the Qt::Key enumeration in the &Qt; documentation. A + preceding a Mode name means that mode is set; for a modifier key, that means it's pressed, whereas for all other modes it means that particular mode is in effect (&ie; active). For example +Ctrl means the key combination will work only if the &Ctrl; key is pressed. A - preceding a Mode name means that mode is reset; basically this is the opposite of putting + before a Mode name, so for a modifier key that means the key isn't pressed, whereas for all other modes it means that particular mode is inactive. For example -Ctrl means the key combination will work only if the &Ctrl; key is not pressed. If a Mode name isn't present in a key combination, its state is ignored. The supported Key Bindings modes are listed below: Alt, Ctrl, Shift One or more of these Modes can be used in a key combination, if any of them is set, the key combination uses that modifier key, respectively; and vice versa if it's reset AnyModifier If this mode is set, the key combination uses any modifier key (any of the previous three modifier keys); and vice versa if it's reset Ansi If this mode is set, &konsole; will send ANSI escape and control sequences If this mode is reset &konsole; will send VT52 escape and control sequences AppScreen If this mode is set, the key combination will only affect interactive programs that use the Alternate Screen buffer If this mode is reset the key combination will only affect the terminal when it's using the Normal Screen buffer &konsole; makes use of two screen buffers: The Normal Screen buffer (default): allows you to scroll back to view previous lines of output, this is the default buffer you usually use to execute commands... &etc; The Alternate Screen buffer: the terminal switches to this buffer when you run an interactive program (⪚ less, vim, screen, tmux... &etc;) KeyPad If this mode is set, the key combination uses a key on the Keypad (Number Pad). This mode is useful to distinguish between keys on the keyboard and keys on the Keypad. For example when Num Lock is on you can configure two separate key combinations, one using the key labelled 1 on the keyboard (usually under the F1 key) and the other using the key labelled 1 on the Keypad. The same concept applies when Num Lock is off for the End, Home, Cursor Keys ...etc on the Keypad AppCursorKeys This mode implements the VT100 Cursor Keys Mode (DECCKM). It controls the escape sequences each Cursor Key (Up, Down, Right, Left) sends, depending on whether this mode is set or reset By default &konsole; follows the XTerm behavior of treating the Home and End keys as cursor keys with respect to DECCKM AppKeyPad If this mode is set, the key combination will only work when the Keypad is in Application Mode (DECKPAM) If this mode is reset, the key combination will only work when the Keypad is in Numeric Mode (DECKPNM) NewLine If this mode is set, the Return (Enter) key on the keyboard will send both Carriage Return "\r" and New Line "\n" control characters If this mode is reset, the Return key will send only a Carriage Return "\r" The same applies to the Enter key on the Keypad This mode emulates the LNM - Line Feed/New Line Mode Note that each combination of Key and Modes (set/reset) must be unique. For example, consider the following two rules: A+Shift : A a : a &konsole; will not accept the small letter a rule, you have to add a -Shift to that rule to make it work. The Output Field In the Output field you can add the escape sequences or control characters that you want &konsole; to send to the terminal when the associated key combination is pressed. You can also use any of the following keywords, each of which has a special meaning in &konsole;: scrollUpLine : scroll up one line in the shell history scrollback buffer scrollUpPage : scroll up one page in the shell history scrollback buffer scrollDownLine : scroll down one line in the shell history scrollback buffer scrollDownPage : scroll down one page in the shell history scrollback buffer scrollUpToTop : scroll up to the begining of the shell history scrollback buffer scrollDownToBottom : scroll down to the end of the shell history scrollback buffer You can also use strings with C-string syntax; you may use the following escapes sequences: \E : Escape \\ : Backslash \" : Double quote \t : Tab \r : Carriage Return \n : New line \b : Backspace \xHH : where HH are two hex digits This can be used to send ASCII control characters, ⪚ \x00 which is the NUL character Other System Resources There are other system resources that can affect terminal Key Bindings: Consult the terminfo or termcap database for the expected escape sequences and control characters that each key combination is supposed to send. It is likely that your system has other keyboard databases which have to be in sync too, (⪚ /etc/inputrc and readline for the BASH shell) as they affect the operations (interactions) bound to key combinations. Further Reading For more information on escape sequences and control characters, check the following documentation: The VT100 user guide The VT102 user guide The comprehensive and indispensable XTerm Control Sequences documentation Using Style Sheet for the Tab Bar The default style sheet for the tab bar sets the minimum and maximum tab widths. The user can create a .css file and have &konsole; use that as the style sheet for the tab bar. In the .css file, the widget to use is QTabBar::tab. For more information, consider reading &Qt; Style Sheets Examples: Change the selected tab's background to a light gray QTabBar::tab:selected { background: #999999 } Change the selected tab's text to red QTabBar::tab:selected { color: red } All tabs will be at least 200 pixels in width QTabBar::tab { min-width: 200px } Only the selected tab will be at least 200 pixels in width QTabBar::tab::selected { min-width: 200px } Any of these can be combined in one file QTabBar::tab::selected { background: #999999; color: red; min-width: 200px; } QTabBar::tab { min-width: 100px } Did You Know?, Common Issues and More Did You Know? Pressing &Ctrl; while selecting text will cause lines breaks to be converted to spaces when pasted. Pressing the &Ctrl;&Alt; keys while selecting text will select columns. The &Ctrl;Wheel combination will zoom text size, like in konqueror and firefox. When a program evaluates either mouse button, pressing the &Shift; key will allow the popup menu to appear. The &Ctrl;&Shift;F10 shortcut will activate the menu. The &Shift;Insert keys will insert the clipboard. Double-clicking will select a whole word. Continuing to hold the mouse button and moving the mouse will extend the selection. Triple-clicking will select a whole line. Continuing to hold the mouse button and moving the mouse will extend the selection. There is a hidden feature for the "%d" formatter in tab title. You can tell &konsole; to abbreviate a directory name into its first character. For example, "/path/to/konsole/src" can be abbreviated into "konsole/s". If you want to enable and control this hidden feature, open konsolerc in qtpaths and add following lines: [ProcessInfo] CommonDirNames=name1,name2,name3... If you are using Yakuake, you need to edit yakuakerc in qtpaths instead. Common Issues Some fonts might be unavailable for usage in &konsole;, although they are available in other applications. That doesn't mean there is a bug in &konsole;. &konsole; requires monospaced fonts to provide the best visual result, so it asks &Qt; to only list monospaced fonts. Starting with version 16.08 (August 2016), &konsole; can be configured to allow selecting any font with the caveat that the display may not be correct. Since KDE4 all the tabs use the same process ID. This has the side-effect that if one tab's process has issues, all the other tabs may experience issues as well. This is most noticeable when a command that connects to an external device or system (ssh, nfs) has issues. &konsole; treats arguments after the option as one command and runs it directly, instead of parsing it and possibly dividing it into sub-commands for execution. This is different from xterm. konsole -e "command1 ; command2" does not work konsole -e $SHELL -c "command1 ; command2" works &konsole; doesn't provide convenience for running login shell, because developers don't like the idea of running login shell in a terminal emulator. Of course, users still can run login shell in &konsole; if they really need to. Edit the profile in use and modify its command to the form of starting a login shell explicitly, such as "bash -l" and "zsh -l". The option sometimes behaves strangely. It may create new window, or it may create new tab in another existing &konsole; window instead of the current &konsole; window. Those behaviors feel strange, but they are not necessarily bugs. The option tries to reuse existing &konsole; windows, but not all &konsole; windows are reusable. All &konsole; windows opened through &krunner; are reusable, while most &konsole; windows opened from command line are not. Credits and Copyright &konsole; is currently maintained by &Kurt.Hindenburg; &Kurt.Hindenburg.mail; Previous &konsole; maintainers include: &Robert.Knight; &Robert.Knight.mail; and &Waldo.Bastian; &Waldo.Bastian.mail; The application &konsole; Copyright © 1997-2008 &Lars.Doelle; &Lars.Doelle.mail; This document was originally written by &Jonathan.Singer; &Jonathan.Singer.mail; This document was updated for &kde; 4.x by &Kurt.Hindenburg; &Kurt.Hindenburg.mail; This document was updated for &kde; 3.4 by &Kurt.Hindenburg; &Kurt.Hindenburg.mail; Originally converted to DocBook SGML by &Mike.McBride; and &Lauri.Watts; &underFDL; &underGPL; Links For more information please visit these websites: &konsole;'s homepage on &kde;'s UserBase  &konsole;'s homepage &konsole;'s mailing list &kde; on FreeBSD &kde; on &Solaris; &documentation.index; diff --git a/src/SessionController.cpp b/src/SessionController.cpp index 5cff9c4a..9a96561a 100644 --- a/src/SessionController.cpp +++ b/src/SessionController.cpp @@ -1,2118 +1,2125 @@ /* Copyright 2006-2008 by Robert Knight Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "SessionController.h" #include "ProfileManager.h" #include "konsoledebug.h" // Qt #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Konsole #include "EditProfileDialog.h" #include "CopyInputDialog.h" #include "Emulation.h" #include "Filter.h" #include "History.h" #include "HistorySizeDialog.h" #include "IncrementalSearchBar.h" #include "RenameTabDialog.h" #include "ScreenWindow.h" #include "Session.h" #include "ProfileList.h" #include "TerminalDisplay.h" #include "SessionManager.h" #include "Enumeration.h" #include "PrintOptions.h" // for SaveHistoryTask #include #include #include "TerminalCharacterDecoder.h" // For Unix signal names #include using namespace Konsole; // TODO - Replace the icon choices below when suitable icons for silence and // activity are available Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _activityIcon, (QIcon::fromTheme(QLatin1String("dialog-information")))) Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _silenceIcon, (QIcon::fromTheme(QLatin1String("dialog-information")))) Q_GLOBAL_STATIC_WITH_ARGS(QIcon, _broadcastIcon, (QIcon::fromTheme(QLatin1String("emblem-important")))) QSet SessionController::_allControllers; int SessionController::_lastControllerId; SessionController::SessionController(Session* session , TerminalDisplay* view, QObject* parent) : ViewProperties(parent) , KXMLGUIClient() , _session(session) , _view(view) , _copyToGroup(nullptr) , _profileList(nullptr) , _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::keyPressedSignal, this, &Konsole::SessionController::interactionHandler); // take a snapshot of the session state periodically in the background auto backgroundTimer = new QTimer(_session); backgroundTimer->setSingleShot(false); backgroundTimer->setInterval(2000); connect(backgroundTimer, &QTimer::timeout, this, &Konsole::SessionController::snapshot); backgroundTimer->start(); // xterm '11;?' request connect(_session.data(), &Konsole::Session::getBackgroundColor, this, &Konsole::SessionController::sendBackgroundColor); _allControllers.insert(this); // A list of programs that accept Ctrl+C to clear command line used // before outputting bookmark. _bookmarkValidProgramsToClear << QStringLiteral("bash") << QStringLiteral("fish") << QStringLiteral("sh"); _bookmarkValidProgramsToClear << QStringLiteral("tcsh") << QStringLiteral("zsh"); setupSearchBar(); _searchBar->setVisible(_isSearchBarEnabled); } SessionController::~SessionController() { 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() { QAction * action = qobject_cast(sender()); if (action == nullptr) { return; } KUriFilterData filterData(action->data().toString()); if (KUriFilter::self()->filterUri(filterData, QStringList() << QStringLiteral("kurisearchfilter"))) { const QUrl& url = filterData.uri(); new KRun(url, QApplication::activeWindow()); } } void SessionController::configureWebShortcuts() { KToolInvocation::kdeinitExec(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts")); } void SessionController::sendSignal(QAction* action) { const int signal = action->data().value(); _session->sendSignal(signal); } void SessionController::sendBackgroundColor() { const QColor c = _view->getBackgroundColor(); _session->reportBackgroundColor(c); } void SessionController::toggleReadOnly() { QAction *action = qobject_cast(sender()); if (action != nullptr) { bool readonly = !isReadOnly(); _session->setReadOnly(readonly); } } bool SessionController::eventFilter(QObject* watched , QEvent* event) { if (event->type() == QEvent::FocusIn && watched == _view) { // notify the world that the view associated with this session has been focused // used by the view manager to update the title of the MainWindow widget containing the view emit focused(this); // when the view is focused, set bell events from the associated session to be delivered // by the focused view // first, disconnect any other views which are listening for bell signals from the session disconnect(_session.data(), &Konsole::Session::bellRequest, nullptr, nullptr); // second, connect the newly focused view to listen for the session's bell signal connect(_session.data(), &Konsole::Session::bellRequest, _view.data(), &Konsole::TerminalDisplay::bell); if ((_copyInputToAllTabsAction != nullptr) && _copyInputToAllTabsAction->isChecked()) { // A session with "Copy To All Tabs" has come into focus: // Ensure that newly created sessions are included in _copyToGroup! copyInputToAllTabs(); } } return Konsole::ViewProperties::eventFilter(watched, event); } void SessionController::removeSearchFilter() { if (_searchFilter == nullptr) { return; } _view->filterChain()->removeFilter(_searchFilter); delete _searchFilter; _searchFilter = nullptr; } void SessionController::setupSearchBar() { connect(_searchBar.data(), &Konsole::IncrementalSearchBar::unhandledMovementKeyPressed, this, &Konsole::SessionController::movementKeyFromSearchBarReceived); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::closeClicked, this, &Konsole::SessionController::searchClosed); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::searchFromClicked, this, &Konsole::SessionController::searchFrom); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::findNextClicked, this, &Konsole::SessionController::findNextInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::findPreviousClicked, this, &Konsole::SessionController::findPreviousInHistory); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::highlightMatchesToggled , this , &Konsole::SessionController::highlightMatches); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::matchCaseToggled, this, &Konsole::SessionController::changeSearchMatch); connect(_searchBar.data(), &Konsole::IncrementalSearchBar::matchRegExpToggled, this, &Konsole::SessionController::changeSearchMatch); } void SessionController::setShowMenuAction(QAction* action) { _showMenuAction = action; } void SessionController::setupCommonActions() { KActionCollection* collection = actionCollection(); // Close Session QAction* action = collection->addAction(QStringLiteral("close-session"), this, SLOT(closeSession())); if (isKonsolePart()) { action->setText(i18n("&Close Session")); } else { action->setText(i18n("&Close Tab")); } action->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_W); // Open Browser action = collection->addAction(QStringLiteral("open-browser"), this, SLOT(openBrowser())); action->setText(i18n("Open File Manager")); action->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager"))); // Copy and Paste action = KStandardAction::copy(this, SLOT(copy()), collection); #ifdef Q_OS_MACOS // Don't use the Konsole::ACCEL const here, we really want the Command key (Qt::META) // TODO: check what happens if we leave it to Qt to assign the default? collection->setDefaultShortcut(action, Qt::META + Qt::Key_C); #else collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_C); #endif // disabled at first, since nothing has been selected now action->setEnabled(false); action = KStandardAction::paste(this, SLOT(paste()), collection); QList pasteShortcut; #ifdef Q_OS_MACOS pasteShortcut.append(QKeySequence(Qt::META + Qt::Key_V)); // No Insert key on Mac keyboards #else pasteShortcut.append(QKeySequence(Konsole::ACCEL + Qt::SHIFT + Qt::Key_V)); pasteShortcut.append(QKeySequence(Qt::SHIFT + Qt::Key_Insert)); #endif collection->setDefaultShortcuts(action, pasteShortcut); action = collection->addAction(QStringLiteral("paste-selection"), this, SLOT(pasteFromX11Selection())); action->setText(i18n("Paste Selection")); #ifdef Q_OS_MACOS collection->setDefaultShortcut(action, Qt::META + Qt::SHIFT + Qt::Key_V); #else collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Insert); #endif _webSearchMenu = new KActionMenu(i18n("Web Search"), this); _webSearchMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts"))); _webSearchMenu->setVisible(false); collection->addAction(QStringLiteral("web-search"), _webSearchMenu); action = collection->addAction(QStringLiteral("select-all"), this, SLOT(selectAll())); action->setText(i18n("&Select All")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all"))); action = collection->addAction(QStringLiteral("select-line"), this, SLOT(selectLine())); action->setText(i18n("Select &Line")); action = KStandardAction::saveAs(this, SLOT(saveHistory()), collection); action->setText(i18n("Save Output &As...")); #ifdef Q_OS_MACOS action->setShortcut(QKeySequence(Qt::META + Qt::Key_S)); #endif action = KStandardAction::print(this, SLOT(print_screen()), collection); action->setText(i18n("&Print Screen...")); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_P); action = collection->addAction(QStringLiteral("adjust-history"), this, SLOT(showHistoryOptions())); action->setText(i18n("Adjust Scrollback...")); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); action = collection->addAction(QStringLiteral("clear-history"), this, SLOT(clearHistory())); action->setText(i18n("Clear Scrollback")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); action = collection->addAction(QStringLiteral("clear-history-and-reset"), this, SLOT(clearHistoryAndReset())); action->setText(i18n("Clear Scrollback and Reset")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::SHIFT + Qt::Key_K); // Profile Options action = collection->addAction(QStringLiteral("edit-current-profile"), this, SLOT(editCurrentProfile())); action->setText(i18n("Edit Current Profile...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); _switchProfileMenu = new KActionMenu(i18n("Switch Profile"), this); collection->addAction(QStringLiteral("switch-profile"), _switchProfileMenu); connect(_switchProfileMenu->menu(), &QMenu::aboutToShow, this, &Konsole::SessionController::prepareSwitchProfileMenu); // 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 KToggleAction* copyInputToAllTabsAction = collection->add(QStringLiteral("copy-input-to-all-tabs")); copyInputToAllTabsAction->setText(i18n("&All Tabs in Current Window")); copyInputToAllTabsAction->setData(CopyInputToAllTabsMode); // this action is also used in other place, so remember it _copyInputToAllTabsAction = copyInputToAllTabsAction; // Copy input to ==> selected tabs KToggleAction* copyInputToSelectedTabsAction = collection->add(QStringLiteral("copy-input-to-selected-tabs")); copyInputToSelectedTabsAction->setText(i18n("&Select Tabs...")); collection->setDefaultShortcut(copyInputToSelectedTabsAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Period); copyInputToSelectedTabsAction->setData(CopyInputToSelectedTabsMode); // Copy input to ==> none KToggleAction* copyInputToNoneAction = collection->add(QStringLiteral("copy-input-to-none")); copyInputToNoneAction->setText(i18nc("@action:inmenu Do not select any tabs", "&None")); collection->setDefaultShortcut(copyInputToNoneAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_Slash); copyInputToNoneAction->setData(CopyInputToNoneMode); copyInputToNoneAction->setChecked(true); // the default state // The "Copy Input To" submenu // The above three choices are represented as combo boxes KSelectAction* copyInputActions = collection->add(QStringLiteral("copy-input-to")); copyInputActions->setText(i18n("Copy Input To")); copyInputActions->addAction(copyInputToAllTabsAction); copyInputActions->addAction(copyInputToSelectedTabsAction); copyInputActions->addAction(copyInputToNoneAction); connect(copyInputActions, static_cast(&KSelectAction::triggered), this, &Konsole::SessionController::copyInputActionsTriggered); action = collection->addAction(QStringLiteral("zmodem-upload"), this, SLOT(zmodemUpload())); action->setText(i18n("&ZModem Upload...")); action->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::ALT + Qt::Key_U); // Monitor KToggleAction* toggleAction = new KToggleAction(i18n("Monitor for &Activity"), this); collection->setDefaultShortcut(toggleAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_A); action = collection->addAction(QStringLiteral("monitor-activity"), toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorActivity); toggleAction = new KToggleAction(i18n("Monitor for &Silence"), this); collection->setDefaultShortcut(toggleAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_I); action = collection->addAction(QStringLiteral("monitor-silence"), toggleAction); connect(action, &QAction::toggled, this, &Konsole::SessionController::monitorSilence); // Text Size action = collection->addAction(QStringLiteral("enlarge-font"), this, SLOT(increaseFontSize())); action->setText(i18n("Enlarge Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-more"))); QList enlargeFontShortcut; enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL + Qt::Key_Plus)); enlargeFontShortcut.append(QKeySequence(Konsole::ACCEL + Qt::Key_Equal)); collection->setDefaultShortcuts(action, enlargeFontShortcut); action = collection->addAction(QStringLiteral("shrink-font"), this, SLOT(decreaseFontSize())); action->setText(i18n("Shrink Font")); action->setIcon(QIcon::fromTheme(QStringLiteral("format-font-size-less"))); collection->setDefaultShortcut(action, Konsole::ACCEL + Qt::Key_Minus); + 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 KSelectAction* sendSignalActions = collection->add(QStringLiteral("send-signal")); sendSignalActions->setText(i18n("Send Signal")); connect(sendSignalActions, static_cast(&KSelectAction::triggered), this, &Konsole::SessionController::sendSignal); action = collection->addAction(QStringLiteral("sigstop-signal")); action->setText(i18n("&Suspend Task") + QStringLiteral(" (STOP)")); action->setData(SIGSTOP); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigcont-signal")); action->setText(i18n("&Continue Task") + QStringLiteral(" (CONT)")); action->setData(SIGCONT); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sighup-signal")); action->setText(i18n("&Hangup") + QStringLiteral(" (HUP)")); action->setData(SIGHUP); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigint-signal")); action->setText(i18n("&Interrupt Task") + QStringLiteral(" (INT)")); action->setData(SIGINT); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigterm-signal")); action->setText(i18n("&Terminate Task") + QStringLiteral(" (TERM)")); action->setData(SIGTERM); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigkill-signal")); action->setText(i18n("&Kill Task") + QStringLiteral(" (KILL)")); action->setData(SIGKILL); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigusr1-signal")); action->setText(i18n("User Signal &1") + QStringLiteral(" (USR1)")); action->setData(SIGUSR1); sendSignalActions->addAction(action); action = collection->addAction(QStringLiteral("sigusr2-signal")); action->setText(i18n("User Signal &2") + QStringLiteral(" (USR2)")); action->setData(SIGUSR2); sendSignalActions->addAction(action); #ifdef Q_OS_MACOS collection->setDefaultShortcut(_findAction, Qt::META + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::META + Qt::Key_G); collection->setDefaultShortcut(_findPreviousAction, Qt::META + Qt::SHIFT + Qt::Key_G); #else collection->setDefaultShortcut(_findAction, Konsole::ACCEL + Qt::SHIFT + Qt::Key_F); collection->setDefaultShortcut(_findNextAction, Qt::Key_F3); collection->setDefaultShortcut(_findPreviousAction, Qt::SHIFT + Qt::Key_F3); #endif } void SessionController::switchProfile(Profile::Ptr profile) { SessionManager::instance()->setSessionProfile(_session, profile); updateFilterList(profile); } void SessionController::prepareSwitchProfileMenu() { if (_switchProfileMenu->menu()->isEmpty()) { _profileList = new ProfileList(false, this); connect(_profileList, &Konsole::ProfileList::profileSelected, this, &Konsole::SessionController::switchProfile); } _switchProfileMenu->menu()->clear(); _switchProfileMenu->menu()->addActions(_profileList->actions()); } void SessionController::updateCodecAction() { _codecAction->setCurrentCodec(QString::fromUtf8(_session->codec())); } void SessionController::changeCodec(QTextCodec* codec) { _session->setCodec(codec); } EditProfileDialog* SessionController::profileDialogPointer() { return _editProfileDialog.data(); } void SessionController::editCurrentProfile() { // Searching for Edit profile dialog opened with the same profile const QList allSessionsControllers = _allControllers.values(); foreach (SessionController* session, allSessionsControllers) { if ((session->profileDialogPointer() != nullptr) && session->profileDialogPointer()->isVisible() && session->profileDialogPointer()->lookupProfile() == SessionManager::instance()->sessionProfile(_session)) { session->profileDialogPointer()->close(); } } // NOTE bug311270: For to prevent the crash, the profile must be reset. if (!_editProfileDialog.isNull()) { // exists but not visible delete _editProfileDialog.data(); } _editProfileDialog = new EditProfileDialog(QApplication::activeWindow()); _editProfileDialog.data()->setProfile(SessionManager::instance()->sessionProfile(_session)); _editProfileDialog.data()->show(); } void SessionController::renameSession() { 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 KXmlGuiWindow* window = qobject_cast(object); if (window != nullptr) { return(window); } object = object->parent(); } return(nullptr); } static bool hasTerminalDisplayInSameWindow(const Session* session, const KXmlGuiWindow* window) { // Iterate all TerminalDisplays of this Session ... foreach(const TerminalDisplay* terminalDisplay, session->views()) { // ... and check whether a TerminalDisplay has the same // window as given in the parameter if (window == findWindow(terminalDisplay)) { return(true); } } return(false); } void SessionController::copyInputActionsTriggered(QAction* action) { const int mode = action->data().value(); switch (mode) { case CopyInputToAllTabsMode: copyInputToAllTabs(); break; case CopyInputToSelectedTabsMode: copyInputToSelectedTabs(); break; case CopyInputToNoneMode: copyInputToNone(); break; default: Q_ASSERT(false); } } void SessionController::copyInputToAllTabs() { if (_copyToGroup == nullptr) { _copyToGroup = new SessionGroup(this); } // Find our window ... const KXmlGuiWindow* myWindow = findWindow(_view); QSet group = QSet::fromList(SessionManager::instance()->sessions()); for (auto session : group) { // First, ensure that the session is removed // (necessary to avoid duplicates on addSession()!) _copyToGroup->removeSession(session); // Add current session if it is displayed our window if (hasTerminalDisplayInSameWindow(session, myWindow)) { _copyToGroup->addSession(session); } } _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); snapshot(); } void SessionController::copyInputToSelectedTabs() { if (_copyToGroup == nullptr) { _copyToGroup = new SessionGroup(this); _copyToGroup->addSession(_session); _copyToGroup->setMasterStatus(_session, true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); } QPointer dialog = new CopyInputDialog(_view); dialog->setMasterSession(_session); QSet currentGroup = QSet::fromList(_copyToGroup->sessions()); currentGroup.remove(_session); dialog->setChosenSessions(currentGroup); QPointer guard(_session); int result = dialog->exec(); if (guard.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() { // 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()) { _sessionIconName = _session->iconName(); _sessionIcon = QIcon::fromTheme(_sessionIconName); updateSessionIcon(); } QString title = _session->title(Session::DisplayedTitleRole); // special handling for the "%w" marker which is replaced with the // window title set by the shell title.replace(QLatin1String("%w"), _session->userTitle()); // special handling for the "%#" marker which is replaced with the // number of the shell title.replace(QLatin1String("%#"), QString::number(_session->sessionId())); if (title.isEmpty()) { title = _session->title(Session::NameRole); } setTitle(title); emit rawTitleChanged(); } void SessionController::sessionReadOnlyChanged() { // Trigger icon update sessionAttributeChanged(); updateReadOnlyActionStates(); // Update all views for (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 == NOTIFYNORMAL) { if (_sessionIconName != _session->iconName()) { _sessionIconName = _session->iconName(); _sessionIcon = QIcon::fromTheme(_sessionIconName); } updateSessionIcon(); } _previousState = state; } void SessionController::zmodemDownload() { QString zmodem = QStandardPaths::findExecutable(QStringLiteral("rz")); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable(QStringLiteral("lrz")); } if (!zmodem.isEmpty()) { const QString path = QFileDialog::getExistingDirectory(_view, i18n("Save ZModem Download to..."), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (!path.isEmpty()) { _session->startZModem(zmodem, path, QStringList()); return; } } else { KMessageBox::error(_view, i18n("

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

" "

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

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

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

")); return; } _session->setZModemBusy(true); QString zmodem = QStandardPaths::findExecutable(QStringLiteral("sz")); if (zmodem.isEmpty()) { zmodem = QStandardPaths::findExecutable(QStringLiteral("lsz")); } if (zmodem.isEmpty()) { KMessageBox::sorry(_view, i18n("

No suitable ZModem software was found on this system.

" "

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

")); return; } QStringList files = QFileDialog::getOpenFileNames(_view, i18n("Select Files for ZModem Upload"), QDir::homePath()); if (!files.isEmpty()) { _session->startZModem(zmodem, QString(), files); } } bool SessionController::isKonsolePart() const { // Check to see if we are being called from Konsole or a KPart return !(qApp->applicationName() == QLatin1String("konsole")); } SessionTask::SessionTask(QObject* parent) : QObject(parent) , _autoDelete(false) { } void SessionTask::setAutoDelete(bool enable) { _autoDelete = enable; } bool SessionTask::autoDelete() const { return _autoDelete; } void SessionTask::addSession(Session* session) { _sessions << session; } QList SessionTask::sessions() const { return _sessions; } SaveHistoryTask::SaveHistoryTask(QObject* parent) : SessionTask(parent) { } SaveHistoryTask::~SaveHistoryTask() = default; void SaveHistoryTask::execute() { // TODO - think about the UI when saving multiple history sessions, if there are more than two or // three then providing a URL for each one will be tedious // TODO - show a warning ( preferably passive ) if saving the history output fails QFileDialog* dialog = new QFileDialog(QApplication::activeWindow(), QString(), QDir::homePath()); dialog->setAcceptMode(QFileDialog::AcceptSave); QStringList mimeTypes; mimeTypes << QStringLiteral("text/plain"); mimeTypes << QStringLiteral("text/html"); dialog->setMimeTypeFilters(mimeTypes); // iterate over each session in the task and display a dialog to allow the user to choose where // to save that session's history. // then start a KIO job to transfer the data from the history to the chosen URL foreach(const SessionPtr& session, sessions()) { dialog->setWindowTitle(i18n("Save Output From %1", session->title(Session::NameRole))); int result = dialog->exec(); if (result != QDialog::Accepted) { continue; } QUrl url = (dialog->selectedUrls()).at(0); if (!url.isValid()) { // UI: Can we make this friendlier? KMessageBox::sorry(nullptr , i18n("%1 is an invalid URL, the output could not be saved.", url.url())); continue; } KIO::TransferJob* job = KIO::put(url, -1, // no special permissions // overwrite existing files // do not resume an existing transfer // show progress information only for remote // URLs KIO::Overwrite | (url.isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags) // a better solution would be to show progress // information after a certain period of time // instead, since the overall speed of transfer // depends on factors other than just the protocol // used ); SaveJob jobInfo; jobInfo.session = session; jobInfo.lastLineFetched = -1; // when each request for data comes in from the KIO subsystem // lastLineFetched is used to keep track of how much of the history // has already been sent, and where the next request should continue // from. // this is set to -1 to indicate the job has just been started if (((dialog->selectedNameFilter()).contains(QLatin1String("html"), Qt::CaseInsensitive)) || ((dialog->selectedFiles()).at(0).endsWith(QLatin1String("html"), Qt::CaseInsensitive))) { jobInfo.decoder = new HTMLDecoder(); } else { jobInfo.decoder = new PlainTextDecoder(); } _jobSession.insert(job, jobInfo); connect(job, &KIO::TransferJob::dataReq, this, &Konsole::SaveHistoryTask::jobDataRequested); connect(job, &KIO::TransferJob::result, this, &Konsole::SaveHistoryTask::jobResult); } dialog->deleteLater(); } void SaveHistoryTask::jobDataRequested(KIO::Job* job , QByteArray& data) { // TODO - Report progress information for the job // PERFORMANCE: Do some tests and tweak this value to get faster saving const int LINES_PER_REQUEST = 500; SaveJob& info = _jobSession[job]; // transfer LINES_PER_REQUEST lines from the session's history // to the save location if (!info.session.isNull()) { // note: when retrieving lines from the emulation, // the first line is at index 0. int sessionLines = info.session->emulation()->lineCount(); if (sessionLines - 1 == info.lastLineFetched) { return; // if there is no more data to transfer then stop the job } int copyUpToLine = qMin(info.lastLineFetched + LINES_PER_REQUEST , sessionLines - 1); QTextStream stream(&data, QIODevice::ReadWrite); info.decoder->begin(&stream); info.session->emulation()->writeToStream(info.decoder , info.lastLineFetched + 1 , copyUpToLine); info.decoder->end(); info.lastLineFetched = copyUpToLine; } } void SaveHistoryTask::jobResult(KJob* job) { if (job->error() != 0) { KMessageBox::sorry(nullptr , i18n("A problem occurred when saving the output.\n%1", job->errorString())); } TerminalCharacterDecoder * decoder = _jobSession[job].decoder; _jobSession.remove(job); delete decoder; // notify the world that the task is done emit completed(true); if (autoDelete()) { deleteLater(); } } void SearchHistoryTask::addScreenWindow(Session* session , ScreenWindow* searchWindow) { _windows.insert(session, searchWindow); } void SearchHistoryTask::execute() { QMapIterator< SessionPtr , ScreenWindowPtr > iter(_windows); while (iter.hasNext()) { iter.next(); executeOnScreenWindow(iter.key() , iter.value()); } } void SearchHistoryTask::executeOnScreenWindow(SessionPtr session , ScreenWindowPtr window) { Q_ASSERT(session); Q_ASSERT(window); Emulation* emulation = session->emulation(); if (!_regExp.pattern().isEmpty()) { int pos = -1; const bool forwards = (_direction == Enum::ForwardsSearch); const int lastLine = window->lineCount() - 1; int startLine; if (forwards && (_startLine == lastLine)) { startLine = 0; } else if (!forwards && (_startLine == 0)) { startLine = lastLine; } else { startLine = _startLine + (forwards ? 1 : -1); } QString string; //text stream to read history into string for pattern or regular expression searching QTextStream searchStream(&string); PlainTextDecoder decoder; decoder.setRecordLinePositions(true); //setup first and last lines depending on search direction int line = startLine; //read through and search history in blocks of 10K lines. //this balances the need to retrieve lots of data from the history each time //(for efficient searching) //without using silly amounts of memory if the history is very large. const int maxDelta = qMin(window->lineCount(), 10000); int delta = forwards ? maxDelta : -maxDelta; int endLine = line; bool hasWrapped = false; // set to true when we reach the top/bottom // of the output and continue from the other // end //loop through history in blocks of lines. do { // ensure that application does not appear to hang // if searching through a lengthy output QApplication::processEvents(); // calculate lines to search in this iteration if (hasWrapped) { if (endLine == lastLine) { line = 0; } else if (endLine == 0) { line = lastLine; } endLine += delta; if (forwards) { endLine = qMin(startLine , endLine); } else { endLine = qMax(startLine , endLine); } } else { endLine += delta; if (endLine > lastLine) { hasWrapped = true; endLine = lastLine; } else if (endLine < 0) { hasWrapped = true; endLine = 0; } } decoder.begin(&searchStream); emulation->writeToStream(&decoder, qMin(endLine, line) , qMax(endLine, line)); decoder.end(); // line number search below assumes that the buffer ends with a new-line string.append(QLatin1Char('\n')); if (forwards) { pos = string.indexOf(_regExp); } else { pos = string.lastIndexOf(_regExp); } //if a match is found, position the cursor on that line and update the screen if (pos != -1) { int newLines = 0; QList linePositions = decoder.linePositions(); while (newLines < linePositions.count() && linePositions[newLines] <= pos) { newLines++; } // ignore the new line at the start of the buffer newLines--; int findPos = qMin(line, endLine) + newLines; highlightResult(window, findPos); emit completed(true); return; } //clear the current block of text and move to the next one string.clear(); line = endLine; } while (startLine != endLine); // if no match was found, clear selection to indicate this window->clearSelection(); window->notifyOutputChanged(); } emit completed(false); } void SearchHistoryTask::highlightResult(ScreenWindowPtr window , int findPos) { //work out how many lines into the current block of text the search result was found //- looks a little painful, but it only has to be done once per search. ////qDebug() << "Found result at line " << findPos; //update display to show area of history containing selection if ((findPos < window->currentLine()) || (findPos >= (window->currentLine() + window->windowLines()))) { int centeredScrollPos = findPos - window->windowLines() / 2; if (centeredScrollPos < 0) { centeredScrollPos = 0; } window->scrollTo(centeredScrollPos); } window->setTrackOutput(false); window->notifyOutputChanged(); window->setCurrentResultLine(findPos); } SearchHistoryTask::SearchHistoryTask(QObject* parent) : SessionTask(parent) , _direction(Enum::BackwardsSearch) , _startLine(0) { } void SearchHistoryTask::setSearchDirection(Enum::SearchDirection direction) { _direction = direction; } void SearchHistoryTask::setStartLine(int line) { _startLine = line; } Enum::SearchDirection SearchHistoryTask::searchDirection() const { return _direction; } void SearchHistoryTask::setRegExp(const QRegularExpression &expression) { _regExp = expression; } QRegularExpression SearchHistoryTask::regExp() const { return _regExp; } QString SessionController::userTitle() const { if (!_session.isNull()) { return _session->userTitle(); } else { return QString(); } } - diff --git a/src/SessionController.h b/src/SessionController.h index 054d48e6..298ad3f2 100644 --- a/src/SessionController.h +++ b/src/SessionController.h @@ -1,533 +1,536 @@ /* Copyright 2006-2008 by Robert Knight Copyright 2009 by Thomas Dreibholz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef SESSIONCONTROLLER_H #define SESSIONCONTROLLER_H // Qt #include #include #include #include #include #include // KDE #include // Konsole #include "ViewProperties.h" #include "Profile.h" #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; // SaveHistoryTask class TerminalCharacterDecoder; typedef QPointer SessionPtr; /** * Provides the menu actions to manipulate a single terminal session and view pair. * The actions provided by this class are defined in the sessionui.rc XML file. * * SessionController monitors the session and provides access to basic information * about the session such as title(), icon() and currentDir(). SessionController * provides notifications of activity in the session via the activity() signal. * * When the controlled view receives the focus, the focused() signal is emitted * with a pointer to the controller. This can be used by main application window * which contains the view to plug the controller's actions into the menu when * the view is focused. */ class KONSOLEPRIVATE_EXPORT SessionController : public ViewProperties, public KXMLGUIClient { Q_OBJECT public: enum CopyInputToEnum { /** Copy keyboard input to all the other tabs in current window */ CopyInputToAllTabsMode = 0, /** Copy keyboard input to user selected tabs in current window */ CopyInputToSelectedTabsMode = 1, /** Do not copy keyboard input to other tabs */ CopyInputToNoneMode = 2 }; /** * Constructs a new SessionController which operates on @p session and @p view. */ SessionController(Session *session, TerminalDisplay *view, QObject *parent); ~SessionController() 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(); + 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 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 searchClosed(); // called when the user clicks on the // history search bar's close button void updateFilterList(Profile::Ptr profile); // Called when the profile has changed, so we might need to change the list of filters void interactionHandler(); void snapshot(); // called periodically as the user types // to take a snapshot of the state of the // foreground process in the terminal void highlightMatches(bool highlight); void scrollBackOptionsChanged(int mode, int lines); void sessionResizeRequest(const QSize &size); void trackOutput(QKeyEvent *event); // move view to end of current output // when a key press occurs in the // display area void updateSearchFilter(); void zmodemDownload(); void zmodemUpload(); // 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(); } /** * Abstract class representing a task which can be performed on a group of sessions. * * Create a new instance of the appropriate sub-class for the task you want to perform and * call the addSession() method to add each session which needs to be processed. * * Finally, call the execute() method to perform the sub-class specific action on each * of the sessions. */ class SessionTask : public QObject { Q_OBJECT public: explicit SessionTask(QObject *parent = nullptr); /** * Sets whether the task automatically deletes itself when the task has been finished. * Depending on whether the task operates synchronously or asynchronously, the deletion * may be scheduled immediately after execute() returns or it may happen some time later. */ void setAutoDelete(bool enable); /** Returns true if the task automatically deletes itself. See setAutoDelete() */ bool autoDelete() const; /** Adds a new session to the group */ void addSession(Session *session); /** * Executes the task on each of the sessions in the group. * The completed() signal is emitted when the task is finished, depending on the specific sub-class * execute() may be synchronous or asynchronous */ virtual void execute() = 0; Q_SIGNALS: /** * Emitted when the task has completed. * Depending on the task this may occur just before execute() returns, or it * may occur later * * @param success Indicates whether the task completed successfully or not */ void completed(bool success); protected: /** Returns a list of sessions in the group */ QList< SessionPtr > sessions() const; private: bool _autoDelete; QList< SessionPtr > _sessions; }; /** * A task which prompts for a URL for each session and saves that session's output * to the given URL */ class SaveHistoryTask : public SessionTask { Q_OBJECT public: /** Constructs a new task to save session output to URLs */ explicit SaveHistoryTask(QObject *parent = nullptr); ~SaveHistoryTask() Q_DECL_OVERRIDE; /** * Opens a save file dialog for each session in the group and begins saving * each session's history to the given URL. * * The data transfer is performed asynchronously and will continue after execute() returns. */ void execute() Q_DECL_OVERRIDE; private Q_SLOTS: void jobDataRequested(KIO::Job *job, QByteArray &data); void jobResult(KJob *job); private: class SaveJob // structure to keep information needed to process // incoming data requests from jobs { public: SessionPtr session; // the session associated with a history save job int lastLineFetched; // the last line processed in the previous data request // set this to -1 at the start of the save job TerminalCharacterDecoder *decoder; // decoder used to convert terminal characters // into output }; QHash _jobSession; }; //class SearchHistoryThread; /** * A task which searches through the output of sessions for matches for a given regular expression. * SearchHistoryTask operates on ScreenWindow instances rather than sessions added by addSession(). * A screen window can be added to the list to search using addScreenWindow() * * When execute() is called, the search begins in the direction specified by searchDirection(), * starting at the position of the current selection. * * FIXME - This is not a proper implementation of SessionTask, in that it ignores sessions specified * with addSession() * * TODO - Implementation requirements: * May provide progress feedback to the user when searching very large output logs. */ class SearchHistoryTask : public SessionTask { Q_OBJECT public: /** * 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: typedef QPointer ScreenWindowPtr; void executeOnScreenWindow(SessionPtr session, ScreenWindowPtr window); void highlightResult(ScreenWindowPtr window, int position); QMap< SessionPtr, ScreenWindowPtr > _windows; QRegularExpression _regExp; Enum::SearchDirection _direction; int _startLine; //static QPointer _thread; }; } #endif //SESSIONCONTROLLER_H diff --git a/src/TerminalDisplay.cpp b/src/TerminalDisplay.cpp index 256a57a4..247908ab 100644 --- a/src/TerminalDisplay.cpp +++ b/src/TerminalDisplay.cpp @@ -1,3967 +1,3979 @@ /* This file is part of Konsole, a terminal emulator for KDE. Copyright 2006-2008 by Robert Knight Copyright 1997,1998 by Lars Doelle This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "TerminalDisplay.h" // Config #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include // Konsole #include "Filter.h" #include "konsoledebug.h" #include "konsole_wcwidth.h" #include "TerminalCharacterDecoder.h" #include "Screen.h" #include "LineFont.h" #include "SessionController.h" #include "ExtendedCharTable.h" #include "TerminalDisplayAccessible.h" #include "SessionManager.h" #include "Session.h" #include "WindowSystemInfo.h" #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::updateScrollBarPalette() { QColor backgroundColor = _colorTable[DEFAULT_BACK_COLOR]; backgroundColor.setAlphaF(_opacity); QPalette p = palette(); p.setColor(QPalette::Window, backgroundColor); //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 QColor buttonColor; buttonColor.setHsvF(backgroundColor.hueF(), backgroundColor.saturationF(), backgroundColor.valueF() + (backgroundColor.valueF() < 0.5 ? 0.2 : -0.2)); p.setColor(QPalette::Button, buttonColor); p.setColor(QPalette::WindowText, _colorTable[DEFAULT_FORE_COLOR]); p.setColor(QPalette::ButtonText, _colorTable[DEFAULT_FORE_COLOR]); _scrollBar->setPalette(p); } void TerminalDisplay::setBackgroundColor(const QColor& color) { _colorTable[DEFAULT_BACK_COLOR] = color; QPalette p = palette(); p.setColor(backgroundRole(), color); setPalette(p); updateScrollBarPalette(); update(); } QColor TerminalDisplay::getBackgroundColor() const { QPalette p = palette(); return p.color(backgroundRole()); } void TerminalDisplay::setForegroundColor(const QColor& color) { _colorTable[DEFAULT_FORE_COLOR] = color; updateScrollBarPalette(); update(); } void TerminalDisplay::setColorTable(const ColorEntry table[]) { for (int i = 0; i < TABLE_COLORS; i++) { _colorTable[i] = table[i]; } setBackgroundColor(_colorTable[DEFAULT_BACK_COLOR]); } /* ------------------------------------------------------------------------- */ /* */ /* Font */ /* */ /* ------------------------------------------------------------------------- */ static inline bool isLineCharString(const QString& string) { if (string.length() == 0) { return false; } return isSupportedLineChar(string.at(0).unicode()); } void TerminalDisplay::fontChange(const QFont&) { QFontMetrics fm(font()); _fontHeight = fm.height() + _lineSpacing; 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); // In case the provided font doesn't have some specific characters it should // fall back to a Monospace fonts. newFont.setStyleHint(QFont::TypeWriter); QFontMetrics fontMetrics(newFont); // This check seems extreme and semi-random // TODO: research if these checks are still needed to prevent // enorgous 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 without anti-aliasing. // depending on the user's font configuration, this may not be respected if (!_antialiasText) { newFont.setStyleStrategy(QFont::StyleStrategy(newFont.styleStrategy() | QFont::NoAntialias)); } // experimental optimization. Konsole assumes that the terminal is using a // mono-spaced font, in which case kerning information should have an effect. // Disabling kerning saves some computation when rendering text. newFont.setKerning(false); // Konsole cannot handle non-integer font metrics newFont.setStyleStrategy(QFont::StyleStrategy(newFont.styleStrategy() | QFont::ForceIntegerMetrics)); // 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 (TerminalDisplay *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) , _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) , _scrollWheelState(ScrollState()) , _searchBar(new IncrementalSearchBar(this)) { // 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; const int ex = x + w - 1; const int ey = y + h - 1; const quint32 toDraw = LineChars[code]; //Top _lines: if ((toDraw & TopL) != 0u) { paint.drawLine(cx - 1, y, cx - 1, cy - 2); } if ((toDraw & TopC) != 0u) { paint.drawLine(cx, y, cx, cy - 2); } if ((toDraw & TopR) != 0u) { paint.drawLine(cx + 1, y, cx + 1, cy - 2); } //Bot _lines: if ((toDraw & BotL) != 0u) { paint.drawLine(cx - 1, cy + 2, cx - 1, ey); } if ((toDraw & BotC) != 0u) { paint.drawLine(cx, cy + 2, cx, ey); } if ((toDraw & BotR) != 0u) { paint.drawLine(cx + 1, cy + 2, cx + 1, ey); } //Left _lines: if ((toDraw & LeftT) != 0u) { paint.drawLine(x, cy - 1, cx - 2, cy - 1); } if ((toDraw & LeftC) != 0u) { paint.drawLine(x, cy, cx - 2, cy); } if ((toDraw & LeftB) != 0u) { paint.drawLine(x, cy + 1, cx - 2, cy + 1); } //Right _lines: if ((toDraw & RightT) != 0u) { paint.drawLine(cx + 2, cy - 1, ex, cy - 1); } if ((toDraw & RightC) != 0u) { paint.drawLine(cx + 2, cy, ex, cy); } if ((toDraw & RightB) != 0u) { paint.drawLine(cx + 2, cy + 1, ex, cy + 1); } //Intersection points. if ((toDraw & Int11) != 0u) { paint.drawPoint(cx - 1, cy - 1); } if ((toDraw & Int12) != 0u) { paint.drawPoint(cx, cy - 1); } if ((toDraw & Int13) != 0u) { paint.drawPoint(cx + 1, cy - 1); } if ((toDraw & Int21) != 0u) { paint.drawPoint(cx - 1, cy); } if ((toDraw & Int22) != 0u) { paint.drawPoint(cx, cy); } if ((toDraw & Int23) != 0u) { paint.drawPoint(cx + 1, cy); } if ((toDraw & Int31) != 0u) { paint.drawPoint(cx - 1, cy + 1); } if ((toDraw & Int32) != 0u) { paint.drawPoint(cx, cy + 1); } if ((toDraw & Int33) != 0u) { paint.drawPoint(cx + 1, cy + 1); } } static void drawOtherChar(QPainter& paint, int x, int y, int w, int h, uchar code) { //Calculate cell midpoints, end points. const int cx = x + w / 2; const int cy = y + h / 2; const int ex = x + w - 1; const int ey = y + h - 1; // Double dashes if (0x4C <= code && code <= 0x4F) { const int xHalfGap = qMax(w / 15, 1); const int yHalfGap = qMax(h / 15, 1); switch (code) { case 0x4D: // BOX DRAWINGS HEAVY DOUBLE DASH HORIZONTAL paint.drawLine(x, cy - 1, cx - xHalfGap - 1, cy - 1); paint.drawLine(x, cy + 1, cx - xHalfGap - 1, cy + 1); paint.drawLine(cx + xHalfGap, cy - 1, ex, cy - 1); paint.drawLine(cx + xHalfGap, cy + 1, ex, cy + 1); // No break! #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) Q_FALLTHROUGH(); #endif case 0x4C: // BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL paint.drawLine(x, cy, cx - xHalfGap - 1, cy); paint.drawLine(cx + xHalfGap, cy, ex, cy); break; case 0x4F: // BOX DRAWINGS HEAVY DOUBLE DASH VERTICAL paint.drawLine(cx - 1, y, cx - 1, cy - yHalfGap - 1); paint.drawLine(cx + 1, y, cx + 1, cy - yHalfGap - 1); paint.drawLine(cx - 1, cy + yHalfGap, cx - 1, ey); paint.drawLine(cx + 1, cy + yHalfGap, cx + 1, ey); // No break! #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) Q_FALLTHROUGH(); #endif case 0x4E: // BOX DRAWINGS LIGHT DOUBLE DASH VERTICAL paint.drawLine(cx, y, cx, cy - yHalfGap - 1); paint.drawLine(cx, cy + yHalfGap, cx, ey); break; } } // Rounded corner characters else if (0x6D <= code && code <= 0x70) { const int r = w * 3 / 8; const int d = 2 * r; switch (code) { case 0x6D: // BOX DRAWINGS LIGHT ARC DOWN AND RIGHT paint.drawLine(cx, cy + r, cx, ey); paint.drawLine(cx + r, cy, ex, cy); paint.drawArc(cx, cy, d, d, 90 * 16, 90 * 16); break; case 0x6E: // BOX DRAWINGS LIGHT ARC DOWN AND LEFT paint.drawLine(cx, cy + r, cx, ey); paint.drawLine(x, cy, cx - r, cy); paint.drawArc(cx - d, cy, d, d, 0 * 16, 90 * 16); break; case 0x6F: // BOX DRAWINGS LIGHT ARC UP AND LEFT paint.drawLine(cx, y, cx, cy - r); paint.drawLine(x, cy, cx - r, cy); paint.drawArc(cx - d, cy - d, d, d, 270 * 16, 90 * 16); break; case 0x70: // BOX DRAWINGS LIGHT ARC UP AND RIGHT paint.drawLine(cx, y, cx, cy - r); paint.drawLine(cx + r, cy, ex, cy); paint.drawArc(cx, cy - d, d, d, 180 * 16, 90 * 16); break; } } // Diagonals else if (0x71 <= code && code <= 0x73) { switch (code) { case 0x71: // BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT paint.drawLine(ex, y, x, ey); break; case 0x72: // BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT paint.drawLine(x, y, ex, ey); break; case 0x73: // BOX DRAWINGS LIGHT DIAGONAL CROSS paint.drawLine(ex, y, x, ey); paint.drawLine(x, y, ex, ey); break; } } } void TerminalDisplay::drawLineCharString(QPainter& painter, int x, int y, const QString& str, const Character* attributes) { const QPen& originalPen = painter.pen(); if (((attributes->rendition & RE_BOLD) != 0) && _boldIntense) { QPen boldPen(originalPen); boldPen.setWidth(3); painter.setPen(boldPen); } for (int i = 0 ; i < str.length(); i++) { const uchar code = str[i].cell(); if (LineChars[code] != 0u) { drawLineChar(painter, x + (_fontWidth * i), y, _fontWidth, _fontHeight, code); } else { drawOtherChar(painter, x + (_fontWidth * i), y, _fontWidth, _fontHeight, code); } } painter.setPen(originalPen); } void TerminalDisplay::setKeyboardCursorShape(Enum::CursorShapeEnum shape) { _cursorShape = shape; } Enum::CursorShapeEnum TerminalDisplay::keyboardCursorShape() const { return _cursorShape; } void TerminalDisplay::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) { Enum::CursorShapeEnum 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(); updateScrollBarPalette(); } void TerminalDisplay::setWallpaper(ColorSchemeWallpaper::Ptr p) { _wallpaper = p; } void TerminalDisplay::drawBackground(QPainter& painter, const QRect& rect, const QColor& backgroundColor, bool useOpacitySetting) { // the area of the widget showing the contents of the terminal display is drawn // using the background color from the color scheme set with setColorTable() // // the area of the widget behind the scroll-bar is drawn using the background // brush from the scroll-bar's palette, to give the effect of the scroll-bar // being outside of the terminal display and visual consistency with other KDE // applications. 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)); painter.save(); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(rect, color); painter.restore(); #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 QRect cursorRect = rect.adjusted(0, 1, 0, 0); QColor cursorColor = _cursorColor.isValid() ? _cursorColor : foregroundColor; painter.setPen(cursorColor); if (_cursorShape == Enum::BlockCursor) { // draw the cursor outline, adjusting the area so that // it is draw entirely inside 'rect' int penWidth = qMax(1, painter.pen().width()); painter.drawRect(cursorRect.adjusted(penWidth / 2, penWidth / 2, - penWidth / 2 - penWidth % 2, - penWidth / 2 - penWidth % 2)); // draw the cursor body only when the widget has focus if (hasFocus()) { painter.fillRect(cursorRect, cursorColor); if (!_cursorColor.isValid()) { // invert the color used to draw the text to ensure that the character at // the cursor position is readable invertCharacterColor = true; } } } else if (_cursorShape == Enum::UnderlineCursor) { painter.drawLine(cursorRect.left(), cursorRect.bottom(), cursorRect.right(), cursorRect.bottom()); } else if (_cursorShape == Enum::IBeamCursor) { painter.drawLine(cursorRect.left(), cursorRect.top(), cursorRect.left(), cursorRect.bottom()); } } void TerminalDisplay::drawCharacters(QPainter& painter, const QRect& rect, const QString& text, const Character* style, bool invertCharacterColor) { // don't draw text which is currently blinking if (_textBlinking && ((style->rendition & RE_BLINK) != 0)) { return; } // don't draw concealed characters if ((style->rendition & RE_CONCEAL) != 0) { return; } // setup bold and underline bool useBold = (((style->rendition & RE_BOLD) != 0) && _boldIntense) || font().bold(); const bool useUnderline = ((style->rendition & RE_UNDERLINE) != 0) || font().underline(); const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || font().italic(); const bool useStrikeOut = ((style->rendition & RE_STRIKEOUT) != 0) || font().strikeOut(); const bool useOverline = ((style->rendition & RE_OVERLINE) != 0) || font().overline(); QFont font = painter.font(); if (font.bold() != useBold || font.underline() != useUnderline || font.italic() != useItalic || font.strikeOut() != useStrikeOut || font.overline() != useOverline) { font.setBold(useBold); font.setUnderline(useUnderline); font.setItalic(useItalic); font.setStrikeOut(useStrikeOut); font.setOverline(useOverline); painter.setFont(font); } // setup pen const CharacterColor& textColor = (invertCharacterColor ? style->backgroundColor : style->foregroundColor); const QColor color = textColor.color(_colorTable); QPen pen = painter.pen(); if (pen.color() != color) { pen.setColor(color); painter.setPen(color); } // draw text if (isLineCharString(text) && !_useFontLineCharacters) { drawLineCharString(painter, rect.x(), rect.y(), text, style); } else { // Force using LTR as the document layout for the terminal area, because // there is no use cases for RTL emulator and RTL terminal application. // // This still allows RTL characters to be rendered in the RTL way. painter.setLayoutDirection(Qt::LeftToRight); 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) { painter.save(); // setup painter const QColor foregroundColor = style->foregroundColor.color(_colorTable); const QColor backgroundColor = style->backgroundColor.color(_colorTable); // draw background if different from the display's background color if (backgroundColor != palette().background().color()) { drawBackground(painter, rect, backgroundColor, false /* do not use transparency */); } // draw cursor shape if the current character is the cursor // this may alter the foreground and background colors bool invertCharacterColor = false; if ((style->rendition & RE_CURSOR) != 0) { drawCursor(painter, rect, foregroundColor, backgroundColor, invertCharacterColor); } // draw text drawCharacters(painter, rect, text, style, invertCharacterColor); painter.restore(); } void TerminalDisplay::drawPrinterFriendlyTextFragment(QPainter& painter, const QRect& rect, const QString& text, const Character* style) { painter.save(); // Set the colors used to draw to black foreground and white // background for printer friendly output when printing Character print_style = *style; print_style.foregroundColor = CharacterColor(COLOR_SPACE_RGB, 0x00000000); print_style.backgroundColor = CharacterColor(COLOR_SPACE_RGB, 0xFFFFFFFF); // draw text drawCharacters(painter, rect, text, &print_style, false); painter.restore(); } void TerminalDisplay::setRandomSeed(uint randomSeed) { _randomSeed = randomSeed; } uint TerminalDisplay::randomSeed() const { return _randomSeed; } // scrolls the image by 'lines', down if lines > 0 or up otherwise. // // the terminal emulation keeps track of the scrolling of the character // image as it receives input, and when the view is updated, it calls scrollImage() // with the final scroll amount. this improves performance because scrolling the // display is much cheaper than re-rendering all the text for the // part of the image which has moved up or down. // Instead only new lines have to be drawn void TerminalDisplay::scrollImage(int lines , const QRect& screenWindowRegion) { // 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 artefacts, see BUG 350651 if (!(WindowSystemInfo::HAVE_TRANSPARENCY && (qApp->devicePixelRatio() > 1.0)) && _wallpaper->isNull() && !_searchBar->isVisible()) { scrollImage(_screenWindow->scrollCount() , _screenWindow->scrollRegion()); _screenWindow->resetScrollCount(); } if (_image == nullptr) { // Create _image. // The emitted changedContentSizeSignal also leads to getImage being recreated, so do this first. updateImageSize(); } Character* const newimg = _screenWindow->getImage(); const int lines = _screenWindow->windowLines(); const int columns = _screenWindow->windowColumns(); setScroll(_screenWindow->currentLine() , _screenWindow->lineCount()); Q_ASSERT(_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; // update the parts of the display which have changed update(dirtyRegion); if (_allowBlinkingText && _hasTextBlinker && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!_hasTextBlinker && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } delete[] dirtyMask; #ifndef QT_NO_ACCESSIBILITY QAccessibleEvent dataChangeEvent(this, QAccessible::VisibleDataChanged); QAccessible::updateAccessibility(&dataChangeEvent); QAccessibleTextCursorEvent cursorEvent(this, _usedColumns * screenWindow()->screen()->getCursorY() + screenWindow()->screen()->getCursorX()); QAccessible::updateAccessibility(&cursorEvent); #endif } void TerminalDisplay::showResizeNotification() { if (_showTerminalSizeHint && isVisible()) { if (_resizeWidget == nullptr) { _resizeWidget = new QLabel(i18n("Size: XXX x XXX"), this); _resizeWidget->setMinimumWidth(_resizeWidget->fontMetrics().width(i18n("Size: XXX x XXX"))); _resizeWidget->setMinimumHeight(_resizeWidget->sizeHint().height()); _resizeWidget->setAlignment(Qt::AlignCenter); _resizeWidget->setStyleSheet(QStringLiteral("background-color:palette(window);border-style:solid;border-width:1px;border-color:palette(dark)")); _resizeTimer = new QTimer(this); _resizeTimer->setInterval(SIZE_HINT_DURATION); _resizeTimer->setSingleShot(true); connect(_resizeTimer, &QTimer::timeout, _resizeWidget, &QLabel::hide); } QString sizeStr = i18n("Size: %1 x %2", _columns, _lines); _resizeWidget->setText(sizeStr); _resizeWidget->move((width() - _resizeWidget->width()) / 2, (height() - _resizeWidget->height()) / 2 + 20); _resizeWidget->show(); _resizeTimer->start(); } } void TerminalDisplay::paintEvent(QPaintEvent* pe) { QPainter paint(this); foreach(const QRect & rect, (pe->region() & contentsRect()).rects()) { drawBackground(paint, rect, palette().background().color(), true /* use opacity setting */); drawContents(paint, rect); } drawCurrentResultRect(paint); drawInputMethodPreeditString(paint, preeditRect()); paintFilters(paint); } void TerminalDisplay::printContent(QPainter& painter, bool friendly) { // Reinitialize the font with the printers paint device so the font // measurement calculations will be done correctly QFont savedFont = getVTFont(); QFont font(savedFont, painter.device()); painter.setFont(font); setVTFont(font); QRect rect(0, 0, size().width(), size().height()); _printerFriendly = friendly; if (!friendly) { drawBackground(painter, rect, getBackgroundColor(), true /* use opacity setting */); } drawContents(painter, rect); _printerFriendly = false; setVTFont(savedFont); } QPoint TerminalDisplay::cursorPosition() const { if (!_screenWindow.isNull()) { return _screenWindow->cursorPosition(); } else { return QPoint(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 int urlNumber = 0; QList spots = _filterChain->hotSpots(); foreach(Filter::HotSpot* spot, spots) { urlNumber++; QRegion region; if (spot->type() == Filter::HotSpot::Link) { QRect r; if (spot->startLine() == spot->endLine()) { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } else { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) { r.setCoords(0 * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } r.setCoords(0 * _fontWidth + _contentRect.left(), spot->endLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); region |= r; } if (_showUrlHint && urlNumber < 10) { // Position at the beginning of the URL const QVector regionRects = region.rects(); QRect hintRect(regionRects.first()); hintRect.setWidth(r.height()); painter.fillRect(hintRect, QColor(0, 0, 0, 128)); painter.setPen(Qt::white); painter.drawRect(hintRect.adjusted(0, 0, -1, -1)); painter.drawText(hintRect, Qt::AlignCenter, QString::number(urlNumber)); } } for (int line = spot->startLine() ; line <= spot->endLine() ; line++) { int startColumn = 0; int endColumn = _columns - 1; // TODO use number of _columns which are actually // occupied on this line rather than the width of the // display in _columns // Check image size so _image[] is valid (see makeImage) if (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 QPoint tL = contentsRect().topLeft(); const int tLx = tL.x(); const int tLy = tL.y(); const int lux = qMin(_usedColumns - 1, qMax(0, (rect.left() - tLx - _contentRect.left()) / _fontWidth)); const int luy = qMin(_usedLines - 1, qMax(0, (rect.top() - tLy - _contentRect.top()) / _fontHeight)); const int rlx = qMin(_usedColumns - 1, qMax(0, (rect.right() - tLx - _contentRect.left()) / _fontWidth)); const int rly = qMin(_usedLines - 1, qMax(0, (rect.bottom() - tLy - _contentRect.top()) / _fontHeight)); const int numberOfColumns = _usedColumns; QVector univec; univec.reserve(numberOfColumns); for (int y = luy; y <= rly; y++) { int x = lux; if ((_image[loc(lux, y)].character == 0u) && (x != 0)) { x--; // Search for start of multi-column character } for (; x <= rlx; x++) { int len = 1; int p = 0; // reset our buffer to the number of columns int bufferSize = numberOfColumns; 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 <= rlx && _image[loc(x + len, y)].foregroundColor == currentForeground && _image[loc(x + len, y)].backgroundColor == currentBackground && (_image[loc(x + len, y)].rendition & ~RE_EXTENDED_CHAR) == (currentRendition & ~RE_EXTENDED_CHAR) && (_image[qMin(loc(x + len, y) + 1, _imageSize - 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() + tLx + _fontWidth * x , _contentRect.top() + tLy + _fontHeight * y , _fontWidth * len , _fontHeight); //move the calculated area to take account of scaling applied to the painter. //the position of the area from the origin (0,0) is scaled //by the opposite of whatever //transformation has been applied to the painter. this ensures that //painting does actually start from textArea.topLeft() //(instead of textArea.topLeft() * painter-scale) textArea.moveTopLeft(textScale.inverted().map(textArea.topLeft())); 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; } QRect r(0, _contentRect.top() + (_screenWindow->currentResultLine() - _screenWindow->currentLine()) * _fontHeight, contentsRect().width(), _fontHeight); painter.fillRect(r, QColor(0, 0, 255, 80)); } QRect TerminalDisplay::imageToWidget(const QRect& imageArea) const { QRect result; result.setLeft(_contentRect.left() + _fontWidth * imageArea.left()); result.setTop(_contentRect.top() + _fontHeight * imageArea.top()); result.setWidth(_fontWidth * imageArea.width()); result.setHeight(_fontHeight * imageArea.height()); return result; } /* ------------------------------------------------------------------------- */ /* */ /* Blinking Text & Cursor */ /* */ /* ------------------------------------------------------------------------- */ void TerminalDisplay::setBlinkingCursorEnabled(bool blink) { _allowBlinkingCursor = blink; if (blink && !_blinkCursorTimer->isActive()) { _blinkCursorTimer->start(); } if (!blink && _blinkCursorTimer->isActive()) { _blinkCursorTimer->stop(); if (_cursorBlinking) { // if cursor is blinking(hidden), blink it again to make it show blinkCursorEvent(); } Q_ASSERT(!_cursorBlinking); } } void TerminalDisplay::setBlinkingTextEnabled(bool blink) { _allowBlinkingText = blink; if (blink && !_blinkTextTimer->isActive()) { _blinkTextTimer->start(); } if (!blink && _blinkTextTimer->isActive()) { _blinkTextTimer->stop(); _textBlinking = false; } } void TerminalDisplay::focusOutEvent(QFocusEvent*) { // trigger a repaint of the cursor so that it is both: // // * visible (in case it was hidden during blinking) // * drawn in a focused out state _cursorBlinking = false; updateCursor(); // suppress further cursor blinking _blinkCursorTimer->stop(); Q_ASSERT(!_cursorBlinking); // if text is blinking (hidden), blink it again to make it shown if (_textBlinking) { blinkTextEvent(); } // suppress further text blinking _blinkTextTimer->stop(); Q_ASSERT(!_textBlinking); _showUrlHint = false; emit focusLost(); } void TerminalDisplay::focusInEvent(QFocusEvent*) { if (_allowBlinkingCursor) { _blinkCursorTimer->start(); } updateCursor(); if (_allowBlinkingText && _hasTextBlinker) { _blinkTextTimer->start(); } emit focusGained(); } void TerminalDisplay::blinkTextEvent() { Q_ASSERT(_allowBlinkingText); _textBlinking = !_textBlinking; // TODO: Optimize to only repaint the areas of the widget where there is // blinking text rather than repainting the whole widget. update(); } void TerminalDisplay::blinkCursorEvent() { Q_ASSERT(_allowBlinkingCursor); _cursorBlinking = !_cursorBlinking; updateCursor(); } void TerminalDisplay::updateCursor() { if (!isCursorOnDisplay()){ return; } const int cursorLocation = loc(cursorPosition().x(), cursorPosition().y()); Q_ASSERT(cursorLocation < _imageSize); int charWidth = konsole_wcwidth(_image[cursorLocation].character); 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()) { QStyle::RequestSoftwareInputPanel behavior = QStyle::RequestSoftwareInputPanel( style()->styleHint(QStyle::SH_RequestSoftwareInputPanel)); if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) { QEvent event(QEvent::RequestSoftwareInputPanel); QApplication::sendEvent(this, &event); } } _lineSelectionMode = false; _wordSelectionMode = false; // The user clicked inside selected text bool selected = _screenWindow->isSelected(pos.x(), pos.y()); // Drag only when the Control key is held if ((!_ctrlRequiredForDrag || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && selected) { _dragInfo.state = diPending; _dragInfo.start = ev->pos(); } else { // No reason to ever start a drag event _dragInfo.state = diNone; _preserveLineBreaks = !(((ev->modifiers() & Qt::ControlModifier) != 0u) && !(ev->modifiers() & Qt::AltModifier)); _columnSelectionMode = ((ev->modifiers() & Qt::AltModifier) != 0u) && ((ev->modifiers() & Qt::ControlModifier) != 0u); // There are a couple of use cases when selecting text : // Normal buffer or Alternate buffer when not using Mouse Tracking: // select text or extendSelection or columnSelection or columnSelection + extendSelection // // Alternate buffer when using Mouse Tracking and with Shift pressed: // select text or columnSelection if (!_usesMouseTracking && ((ev->modifiers() == Qt::ShiftModifier) || (((ev->modifiers() & Qt::ShiftModifier) != 0u) && _columnSelectionMode))) { extendSelection(ev->pos()); } else if ((!_usesMouseTracking && !((ev->modifiers() & Qt::ShiftModifier))) || (_usesMouseTracking && ((ev->modifiers() & Qt::ShiftModifier) != 0u))) { _screenWindow->clearSelection(); pos.ry() += _scrollBar->value(); _iPntSel = _pntSel = pos; _actSel = 1; // left mouse button pressed but nothing selected yet. } else if (_usesMouseTracking && !_readOnly) { emit mouseSignal(0, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u))) { Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) { QObject action; action.setObjectName(QStringLiteral("open-action")); spot->activate(&action); } } } } else if (ev->button() == Qt::MidButton) { processMidButtonClick(ev); } else if (ev->button() == Qt::RightButton) { if (!_usesMouseTracking || ((ev->modifiers() & Qt::ShiftModifier) != 0u)) { emit configureRequest(ev->pos()); } else { if(!_readOnly) { emit mouseSignal(2, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum() , 0); } } } } QList TerminalDisplay::filterActions(const QPoint& position) { int charLine, charColumn; getCharacterPosition(position, charLine, charColumn, false); Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); return spot != nullptr ? spot->actions() : QList(); } void TerminalDisplay::mouseMoveEvent(QMouseEvent* ev) { int charLine = 0; int charColumn = 0; getCharacterPosition(ev->pos(), charLine, charColumn, !_usesMouseTracking); processFilters(); // handle filters // change link hot-spot appearance on mouse-over Filter::HotSpot* spot = _filterChain->hotSpotAt(charLine, charColumn); if ((spot != nullptr) && spot->type() == Filter::HotSpot::Link) { QRegion previousHotspotArea = _mouseOverHotspotArea; _mouseOverHotspotArea = QRegion(); QRect r; if (spot->startLine() == spot->endLine()) { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } else { r.setCoords(spot->startColumn()*_fontWidth + _contentRect.left(), spot->startLine()*_fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (spot->startLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; for (int line = spot->startLine() + 1 ; line < spot->endLine() ; line++) { r.setCoords(0 * _fontWidth + _contentRect.left(), line * _fontHeight + _contentRect.top(), (_columns)*_fontWidth + _contentRect.left() - 1, (line + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } r.setCoords(0 * _fontWidth + _contentRect.left(), spot->endLine()*_fontHeight + _contentRect.top(), (spot->endColumn())*_fontWidth + _contentRect.left() - 1, (spot->endLine() + 1)*_fontHeight + _contentRect.top() - 1); _mouseOverHotspotArea |= r; } if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) && (cursor().shape() != Qt::PointingHandCursor)) { setCursor(Qt::PointingHandCursor); } update(_mouseOverHotspotArea | previousHotspotArea); } else if (!_mouseOverHotspotArea.isEmpty()) { if ((_openLinksByDirectClick || ((ev->modifiers() & Qt::ControlModifier) != 0u)) || (cursor().shape() == Qt::PointingHandCursor)) { setCursor(_usesMouseTracking ? Qt::ArrowCursor : Qt::IBeamCursor); } update(_mouseOverHotspotArea); // set hotspot area to an invalid rectangle _mouseOverHotspotArea = QRegion(); } // for auto-hiding the cursor, we need mouseTracking if (ev->buttons() == Qt::NoButton) { return; } // if the program running in the terminal is interested in Mouse Tracking // evnets then emit a mouse movement signal, unless the shift key is // being held down, which overrides this. if (_usesMouseTracking && !(ev->modifiers() & Qt::ShiftModifier)) { int button = 3; if ((ev->buttons() & Qt::LeftButton) != 0u) { button = 0; } if ((ev->buttons() & Qt::MidButton) != 0u) { button = 1; } if ((ev->buttons() & Qt::RightButton) != 0u) { button = 2; } emit mouseSignal(button, charColumn + 1, charLine + 1 + _scrollBar->value() - _scrollBar->maximum(), 1); return; } if (_dragInfo.state == diPending) { // we had a mouse down, but haven't confirmed a drag yet // if the mouse has moved sufficiently, we will confirm const int distance = QApplication::startDragDistance(); if (ev->x() > _dragInfo.start.x() + distance || ev->x() < _dragInfo.start.x() - distance || ev->y() > _dragInfo.start.y() + distance || ev->y() < _dragInfo.start.y() - distance) { // we've left the drag square, we can start a real drag operation now _screenWindow->clearSelection(); doDrag(); } return; } else if (_dragInfo.state == diDragging) { // this isn't technically needed because mouseMoveEvent is suppressed during // Qt drag operations, replaced by dragMoveEvent return; } if (_actSel == 0) { return; } // don't extend selection while pasting if ((ev->buttons() & Qt::MidButton) != 0u) { return; } extendSelection(ev->pos()); } void TerminalDisplay::leaveEvent(QEvent *) { // remove underline from an active link when cursor leaves the widget area if(!_mouseOverHotspotArea.isEmpty()) { update(_mouseOverHotspotArea); _mouseOverHotspotArea = QRegion(); } } void TerminalDisplay::extendSelection(const QPoint& position) { if (_screenWindow.isNull()) { return; } //if ( !contentsRect().contains(ev->pos()) ) return; const QPoint tL = contentsRect().topLeft(); const int tLx = tL.x(); const int tLy = tL.y(); const int scroll = _scrollBar->value(); // we're in the process of moving the mouse with the left button pressed // the mouse cursor will kept caught within the bounds of the text in // this widget. int linesBeyondWidget = 0; QRect textBounds(tLx + _contentRect.left(), tLy + _contentRect.top(), _usedColumns * _fontWidth - 1, _usedLines * _fontHeight - 1); QPoint pos = position; // Adjust position within text area bounds. const QPoint oldpos = pos; pos.setX(qBound(textBounds.left(), pos.x(), textBounds.right())); pos.setY(qBound(textBounds.top(), pos.y(), textBounds.bottom())); if (oldpos.y() > textBounds.bottom()) { linesBeyondWidget = (oldpos.y() - textBounds.bottom()) / _fontHeight; _scrollBar->setValue(_scrollBar->value() + linesBeyondWidget + 1); // scrollforward } if (oldpos.y() < textBounds.top()) { linesBeyondWidget = (textBounds.top() - oldpos.y()) / _fontHeight; _scrollBar->setValue(_scrollBar->value() - linesBeyondWidget - 1); // history } int charColumn = 0; int charLine = 0; getCharacterPosition(pos, charLine, charColumn, true); QPoint here = QPoint(charColumn, charLine); QPoint ohere; QPoint _iPntSelCorr = _iPntSel; _iPntSelCorr.ry() -= _scrollBar->value(); QPoint _pntSelCorr = _pntSel; _pntSelCorr.ry() -= _scrollBar->value(); bool swapping = false; if (_wordSelectionMode) { // Extend to word boundaries int i; QChar selClass; const bool left_not_right = (here.y() < _iPntSelCorr.y() || (here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x())); const bool old_left_not_right = (_pntSelCorr.y() < _iPntSelCorr.y() || (_pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x())); swapping = left_not_right != old_left_not_right; // Find left (left_not_right ? from here : from start) QPoint left = left_not_right ? here : _iPntSelCorr; i = loc(qBound(0, left.x(), _columns - 1), qBound(0, left.y(), _lines - 1)); if (i >= 0 && i < _imageSize) { selClass = charClass(_image[qMin(i, _imageSize - 1)]); while (((left.x() > 0) || (left.y() > 0 && ((_lineProperties[left.y() - 1] & LINE_WRAPPED) != 0))) && charClass(_image[i - 1]) == selClass) { i--; if (left.x() > 0) { left.rx()--; } else { left.rx() = _usedColumns - 1; left.ry()--; } } } // Find left (left_not_right ? from start : from here) QPoint right = left_not_right ? _iPntSelCorr : here; i = loc(qBound(0, right.x(), _columns - 1), qBound(0, right.y(), _lines - 1)); if (i >= 0 && i < _imageSize) { selClass = charClass(_image[qMin(i, _imageSize - 1)]); while (((right.x() < _usedColumns - 1) || (right.y() < _usedLines - 1 && ((_lineProperties[right.y()] & LINE_WRAPPED) != 0))) && charClass(_image[i + 1]) == selClass) { i++; if (right.x() < _usedColumns - 1) { right.rx()++; } else { right.rx() = 0; right.ry()++; } } } // Pick which is start (ohere) and which is extension (here) if (left_not_right) { here = left; ohere = right; } else { here = right; ohere = left; } ohere.rx()++; } if (_lineSelectionMode) { // Extend to complete line const bool above_not_below = (here.y() < _iPntSelCorr.y()); if (above_not_below) { ohere = findLineEnd(_iPntSelCorr); here = findLineStart(here); } else { ohere = findLineStart(_iPntSelCorr); here = findLineEnd(here); } swapping = !(_tripleSelBegin == ohere); _tripleSelBegin = ohere; ohere.rx()++; } int offset = 0; if (!_wordSelectionMode && !_lineSelectionMode) { QChar selClass; const bool left_not_right = (here.y() < _iPntSelCorr.y() || (here.y() == _iPntSelCorr.y() && here.x() < _iPntSelCorr.x())); const bool old_left_not_right = (_pntSelCorr.y() < _iPntSelCorr.y() || (_pntSelCorr.y() == _iPntSelCorr.y() && _pntSelCorr.x() < _iPntSelCorr.x())); swapping = left_not_right != old_left_not_right; // Find left (left_not_right ? from here : from start) const QPoint left = left_not_right ? here : _iPntSelCorr; // Find left (left_not_right ? from start : from here) QPoint right = left_not_right ? _iPntSelCorr : here; if (right.x() > 0 && !_columnSelectionMode) { 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(); QPoint bgnSel = pos; QPoint endSel = pos; int i = loc(bgnSel.x(), bgnSel.y()); _iPntSel = bgnSel; _iPntSel.ry() += _scrollBar->value(); _wordSelectionMode = true; _actSel = 2; // within selection // find word boundaries... const QChar selClass = charClass(_image[i]); { // find the start of the word int x = bgnSel.x(); while (((x > 0) || (bgnSel.y() > 0 && ((_lineProperties[bgnSel.y() - 1] & LINE_WRAPPED) != 0))) && charClass(_image[i - 1]) == selClass) { i--; if (x > 0) { x--; } else { x = _usedColumns - 1; bgnSel.ry()--; } } bgnSel.setX(x); _screenWindow->setSelectionStart(bgnSel.x() , bgnSel.y() , false); // find the end of the word i = loc(endSel.x(), endSel.y()); x = endSel.x(); while (((x < _usedColumns - 1) || (endSel.y() < _usedLines - 1 && ((_lineProperties[endSel.y()] & LINE_WRAPPED) != 0))) && charClass(_image[i + 1]) == selClass) { i++; if (x < _usedColumns - 1) { x++; } else { x = 0; endSel.ry()++; } } endSel.setX(x); // In word selection mode don't select @ (64) if at end of word. if (((_image[i].rendition & RE_EXTENDED_CHAR) == 0) && (QChar(_image[i].character) == QLatin1Char('@')) && ((endSel.x() - bgnSel.x()) > 0)) { endSel.setX(x - 1); } _actSel = 2; // within selection _screenWindow->setSelectionEnd(endSel.x() , endSel.y()); copyToX11Selection(); } _possibleTripleClick = true; QTimer::singleShot(QApplication::doubleClickInterval(), [this]() { _possibleTripleClick = false; }); } void TerminalDisplay::wheelEvent(QWheelEvent* ev) { // Only vertical scrolling is supported if (ev->orientation() != Qt::Vertical) { return; } const int modifiers = ev->modifiers(); // 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 continiously wrapped (top left corner = 0,0; previous line not visible = 0,-1). */ QPoint TerminalDisplay::findLineStart(const QPoint &pnt) { const int visibleScreenLines = _lineProperties.size(); const int topVisibleLine = _screenWindow->currentLine(); Screen *screen = _screenWindow->screen(); int line = pnt.y(); int lineInHistory= line + topVisibleLine; QVector lineProperties = _lineProperties; while (lineInHistory > 0) { for (; line > 0; line--, lineInHistory--) { // Does previous line wrap around? if ((lineProperties[line - 1] & LINE_WRAPPED) == 0) { return QPoint(0, lineInHistory - topVisibleLine); } } if (lineInHistory < 1) { break; } // _lineProperties is only for the visible screen, so grab new data int newRegionStart = qMax(0, lineInHistory - visibleScreenLines); lineProperties = screen->getLineProperties(newRegionStart, lineInHistory - 1); line = lineInHistory - newRegionStart; } return QPoint(0, lineInHistory - topVisibleLine); } /* Moving right/down from the line containing pnt, return the ending offset point which the given line is continiously wrapped. */ QPoint TerminalDisplay::findLineEnd(const QPoint &pnt) { const int visibleScreenLines = _lineProperties.size(); const int topVisibleLine = _screenWindow->currentLine(); const int maxY = _screenWindow->lineCount() - 1; Screen *screen = _screenWindow->screen(); int line = pnt.y(); int lineInHistory= line + topVisibleLine; QVector lineProperties = _lineProperties; while (lineInHistory < maxY) { for (; line < lineProperties.count() && lineInHistory < maxY; line++, lineInHistory++) { // Does current line wrap around? if ((lineProperties[line] & LINE_WRAPPED) == 0) { return QPoint(_columns - 1, lineInHistory - topVisibleLine); } } line = 0; lineProperties = screen->getLineProperties(lineInHistory, qMin(lineInHistory + visibleScreenLines, maxY)); } return QPoint(_columns - 1, lineInHistory - topVisibleLine); } QPoint TerminalDisplay::findWordStart(const QPoint &pnt) { const int regSize = qMax(_screenWindow->windowLines(), 10); const int curLine = _screenWindow->currentLine(); int i = pnt.y(); int x = pnt.x(); int y = i + curLine; int j = loc(x, i); QVector lineProperties = _lineProperties; Screen *screen = _screenWindow->screen(); Character *image = _image; Character *tmp_image = nullptr; const QChar selClass = charClass(image[j]); const int imageSize = regSize * _columns; while (true) { for (;;j--, x--) { if (x > 0) { if (charClass(image[j - 1]) == selClass) { continue; } goto out; } else if (i > 0) { if (((lineProperties[i - 1] & LINE_WRAPPED) != 0) && charClass(image[j - 1]) == selClass) { x = _columns; i--; y--; continue; } goto out; } else if (y > 0) { break; } else { goto out; } } int newRegStart = qMax(0, y - regSize); lineProperties = screen->getLineProperties(newRegStart, y - 1); i = y - newRegStart; if (tmp_image == nullptr) { tmp_image = new Character[imageSize]; image = tmp_image; } screen->getImage(tmp_image, imageSize, newRegStart, y - 1); j = loc(x, i); } out: if (tmp_image != nullptr) { delete[] tmp_image; } return QPoint(x, y - curLine); } QPoint TerminalDisplay::findWordEnd(const QPoint &pnt) { const int regSize = qMax(_screenWindow->windowLines(), 10); const int curLine = _screenWindow->currentLine(); int i = pnt.y(); int x = pnt.x(); int y = i + curLine; int j = loc(x, i); QVector lineProperties = _lineProperties; Screen *screen = _screenWindow->screen(); Character *image = _image; Character *tmp_image = nullptr; const QChar selClass = charClass(image[j]); const int imageSize = regSize * _columns; const int maxY = _screenWindow->lineCount() - 1; const int maxX = _columns - 1; while (true) { const int lineCount = lineProperties.count(); for (;;j++, x++) { if (x < maxX) { if (charClass(image[j + 1]) == selClass) { continue; } goto out; } else if (i < lineCount - 1) { if (((lineProperties[i] & LINE_WRAPPED) != 0) && charClass(image[j + 1]) == selClass) { x = -1; i++; y++; continue; } goto out; } else if (y < maxY) { if (i < lineCount && ((lineProperties[i] & LINE_WRAPPED) == 0)) { goto out; } break; } else { goto out; } } int newRegEnd = qMin(y + regSize - 1, maxY); lineProperties = screen->getLineProperties(y, newRegEnd); i = 0; if (tmp_image == nullptr) { tmp_image = new Character[imageSize]; image = tmp_image; } screen->getImage(tmp_image, imageSize, y, newRegEnd); x--; j = loc(x, i); } out: y -= curLine; // In word selection mode don't select @ (64) if at end of word. if (((image[j].rendition & RE_EXTENDED_CHAR) == 0) && (QChar(image[j].character) == QLatin1Char('@')) && (y > pnt.y() || x > pnt.x())) { if (x > 0) { x--; } else { y--; } } if (tmp_image != nullptr) { delete[] tmp_image; } return QPoint(x, y); } Screen::DecodingOptions TerminalDisplay::currentDecodingOptions() { Screen::DecodingOptions decodingOptions; if (_preserveLineBreaks) { decodingOptions |= Screen::PreserveLineBreaks; } if (_trimLeadingSpaces) { decodingOptions |= Screen::TrimLeadingWhitespace; } if (_trimTrailingSpaces) { decodingOptions |= Screen::TrimTrailingWhitespace; } return decodingOptions; } void TerminalDisplay::mouseTripleClickEvent(QMouseEvent* ev) { if (_screenWindow.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; } } 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 = string_width(_inputMethodData.preeditString); if (preeditLength == 0) { return QRect(); } 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 hintSelected = event->key() - 0x31; if (hintSelected >= 0 && hintSelected < 10 && hintSelected < _filterChain->hotSpots().count()) { _filterChain->hotSpots().at(hintSelected)->activate(); _showUrlHint = false; update(); return; } if (!_showUrlHint) { processFilters(); _showUrlHint = true; update(); } } _screenWindow->screen()->setCurrentTerminalDisplay(this); 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); } } 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: _scrollBar->setPalette(QApplication::palette()); break; default: break; } return eventHandled ? true : QWidget::event(event); } void TerminalDisplay::contextMenuEvent(QContextMenuEvent* event) { // the logic for the mouse case is within MousePressEvent() if (event->reason() != QContextMenuEvent::Mouse) { emit configureRequest(mapFromGlobal(QCursor::pos())); } } /* --------------------------------------------------------------------- */ /* */ /* Bell */ /* */ /* --------------------------------------------------------------------- */ void TerminalDisplay::setBellMode(int mode) { _bellMode = mode; } int TerminalDisplay::bellMode() const { return _bellMode; } void TerminalDisplay::bell(const QString& message) { if (_bellMasked) { return; } switch (_bellMode) { case Enum::SystemBeepBell: KNotification::beep(); break; case Enum::NotifyBell: // STABLE API: // Please note that these event names, "BellVisible" and "BellInvisible", // should not change and should be kept stable, because other applications // that use this code via KPart rely on these names for notifications. KNotification::event(hasFocus() ? QStringLiteral("BellVisible") : QStringLiteral("BellInvisible"), message, QPixmap(), this); break; case Enum::VisualBell: visualBell(); break; default: break; } // limit the rate at which bells can occur. // ...mainly for sound effects where rapid bells in sequence // produce a horrible noise. _bellMasked = true; QTimer::singleShot(500, [this]() { _bellMasked = false; }); } void TerminalDisplay::visualBell() { swapFGBGColors(); QTimer::singleShot(200, this, &Konsole::TerminalDisplay::swapFGBGColors); } void TerminalDisplay::swapFGBGColors() { // swap the default foreground & background color ColorEntry color = _colorTable[DEFAULT_BACK_COLOR]; _colorTable[DEFAULT_BACK_COLOR] = _colorTable[DEFAULT_FORE_COLOR]; _colorTable[DEFAULT_FORE_COLOR] = color; update(); } /* --------------------------------------------------------------------- */ /* */ /* Drag & Drop */ /* */ /* --------------------------------------------------------------------- */ void TerminalDisplay::dragEnterEvent(QDragEnterEvent* event) { // text/plain alone is enough for KDE-apps // text/uri-list is for supporting some non-KDE apps, such as thunar // and pcmanfm // That also applies in dropEvent() const auto mimeData = event->mimeData(); if ((!_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 QAction* action = qobject_cast(sender()); if (action != nullptr) { emit sendStringToEmu(action->data().toString().toLocal8Bit()); } } } void TerminalDisplay::dropMenuCdActionTriggered() { if (sender() != nullptr) { const QAction* action = qobject_cast(sender()); if (action != nullptr) { emit sendStringToEmu(action->data().toString().toLocal8Bit()); } } } void TerminalDisplay::doDrag() { _dragInfo.state = diDragging; _dragInfo.dragObject = new QDrag(this); auto mimeData = new QMimeData(); mimeData->setText(QApplication::clipboard()->mimeData(QClipboard::Selection)->text()); mimeData->setHtml(QApplication::clipboard()->mimeData(QClipboard::Selection)->html()); _dragInfo.dragObject->setMimeData(mimeData); _dragInfo.dragObject->exec(Qt::CopyAction); } void TerminalDisplay::setSessionController(SessionController* controller) { _sessionController = controller; } SessionController* TerminalDisplay::sessionController() { return _sessionController; } 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: { QMouseEvent* mouseEvent = static_cast(event); bool mouseInWidget = widget()->rect().contains(mouseEvent->pos()); if (mouseInWidget) { if (_timerId != 0) { killTimer(_timerId); } _timerId = 0; } else { if ((_timerId == 0) && ((mouseEvent->buttons() & Qt::LeftButton) != 0u)) { _timerId = startTimer(100); } } break; } case QEvent::MouseButtonRelease: { QMouseEvent* mouseEvent = static_cast(event); if ((_timerId != 0) && ((mouseEvent->buttons() & ~Qt::LeftButton) != 0u)) { killTimer(_timerId); _timerId = 0; } break; } default: break; }; return false; } diff --git a/src/TerminalDisplay.h b/src/TerminalDisplay.h index ed70f85c..b2469e3b 100644 --- a/src/TerminalDisplay.h +++ b/src/TerminalDisplay.h @@ -1,1078 +1,1081 @@ /* 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); /** * 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; } /** * Reimplemented. Has no effect. Use setVTFont() to change the font * used to draw characters in the display. */ virtual void setFont(const QFont &); /** Returns the font used to draw characters in the display */ QFont getVTFont() { return font(); } /** * Sets the font used to draw the display. Has no effect if @p font * is larger than the size of the display itself. */ void setVTFont(const QFont &f); /** Increases the font size */ void increaseFontSize(); /** Decreases the font size */ void decreaseFontSize(); + /** 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); } /** * 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); // 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: bool event(QEvent *event) Q_DECL_OVERRIDE; void paintEvent(QPaintEvent *pe) Q_DECL_OVERRIDE; void showEvent(QShowEvent *event) Q_DECL_OVERRIDE; void hideEvent(QHideEvent *event) Q_DECL_OVERRIDE; void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE; void contextMenuEvent(QContextMenuEvent *event) Q_DECL_OVERRIDE; virtual void fontChange(const QFont &); void focusInEvent(QFocusEvent *event) Q_DECL_OVERRIDE; void focusOutEvent(QFocusEvent *event) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; void keyReleaseEvent(QKeyEvent *event) Q_DECL_OVERRIDE; void leaveEvent(QEvent *event) Q_DECL_OVERRIDE; void mouseDoubleClickEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mousePressEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mouseReleaseEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; void mouseMoveEvent(QMouseEvent *ev) Q_DECL_OVERRIDE; virtual void extendSelection(const QPoint &position); void wheelEvent(QWheelEvent *ev) Q_DECL_OVERRIDE; bool focusNextPrevChild(bool next) Q_DECL_OVERRIDE; // drag and drop void dragEnterEvent(QDragEnterEvent *event) Q_DECL_OVERRIDE; void dropEvent(QDropEvent *event) Q_DECL_OVERRIDE; void doDrag(); enum DragState { diNone, diPending, diDragging }; struct DragInfo { DragState state; QPoint start; QDrag *dragObject; } _dragInfo; // classifies the 'ch' into one of three categories // and returns a character to indicate which category it is in // // - A space (returns ' ') // - Part of a word (returns 'a') // - Other characters (returns the input character) QChar charClass(const Character &ch) const; void clearImage(); void mouseTripleClickEvent(QMouseEvent *ev); void selectLine(QPoint pos, bool entireLine); // reimplemented void inputMethodEvent(QInputMethodEvent *event) Q_DECL_OVERRIDE; QVariant inputMethodQuery(Qt::InputMethodQuery query) const Q_DECL_OVERRIDE; void updateScrollBarPalette(); protected Q_SLOTS: void scrollBarPositionChanged(int value); void blinkTextEvent(); void blinkCursorEvent(); private Q_SLOTS: void swapFGBGColors(); void viewScrolledByUser(); /** * Called from the drag-and-drop popup. Causes the dropped URLs to be pasted as text. */ void dropMenuPasteActionTriggered(); void dropMenuCdActionTriggered(); void dismissOutputSuspendedMessage(); private: Q_DISABLE_COPY(TerminalDisplay) // -- Drawing helpers -- // divides the part of the display specified by 'rect' into // fragments according to their colors and styles and calls // drawTextFragment() or drawPrinterFriendlyTextFragment() // to draw the fragments void drawContents(QPainter &painter, const QRect &rect); // draw a transparent rectangle over the line of the current match void drawCurrentResultRect(QPainter &painter); // draws a section of text, all the text in this section // has a common color and style void drawTextFragment(QPainter &painter, const QRect &rect, const QString &text, const Character *style); void drawPrinterFriendlyTextFragment(QPainter &painter, const QRect &rect, const QString &text, const Character *style); // draws the background for a text fragment // if useOpacitySetting is true then the color's alpha value will be set to // the display's transparency (set with setOpacity()), otherwise the background // will be drawn fully opaque void drawBackground(QPainter &painter, const QRect &rect, const QColor &backgroundColor, bool useOpacitySetting); // draws the cursor character void drawCursor(QPainter &painter, const QRect &rect, const QColor &foregroundColor, const QColor &backgroundColor, bool &invertCharacterColor); // draws the characters or line graphics in a text fragment void drawCharacters(QPainter &painter, const QRect &rect, const QString &text, const Character *style, bool invertCharacterColor); // draws a string of line graphics void drawLineCharString(QPainter &painter, int x, int y, const QString &str, const Character *attributes); // draws the preedit string for input methods void drawInputMethodPreeditString(QPainter &painter, const QRect &rect); // -- // maps an area in the character image to an area on the widget QRect imageToWidget(const QRect &imageArea) const; // maps a point on the widget to the position ( ie. line and column ) // of the character at that point. 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 _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; ScrollState _scrollWheelState; IncrementalSearchBar *_searchBar; 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