Files
konsole/src/SessionController.cpp
Jekyll Wu cce19d8cb8 Allow searching scrollback in konsolepart
No keyboard shortcuts are provided because they often conflict with
actions from hosting applications. The only way to use this searching
feature is from context menu

FEATURE: 162319
FIXED-IN: 4.9.0
REVIEW: 104608

DIGEST:
2012-04-20 13:18:47 +08:00

1734 lines
57 KiB
C++

/*
Copyright 2006-2008 by Robert Knight <robertknight@gmail.com>
Copyright 2009 by Thomas Dreibholz <dreibh@iem.uni-due.de>
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"
// Qt
#include <QtGui/QApplication>
#include <QtGui/QMenu>
#include <QtGui/QKeyEvent>
// KDE
#include <KAction>
#include <KActionMenu>
#include <KActionCollection>
#include <KIcon>
#include <KLocalizedString>
#include <KMenu>
#include <KMessageBox>
#include <KRun>
#include <KShell>
#include <KToolInvocation>
#include <KStandardDirs>
#include <KToggleAction>
#include <KSelectAction>
#include <KUrl>
#include <KXmlGuiWindow>
#include <KXMLGUIFactory>
#include <KXMLGUIBuilder>
#include <KDebug>
#include <KUriFilter>
#include <KStringHandler>
#include <kcodecaction.h>
// 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"
// for SaveHistoryTask
#include <KFileDialog>
#include <KIO/Job>
#include <KJob>
#include "TerminalCharacterDecoder.h"
using namespace Konsole;
// TODO - Replace the icon choices below when suitable icons for silence and
// activity are available
const KIcon SessionController::_activityIcon("dialog-information");
const KIcon SessionController::_silenceIcon("dialog-information");
const KIcon SessionController::_broadcastIcon("emblem-important");
QSet<SessionController*> SessionController::_allControllers;
int SessionController::_lastControllerId;
SessionController::SessionController(Session* session , TerminalDisplay* view, QObject* parent)
: ViewProperties(parent)
, KXMLGUIClient()
, _session(session)
, _view(view)
, _copyToGroup(0)
, _profileList(0)
, _previousState(-1)
, _viewUrlFilter(0)
, _searchFilter(0)
, _copyInputToAllTabsAction(0)
, _searchToggleAction(0)
, _findNextAction(0)
, _findPreviousAction(0)
, _urlFilterUpdateRequired(false)
, _searchBar(0)
, _codecAction(0)
, _switchProfileMenu(0)
, _webSearchMenu(0)
, _listenForScreenWindowUpdates(false)
, _preventClose(false)
, _keepIconUntilInteraction(false)
{
Q_ASSERT(session);
Q_ASSERT(view);
// handle user interface related to session (menus etc.)
if (isKonsolePart()) {
setXMLFile("konsole/partui.rc");
setupCommonActions();
} else {
setXMLFile("konsole/sessionui.rc");
setupCommonActions();
setupExtraActions();
}
actionCollection()->addAssociatedWidget(view);
foreach(QAction * action, actionCollection()->actions()) {
action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
}
setIdentifier(++_lastControllerId);
sessionTitleChanged();
view->installEventFilter(this);
view->setSessionController(this);
// listen for session resize requests
connect(_session, SIGNAL(resizeRequest(QSize)), this,
SLOT(sessionResizeRequest(QSize)));
// listen for popup menu requests
connect(_view, SIGNAL(configureRequest(QPoint)), this,
SLOT(showDisplayContextMenu(QPoint)));
// move view to newest output when keystrokes occur
connect(_view, SIGNAL(keyPressedSignal(QKeyEvent*)), this,
SLOT(trackOutput(QKeyEvent*)));
// listen to activity / silence notifications from session
connect(_session, SIGNAL(stateChanged(int)), this,
SLOT(sessionStateChanged(int)));
// listen to title and icon changes
connect(_session, SIGNAL(titleChanged()), this, SLOT(sessionTitleChanged()));
connect(_session , SIGNAL(currentDirectoryChanged(QString)) ,
this , SIGNAL(currentDirectoryChanged(QString)));
// listen for color changes
connect(_session, SIGNAL(changeBackgroundColorRequest(QColor)), _view, SLOT(setBackgroundColor(QColor)));
connect(_session, SIGNAL(changeForegroundColorRequest(QColor)), _view, SLOT(setForegroundColor(QColor)));
// update the title when the session starts
connect(_session, SIGNAL(started()), this, SLOT(snapshot()));
// listen for output changes to set activity flag
connect(_session->emulation(), SIGNAL(outputChanged()), this,
SLOT(fireActivity()));
// listen for detection of ZModem transfer
connect(_session, SIGNAL(zmodemDetected()), this, SLOT(zmodemDownload()));
// listen for flow control status changes
connect(_session, SIGNAL(flowControlEnabledChanged(bool)), _view,
SLOT(setFlowControlWarningEnabled(bool)));
_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(1000);
connect(_interactionTimer, SIGNAL(timeout()), this, SLOT(snapshot()));
connect(_view, SIGNAL(keyPressedSignal(QKeyEvent*)), this, SLOT(interactionHandler()));
// take a snapshot of the session state periodically in the background
QTimer* backgroundTimer = new QTimer(_session);
backgroundTimer->setSingleShot(false);
backgroundTimer->setInterval(2000);
connect(backgroundTimer, SIGNAL(timeout()), this, SLOT(snapshot()));
backgroundTimer->start();
_allControllers.insert(this);
}
SessionController::~SessionController()
{
if (_view)
_view->setScreenWindow(0);
_allControllers.remove(this);
}
void SessionController::trackOutput(QKeyEvent* event)
{
Q_ASSERT(_view->screenWindow());
// jump to the end of the history buffer unless the key pressed
// is one of the three main modifiers, as these are used to select
// the selection mode (eg. Ctrl+Alt+<Left Click> for column/block selection)
switch (event->key()) {
case Qt::Key_Shift:
case Qt::Key_Control:
case Qt::Key_Alt:
break;
default:
_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::requireUrlFilterUpdate()
{
// this method is called every time the screen window's output changes, so do not
// do anything expensive here.
_urlFilterUpdateRequired = true;
}
void SessionController::snapshot()
{
Q_ASSERT(_session != 0);
QString title = _session->getDynamicTitle();
title = title.simplified();
// Visualize that the session is broadcasting to others
if (_copyToGroup && _copyToGroup->sessions().count() > 1) {
title.append('*');
}
// use the fallback title if needed
if (title.isEmpty()) {
title = _session->title(Session::NameRole);
}
// apply new title
_session->setTitle(Session::DisplayedTitleRole, title);
// do not forget icon
updateSessionIcon();
}
QString SessionController::currentDir() const
{
return _session->currentWorkingDirectory();
}
KUrl SessionController::url() const
{
return _session->getUrl();
}
void SessionController::rename()
{
renameSession();
}
void SessionController::openUrl(const KUrl& url)
{
// handle local paths
if (url.isLocalFile()) {
QString path = url.toLocalFile();
_session->emulation()->sendText("cd " + KShell::quoteArg(path) + '\r');
} else if (url.protocol().isEmpty()) {
// KUrl couldn't parse what the user entered into the URL field
// so just dump it to the shell
QString command = url.prettyUrl();
if (!command.isEmpty())
_session->emulation()->sendText(command + '\r');
} else if (url.protocol() == "ssh") {
QString sshCommand = "ssh ";
if (url.port() > -1) {
sshCommand += QString("-p %1 ").arg(url.port());
}
if (url.hasUser()) {
sshCommand += (url.user() + '@');
}
if (url.hasHost()) {
sshCommand += url.host();
}
_session->sendText(sshCommand + '\r');
} else if (url.protocol() == "telnet") {
QString telnetCommand = "telnet ";
if (url.hasUser()) {
telnetCommand += QString("-l %1 ").arg(url.user());
}
if (url.hasHost()) {
telnetCommand += (url.host() + ' ');
}
if (url.port() > -1) {
telnetCommand += QString::number(url.port());
}
_session->sendText(telnetCommand + '\r');
} else {
//TODO Implement handling for other Url types
KMessageBox::sorry(_view->window(),
i18n("Konsole does not know how to open the bookmark: ") +
url.prettyUrl());
kWarning() << "Unable to open bookmark at url" << url << ", I do not know"
<< " how to handle the protocol " << url.protocol();
}
}
void SessionController::setupPrimaryScreenSpecificActions(bool use)
{
KActionCollection* collection = actionCollection();
QAction* clearAction = collection->action("clear-history");
QAction* resetAction = collection->action("clear-history-and-reset");
QAction* selectAllAction = collection->action("select-all");
// these actions are meaningful only when primary screen is used.
clearAction->setEnabled(use);
resetAction->setEnabled(use);
selectAllAction->setEnabled(use);
}
void SessionController::selectionChanged(const QString& selectedText)
{
_selectedText = selectedText;
updateCopyAction(selectedText);
updateWebSearchMenu();
}
void SessionController::updateCopyAction(const QString& selectedText)
{
QAction* copyAction = actionCollection()->action("edit_copy");
// copy action is meaningful only when some text is selected.
copyAction->setEnabled(!selectedText.isEmpty());
}
void SessionController::updateWebSearchMenu()
{
// reset
_webSearchMenu->setVisible(false);
_webSearchMenu->menu()->clear();
if ( _selectedText.isEmpty() )
return ;
QString searchText = _selectedText;
searchText = searchText.replace('\n', ' ').replace('\r', ' ').simplified();
if (searchText.isEmpty())
return;
KUriFilterData filterData(searchText);
filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly);
if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) {
const QStringList searchProviders = filterData.preferredSearchProviders();
if (!searchProviders.isEmpty()) {
_webSearchMenu->setText(i18n("Search for '%1' with", KStringHandler::rsqueeze(searchText, 16)));
KAction* action = 0;
foreach(const QString& searchProvider, searchProviders)
{
action = new KAction(searchProvider, _webSearchMenu);
action->setIcon(KIcon(filterData.iconNameForPreferredSearchProvider(searchProvider)));
action->setData(filterData.queryForPreferredSearchProvider(searchProvider));
connect(action, SIGNAL(triggered()), this, SLOT(handleWebShortcutAction()));
_webSearchMenu->addAction(action);
}
_webSearchMenu->addSeparator();
action = new KAction(i18n("Configure Web Shortcuts..."), _webSearchMenu);
action->setIcon(KIcon("configure"));
connect(action, SIGNAL(triggered()), this, SLOT(configureWebShortcuts()));
_webSearchMenu->addAction(action);
_webSearchMenu->setVisible(true);
}
}
}
void SessionController::handleWebShortcutAction()
{
KAction* action = qobject_cast<KAction*>(sender());
if (!action)
return;
KUriFilterData filterData(action->data().toString());
if (KUriFilter::self()->filterUri(filterData, QStringList() << "kurisearchfilter")) {
const KUrl& url = filterData.uri();
new KRun(url, QApplication::activeWindow(), 0, true, true);
}
}
void SessionController::configureWebShortcuts()
{
KToolInvocation::kdeinitExec("kcmshell4", QStringList() << "ebrowsing");
}
bool SessionController::eventFilter(QObject* watched , QEvent* event)
{
if (watched == _view) {
if (event->type() == QEvent::FocusIn) {
// notify the world that the view associated with this session has been focused
// used by the view manager to update the title of the MainWindow widget containing the view
emit focused(this);
// when the view is focused, set bell events from the associated session to be delivered
// by the focused view
// first, disconnect any other views which are listening for bell signals from the session
disconnect(_session, SIGNAL(bellRequest(QString)), 0, 0);
// second, connect the newly focused view to listen for the session's bell signal
connect(_session, SIGNAL(bellRequest(QString)),
_view, SLOT(bell(QString)));
if (_copyInputToAllTabsAction && _copyInputToAllTabsAction->isChecked()) {
// A session with "Copy To All Tabs" has come into focus:
// Ensure that newly created sessions are included in _copyToGroup!
copyInputToAllTabs();
}
}
// when a mouse move is received, create the URL filter and listen for output changes if
// it has not already been created. If it already exists, then update only if the output
// has changed since the last update ( _urlFilterUpdateRequired == true )
//
// also check that no mouse buttons are pressed since the URL filter only applies when
// the mouse is hovering over the view
if (event->type() == QEvent::MouseMove &&
(!_viewUrlFilter || _urlFilterUpdateRequired) &&
((QMouseEvent*)event)->buttons() == Qt::NoButton) {
if (_view->screenWindow() && !_viewUrlFilter) {
connect(_view->screenWindow(), SIGNAL(scrolled(int)), this,
SLOT(requireUrlFilterUpdate()));
connect(_view->screenWindow(), SIGNAL(outputChanged()), this,
SLOT(requireUrlFilterUpdate()));
// install filter on the view to highlight URLs
_viewUrlFilter = new UrlFilter();
_view->filterChain()->addFilter(_viewUrlFilter);
}
_view->processFilters();
_urlFilterUpdateRequired = false;
}
}
return false;
}
void SessionController::removeSearchFilter()
{
if (!_searchFilter)
return;
_view->filterChain()->removeFilter(_searchFilter);
delete _searchFilter;
_searchFilter = 0;
}
void SessionController::setSearchBar(IncrementalSearchBar* searchBar)
{
// disconnect the existing search bar
if (_searchBar) {
disconnect(this, 0, _searchBar, 0);
disconnect(_searchBar, 0, this, 0);
}
// remove any existing search filter
removeSearchFilter();
// connect new search bar
_searchBar = searchBar;
if (_searchBar) {
connect(_searchBar, SIGNAL(closeClicked()), this, SLOT(searchClosed()));
connect(_searchBar, SIGNAL(findNextClicked()), this, SLOT(findNextInHistory()));
connect(_searchBar, SIGNAL(findPreviousClicked()), this, SLOT(findPreviousInHistory()));
connect(_searchBar, SIGNAL(highlightMatchesToggled(bool)) , this , SLOT(highlightMatches(bool)));
connect(_searchBar, SIGNAL(matchCaseToggled(bool)), this, SLOT(changeSearchMatch()));
// if the search bar was previously active
// then re-enter search mode
searchHistory(_searchToggleAction->isChecked());
}
}
IncrementalSearchBar* SessionController::searchBar() const
{
return _searchBar;
}
void SessionController::setShowMenuAction(QAction* action)
{
actionCollection()->addAction("show-menubar", action);
}
void SessionController::setupCommonActions()
{
KAction* action = 0;
KActionCollection* collection = actionCollection();
// Close Session
action = collection->addAction("close-session", this, SLOT(closeSession()));
action->setText(i18n("&Close Tab"));
action->setIcon(KIcon("tab-close"));
action->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_W));
// Open Browser
action = collection->addAction("open-browser", this, SLOT(openBrowser()));
action->setText(i18n("Open File Manager"));
action->setIcon(KIcon("system-file-manager"));
// Copy and Paste
action = KStandardAction::copy(this, SLOT(copy()), collection);
action->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_C));
// disabled at first, since nothing has been selected now
action->setEnabled(false);
action = KStandardAction::paste(this, SLOT(paste()), collection);
KShortcut pasteShortcut = action->shortcut();
pasteShortcut.setPrimary(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_V));
pasteShortcut.setAlternate(QKeySequence(Qt::SHIFT + Qt::Key_Insert));
action->setShortcut(pasteShortcut);
action = collection->addAction("paste-selection", this, SLOT(pasteFromXSelection()));
action->setText(i18n("Paste Selection"));
action->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Insert));
_webSearchMenu = new KActionMenu(i18n("Web Search"), _view);
_webSearchMenu->setIcon(KIcon("preferences-web-browser-shortcuts"));
_webSearchMenu->setVisible(false);
collection->addAction("web-search", _webSearchMenu);
action = collection->addAction("select-all", this, SLOT(selectAll()));
action->setText(i18n("&Select All"));
action->setIcon(KIcon("edit-select-all"));
action = KStandardAction::saveAs(this, SLOT(saveHistory()), collection);
action->setText(i18n("Save Output &As..."));
action = collection->addAction("adjust-history", this, SLOT(showHistoryOptions()));
action->setText(i18n("Adjust Scrollback..."));
action->setIcon(KIcon("configure"));
action = collection->addAction("clear-history", this, SLOT(clearHistory()));
action->setText(i18n("Clear Scrollback"));
action->setIcon(KIcon("edit-clear-history"));
action = collection->addAction("clear-history-and-reset", this, SLOT(clearHistoryAndReset()));
action->setText(i18n("Clear Scrollback and Reset"));
action->setIcon(KIcon("edit-clear-history"));
action->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_K));
// Profile Options
action = collection->addAction("edit-current-profile", this, SLOT(editCurrentProfile()));
action->setText(i18n("Edit Current Profile..."));
action->setIcon(KIcon("document-properties"));
_switchProfileMenu = new KActionMenu(i18n("Switch Profile"), _view);
collection->addAction("switch-profile", _switchProfileMenu);
connect(_switchProfileMenu->menu(), SIGNAL(aboutToShow()), this, SLOT(prepareSwitchProfileMenu()));
// History
_searchToggleAction = KStandardAction::find(this, 0, collection);
_searchToggleAction->setShortcut(QKeySequence());
_searchToggleAction->setCheckable(true);
connect(_searchToggleAction, SIGNAL(toggled(bool)), this, SLOT(searchHistory(bool)));
_findNextAction = KStandardAction::findNext(this, SLOT(findNextInHistory()), collection);
_findNextAction->setShortcut(QKeySequence());
_findNextAction->setEnabled(false);
_findPreviousAction = KStandardAction::findPrev(this, SLOT(findPreviousInHistory()), collection);
_findPreviousAction->setShortcut(QKeySequence());
_findPreviousAction->setEnabled(false);
}
void SessionController::setupExtraActions()
{
KAction* action = 0;
KToggleAction* toggleAction = 0;
KActionCollection* collection = actionCollection();
// Rename Session
action = collection->addAction("rename-session", this, SLOT(renameSession()));
action->setText(i18n("&Rename Tab..."));
action->setIcon(KIcon("edit-rename"));
action->setShortcut(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_S));
// Copy input to ==> all tabs
KToggleAction* copyInputToAllTabsAction = collection->add<KToggleAction>("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<KToggleAction>("copy-input-to-selected-tabs");
copyInputToSelectedTabsAction->setText(i18n("&Select Tabs..."));
copyInputToSelectedTabsAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Period));
copyInputToSelectedTabsAction->setData(CopyInputToSelectedTabsMode);
// Copy input to ==> none
KToggleAction* copyInputToNoneAction = collection->add<KToggleAction>("copy-input-to-none");
copyInputToNoneAction->setText(i18nc("@action:inmenu Do not select any tabs", "&None"));
copyInputToNoneAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Slash));
copyInputToNoneAction->setData(CopyInputToNoneMode);
copyInputToNoneAction->setChecked(true); // the default state
// The "Copy Input To" submenu
// The above three choices are represented as combo boxes
KSelectAction* copyInputActions = collection->add<KSelectAction>("copy-input-to");
copyInputActions->setText(i18n("Copy Input To"));
copyInputActions->addAction(copyInputToAllTabsAction);
copyInputActions->addAction(copyInputToSelectedTabsAction);
copyInputActions->addAction(copyInputToNoneAction);
connect(copyInputActions, SIGNAL(triggered(QAction*)), this, SLOT(copyInputActionsTriggered(QAction*)));
action = collection->addAction("zmodem-upload", this, SLOT(zmodemUpload()));
action->setText(i18n("&ZModem Upload..."));
action->setIcon(KIcon("document-open"));
action->setShortcut(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_U));
// Monitor
toggleAction = new KToggleAction(i18n("Monitor for &Activity"), this);
toggleAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_A));
action = collection->addAction("monitor-activity", toggleAction);
connect(action, SIGNAL(toggled(bool)), this, SLOT(monitorActivity(bool)));
toggleAction = new KToggleAction(i18n("Monitor for &Silence"), this);
toggleAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_I));
action = collection->addAction("monitor-silence", toggleAction);
connect(action, SIGNAL(toggled(bool)), this, SLOT(monitorSilence(bool)));
// Character Encoding
_codecAction = new KCodecAction(i18n("Set &Encoding"), this);
_codecAction->setIcon(KIcon("character-set"));
collection->addAction("set-encoding", _codecAction);
connect(_codecAction->menu(), SIGNAL(aboutToShow()), this, SLOT(updateCodecAction()));
connect(_codecAction, SIGNAL(triggered(QTextCodec*)), this, SLOT(changeCodec(QTextCodec*)));
// Text Size
action = collection->addAction("enlarge-font", this, SLOT(increaseTextSize()));
action->setText(i18n("Enlarge Font"));
action->setIcon(KIcon("format-font-size-more"));
KShortcut enlargeFontShortcut = action->shortcut();
enlargeFontShortcut.setPrimary(QKeySequence(Qt::CTRL + Qt::Key_Plus));
enlargeFontShortcut.setAlternate(QKeySequence(Qt::CTRL + Qt::Key_Equal));
action->setShortcut(enlargeFontShortcut);
action = collection->addAction("shrink-font", this, SLOT(decreaseTextSize()));
action->setText(i18n("Shrink Font"));
action->setIcon(KIcon("format-font-size-less"));
action->setShortcut(KShortcut(Qt::CTRL | Qt::Key_Minus));
_searchToggleAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_F));
_findPreviousAction->setShortcut(QKeySequence(Qt::Key_F3));
_findPreviousAction->setShortcut(QKeySequence(Qt::SHIFT + Qt::Key_F3));
}
void SessionController::switchProfile(Profile::Ptr profile)
{
SessionManager::instance()->setSessionProfile(_session, profile);
}
void SessionController::prepareSwitchProfileMenu()
{
if (_switchProfileMenu->menu()->isEmpty()) {
_profileList = new ProfileList(false, this);
connect(_profileList, SIGNAL(profileSelected(Profile::Ptr)), this, SLOT(switchProfile(Profile::Ptr)));
}
_switchProfileMenu->menu()->clear();
_switchProfileMenu->menu()->addActions(_profileList->actions());
}
void SessionController::updateCodecAction()
{
_codecAction->setCurrentCodec(QString(_session->codec()));
}
void SessionController::changeCodec(QTextCodec* codec)
{
_session->setCodec(codec);
}
void SessionController::editCurrentProfile()
{
EditProfileDialog* dialog = new EditProfileDialog(QApplication::activeWindow());
dialog->setProfile(SessionManager::instance()->sessionProfile(_session));
dialog->show();
}
void SessionController::renameSession()
{
QScopedPointer<RenameTabDialog> dialog(new RenameTabDialog(QApplication::activeWindow()));
dialog->setTabTitleText(_session->tabTitleFormat(Session::LocalTabTitle));
dialog->setRemoteTabTitleText(_session->tabTitleFormat(Session::RemoteTabTitle));
if (_session->isRemote()) {
dialog->focusRemoteTabTitleText();
} else {
dialog->focusTabTitleText();
}
QPointer<Session> guard(_session);
int result = dialog->exec();
if (!guard)
return;
if (result) {
QString tabTitle = dialog->tabTitleText();
QString remoteTabTitle = dialog->remoteTabTitleText();
_session->setTabTitleFormat(Session::LocalTabTitle, tabTitle);
_session->setTabTitleFormat(Session::RemoteTabTitle, remoteTabTitle);
// trigger an update of the tab text
snapshot();
}
}
bool SessionController::confirmClose() const
{
if (_session->isForegroundProcessActive()) {
QString title = _session->foregroundProcessName();
// hard coded for now. In future make it possible for the user to specify which programs
// are ignored when considering whether to display a confirmation
QStringList ignoreList;
ignoreList << QString(qgetenv("SHELL")).section('/', -1);
if (ignoreList.contains(title))
return true;
QString question;
if (title.isEmpty())
question = i18n("A program is currently running in this session."
" Are you sure you want to close it?");
else
question = i18n("The program '%1' is currently running in this session."
" Are you sure you want to close it?", title);
int result = KMessageBox::warningYesNo(_view->window(), question, i18n("Confirm Close"));
return (result == KMessageBox::Yes) ? true : false;
}
return true;
}
bool SessionController::confirmForceClose() const
{
if (_session->isRunning()) {
QString title = _session->program();
// hard coded for now. In future make it possible for the user to specify which programs
// are ignored when considering whether to display a confirmation
QStringList ignoreList;
ignoreList << QString(qgetenv("SHELL")).section('/', -1);
if (ignoreList.contains(title))
return true;
QString question;
if (title.isEmpty())
question = i18n("A program in this session would not die."
" Are you sure you want to kill it by force?");
else
question = i18n("The program '%1' is in this session would not die."
" Are you sure you want to kill it by force?", title);
int result = KMessageBox::warningYesNo(_view->window(), question, i18n("Confirm Close"));
return (result == KMessageBox::Yes) ? true : false;
}
return true;
}
void SessionController::closeSession()
{
if (_preventClose)
return;
if (confirmClose()) {
if (_session->closeInNormalWay()) {
return;
} else if (confirmForceClose()) {
if (_session->closeInForceWay())
return;
else
kWarning() << "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()
{
KUrl currentUrl = url();
if (currentUrl.isLocalFile())
new KRun(currentUrl, QApplication::activeWindow(), 0, true, true);
else
new KRun(KUrl(QDir::homePath()), QApplication::activeWindow(), 0, true, true);
}
void SessionController::copy()
{
_view->copyToClipboard();
}
void SessionController::paste()
{
_view->pasteFromClipboard();
}
void SessionController::pasteFromXSelection()
{
_view->pasteFromXSelection();
}
void SessionController::selectAll()
{
ScreenWindow * screenWindow = _view->screenWindow();
screenWindow->setSelectionByLineRange(0, _session->emulation()->lineCount());
TerminalDisplay::setXSelection(screenWindow->selectedText(true));
}
static const KXmlGuiWindow* findWindow(const QObject* object)
{
// Walk up the QObject hierarchy to find a KXmlGuiWindow.
while (object != 0) {
const KXmlGuiWindow* window = qobject_cast<const KXmlGuiWindow*>(object);
if (window != 0) {
return(window);
}
object = object->parent();
}
return(0);
}
static bool hasTerminalDisplayInSameWindow(const Session* session, const KXmlGuiWindow* window)
{
// Iterate all TerminalDisplays of this Session ...
foreach(const TerminalDisplay* terminalDisplay, session->views()) {
// ... and check whether a TerminalDisplay has the same
// window as given in the parameter
if (window == findWindow(terminalDisplay)) {
return(true);
}
}
return(false);
}
void SessionController::copyInputActionsTriggered(QAction* action)
{
const int mode = action->data().value<int>();
switch (mode) {
case CopyInputToAllTabsMode:
copyInputToAllTabs();
break;
case CopyInputToSelectedTabsMode:
copyInputToSelectedTabs();
break;
case CopyInputToNoneMode:
copyInputToNone();
break;
default:
Q_ASSERT(false);
}
}
void SessionController::copyInputToAllTabs()
{
if (!_copyToGroup) {
_copyToGroup = new SessionGroup(this);
}
// Find our window ...
const KXmlGuiWindow* myWindow = findWindow(_view);
QSet<Session*> group =
QSet<Session*>::fromList(SessionManager::instance()->sessions());
for (QSet<Session*>::iterator iterator = group.begin();
iterator != group.end(); ++iterator) {
Session* session = *iterator;
// First, ensure that the session is removed
// (necessary to avoid duplicates on addSession()!)
_copyToGroup->removeSession(session);
// Add current session if it is displayed our window
if (hasTerminalDisplayInSameWindow(session, myWindow)) {
_copyToGroup->addSession(session);
}
}
_copyToGroup->setMasterStatus(_session, true);
_copyToGroup->setMasterMode(SessionGroup::CopyInputToAll);
snapshot();
}
void SessionController::copyInputToSelectedTabs()
{
if (!_copyToGroup) {
_copyToGroup = new SessionGroup(this);
_copyToGroup->addSession(_session);
_copyToGroup->setMasterStatus(_session, true);
_copyToGroup->setMasterMode(SessionGroup::CopyInputToAll);
}
QPointer<CopyInputDialog> dialog = new CopyInputDialog(_view);
dialog->setMasterSession(_session);
QSet<Session*> currentGroup = QSet<Session*>::fromList(_copyToGroup->sessions());
currentGroup.remove(_session);
dialog->setChosenSessions(currentGroup);
QPointer<Session> guard(_session);
int result = dialog->exec();
if (!guard)
return;
if (result == QDialog::Accepted) {
QSet<Session*> newGroup = dialog->chosenSessions();
newGroup.remove(_session);
QSet<Session*> completeGroup = newGroup | currentGroup;
foreach(Session * session, completeGroup) {
if (newGroup.contains(session) && !currentGroup.contains(session))
_copyToGroup->addSession(session);
else if (!newGroup.contains(session) && currentGroup.contains(session))
_copyToGroup->removeSession(session);
}
_copyToGroup->setMasterStatus(_session, true);
_copyToGroup->setMasterMode(SessionGroup::CopyInputToAll);
snapshot();
}
}
void SessionController::copyInputToNone()
{
if (!_copyToGroup) // No 'Copy To' is active
return;
QSet<Session*> group =
QSet<Session*>::fromList(SessionManager::instance()->sessions());
for (QSet<Session*>::iterator iterator = group.begin();
iterator != group.end(); ++iterator) {
Session* session = *iterator;
if (session != _session) {
_copyToGroup->removeSession(*iterator);
}
}
delete _copyToGroup;
_copyToGroup = 0;
snapshot();
}
void SessionController::searchClosed()
{
_searchToggleAction->toggle();
}
void SessionController::listenForScreenWindowUpdates()
{
if (_listenForScreenWindowUpdates)
return;
connect(_view->screenWindow(), SIGNAL(outputChanged()), this,
SLOT(updateSearchFilter()));
connect(_view->screenWindow(), SIGNAL(scrolled(int)), this,
SLOT(updateSearchFilter()));
_listenForScreenWindowUpdates = true;
}
void SessionController::updateSearchFilter()
{
if (_searchFilter) {
Q_ASSERT(searchBar());
_view->processFilters();
}
}
// 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)
{
if (_searchBar) {
_searchBar->setVisible(showSearchBar);
if (showSearchBar) {
removeSearchFilter();
listenForScreenWindowUpdates();
_searchFilter = new RegExpFilter();
_view->filterChain()->addFilter(_searchFilter);
connect(_searchBar, SIGNAL(searchChanged(QString)), this,
SLOT(searchTextChanged(QString)));
// invoke search for matches for the current search text
const QString& currentSearchText = _searchBar->searchText();
if (!currentSearchText.isEmpty()) {
searchTextChanged(currentSearchText);
}
setFindNextPrevEnabled(true);
} else {
setFindNextPrevEnabled(false);
disconnect(_searchBar, SIGNAL(searchChanged(QString)), this,
SLOT(searchTextChanged(QString)));
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 (text.isEmpty())
_view->screenWindow()->clearSelection();
// update search. this is called even when the text is
// empty to clear the view's filters
beginSearch(text , SearchHistoryTask::ForwardsSearch);
}
void SessionController::searchCompleted(bool success)
{
if (_searchBar)
_searchBar->setFoundMatch(success);
}
void SessionController::beginSearch(const QString& text , int direction)
{
Q_ASSERT(_searchBar);
Q_ASSERT(_searchFilter);
QBitArray options = _searchBar->optionsChecked();
Qt::CaseSensitivity caseHandling = options.at(IncrementalSearchBar::MatchCase) ? Qt::CaseSensitive : Qt::CaseInsensitive;
QRegExp::PatternSyntax syntax = options.at(IncrementalSearchBar::RegExp) ? QRegExp::RegExp : QRegExp::FixedString;
QRegExp regExp(text , caseHandling , syntax);
_searchFilter->setRegExp(regExp);
if (!regExp.isEmpty()) {
SearchHistoryTask* task = new SearchHistoryTask(this);
connect(task, SIGNAL(completed(bool)), this, SLOT(searchCompleted(bool)));
task->setRegExp(regExp);
task->setSearchDirection((SearchHistoryTask::SearchDirection)direction);
task->setAutoDelete(true);
task->addScreenWindow(_session , _view->screenWindow());
task->execute();
}
_view->processFilters();
}
void SessionController::highlightMatches(bool highlight)
{
if (highlight) {
_view->filterChain()->addFilter(_searchFilter);
_view->processFilters();
} else {
_view->filterChain()->removeFilter(_searchFilter);
}
_view->update();
}
void SessionController::findNextInHistory()
{
Q_ASSERT(_searchBar);
Q_ASSERT(_searchFilter);
beginSearch(_searchBar->searchText(), SearchHistoryTask::ForwardsSearch);
}
void SessionController::findPreviousInHistory()
{
Q_ASSERT(_searchBar);
Q_ASSERT(_searchFilter);
beginSearch(_searchBar->searchText(), SearchHistoryTask::BackwardsSearch);
}
void SessionController::changeSearchMatch()
{
Q_ASSERT(_searchBar);
Q_ASSERT(_searchFilter);
// reset Selection for new case match
_view->screenWindow()->clearSelection();
beginSearch(_searchBar->searchText(), SearchHistoryTask::ForwardsSearch);
}
void SessionController::showHistoryOptions()
{
QScopedPointer<HistorySizeDialog> 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<Session> guard(_session);
int result = dialog->exec();
if (!guard)
return;
if (result) {
scrollBackOptionsChanged(dialog->mode(), dialog->lineCount());
}
}
void SessionController::sessionResizeRequest(const QSize& size)
{
//kDebug() << "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::saveHistory()
{
SessionTask* task = new SaveHistoryTask(this);
task->setAutoDelete(true);
task->addSession(_session);
task->execute();
}
void SessionController::clearHistory()
{
_session->clearHistory();
_view->updateImage(); // To reset view scrollbar
}
void SessionController::clearHistoryAndReset()
{
Profile::Ptr profile = SessionManager::instance()->sessionProfile(_session);
QByteArray name = profile->defaultEncoding().toUtf8();
Emulation* emulation = _session->emulation();
emulation->reset();
_session->refresh();
_session->setCodec(QTextCodec::codecForName(name));
clearHistory();
}
void SessionController::increaseTextSize()
{
QFont font = _view->getVTFont();
font.setPointSizeF(font.pointSizeF() + 1);
_view->setVTFont(font);
}
void SessionController::decreaseTextSize()
{
static const qreal MinimumFontSize = 6;
QFont font = _view->getVTFont();
font.setPointSizeF(qMax(font.pointSizeF() - 1, MinimumFontSize));
_view->setVTFont(font);
}
void SessionController::monitorActivity(bool monitor)
{
_session->setMonitorActivity(monitor);
}
void SessionController::monitorSilence(bool monitor)
{
_session->setMonitorSilence(monitor);
}
void SessionController::updateSessionIcon()
{
// Visualize that the session is broadcasting to others
if (_copyToGroup && _copyToGroup->sessions().count() > 1) {
// Master Mode: set different icon, to warn the user to be careful
setIcon(_broadcastIcon);
} else {
if (!_keepIconUntilInteraction) {
// Not in Master Mode: use normal icon
setIcon(_sessionIcon);
}
}
}
void SessionController::sessionTitleChanged()
{
if (_sessionIconName != _session->iconName()) {
_sessionIconName = _session->iconName();
_sessionIcon = KIcon(_sessionIconName);
updateSessionIcon();
}
QString title = _session->title(Session::DisplayedTitleRole);
// special handling for the "%w" marker which is replaced with the
// window title set by the shell
title.replace("%w", _session->userTitle());
// special handling for the "%#" marker which is replaced with the
// number of the shell
title.replace("%#", QString::number(_session->sessionId()));
if (title.isEmpty())
title = _session->title(Session::NameRole);
setTitle(title);
emit rawTitleChanged();
}
void SessionController::showDisplayContextMenu(const QPoint& position)
{
// needed to make sure the popup menu is available, even if a hosting
// application did not merge our GUI.
if (!factory()) {
if (!clientBuilder()) {
setClientBuilder(new KXMLGUIBuilder(_view));
}
KXMLGUIFactory* factory = new KXMLGUIFactory(clientBuilder(), this);
factory->addClient(this);
//kDebug() << "Created xmlgui factory" << factory;
}
QPointer<QMenu> popup = qobject_cast<QMenu*>(factory()->container("session-popup-menu", this));
if (popup) {
// prepend content-specific actions such as "Open Link", "Copy Email Address" etc.
QList<QAction*> contentActions = _view->filterActions(position);
QAction* contentSeparator = new QAction(popup);
contentSeparator->setSeparator(true);
contentActions << contentSeparator;
popup->insertActions(popup->actions().value(0, 0), contentActions);
// always update this submenu before showing the context menu,
// because the available search services might have changed
// since the context menu is shown last time
updateWebSearchMenu();
_preventClose = true;
QAction* chosen = popup->exec(_view->mapToGlobal(position));
// check for validity of the pointer to the popup menu
if (popup) {
// Remove content-specific actions
//
// If the close action was chosen, the popup menu will be partially
// destroyed at this point, and the rest will be destroyed later by
// 'chosen->trigger()'
foreach(QAction * action, contentActions) {
popup->removeAction(action);
}
delete contentSeparator;
}
_preventClose = false;
if (chosen && chosen->objectName() == "close-session")
chosen->trigger();
} else {
kWarning() << "Unable to display popup menu for session"
<< _session->title(Session::NameRole)
<< ", no GUI factory available to build the popup.";
}
}
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 = KIcon(_sessionIconName);
}
updateSessionIcon();
}
_previousState = state;
}
void SessionController::zmodemDownload()
{
QString zmodem = KStandardDirs::findExe("rz");
if (zmodem.isEmpty()) {
zmodem = KStandardDirs::findExe("lrz");
}
if (!zmodem.isEmpty()) {
const QString path = KFileDialog::getExistingDirectory(
QString(), _view,
i18n("Save ZModem Download to..."));
if (!path.isEmpty()) {
_session->startZModem(zmodem, path, QStringList());
return;
}
} else {
KMessageBox::error(_view,
i18n("<p>A ZModem file transfer attempt has been detected, "
"but no suitable ZModem software was found on this system.</p>"
"<p>You may wish to install the 'rzsz' or 'lrzsz' package.</p>"));
}
_session->cancelZModem();
return;
}
void SessionController::zmodemUpload()
{
if (_session->isZModemBusy()) {
KMessageBox::sorry(_view,
i18n("<p>The current session already has a ZModem file transfer in progress.</p>"));
return;
}
QString zmodem = KStandardDirs::findExe("sz");
if (zmodem.isEmpty()) {
zmodem = KStandardDirs::findExe("lsz");
}
if (zmodem.isEmpty()) {
KMessageBox::sorry(_view,
i18n("<p>No suitable ZModem software was found on this system.</p>"
"<p>You may wish to install the 'rzsz' or 'lrzsz' package.</p>"));
return;
}
QStringList files = KFileDialog::getOpenFileNames(KUrl(), QString(), _view,
i18n("Select Files for ZModem Upload"));
if (!files.isEmpty()) {
_session->startZModem(zmodem, QString(), files);
}
}
bool SessionController::isKonsolePart() const
{
// Check to see if we are being called from Konsole or a KPart
if (QString(qApp->metaObject()->className()) == "Konsole::Application")
return false;
else
return true;
}
SessionTask::SessionTask(QObject* parent)
: QObject(parent)
, _autoDelete(false)
{
}
void SessionTask::setAutoDelete(bool enable)
{
_autoDelete = enable;
}
bool SessionTask::autoDelete() const
{
return _autoDelete;
}
void SessionTask::addSession(Session* session)
{
_sessions << session;
}
QList<SessionPtr> SessionTask::sessions() const
{
return _sessions;
}
SaveHistoryTask::SaveHistoryTask(QObject* parent)
: SessionTask(parent)
{
}
SaveHistoryTask::~SaveHistoryTask()
{
}
void SaveHistoryTask::execute()
{
// TODO - think about the UI when saving multiple history sessions, if there are more than two or
// three then providing a URL for each one will be tedious
// TODO - show a warning ( preferably passive ) if saving the history output fails
//
KFileDialog* dialog = new KFileDialog(QString(":konsole") /* check this */,
QString(), QApplication::activeWindow());
dialog->setOperationMode(KFileDialog::Saving);
dialog->setConfirmOverwrite(true);
QStringList mimeTypes;
mimeTypes << "text/plain";
mimeTypes << "text/html";
dialog->setMimeFilter(mimeTypes, "text/plain");
// 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->setCaption(i18n("Save Output From %1", session->title(Session::NameRole)));
int result = dialog->exec();
if (result != QDialog::Accepted)
continue;
KUrl url = dialog->selectedUrl();
if (!url.isValid()) {
// UI: Can we make this friendlier?
KMessageBox::sorry(0 , i18n("%1 is an invalid URL, the output could not be saved.", url.url()));
continue;
}
KIO::TransferJob* job = KIO::put(url,
-1, // no special permissions
// overwrite existing files
// do not resume an existing transfer
// show progress information only for remote
// URLs
KIO::Overwrite | (url.isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags)
// a better solution would be to show progress
// information after a certain period of time
// instead, since the overall speed of transfer
// depends on factors other than just the protocol
// used
);
SaveJob jobInfo;
jobInfo.session = session;
jobInfo.lastLineFetched = -1; // when each request for data comes in from the KIO subsystem
// lastLineFetched is used to keep track of how much of the history
// has already been sent, and where the next request should continue
// from.
// this is set to -1 to indicate the job has just been started
if (dialog->currentMimeFilter() == "text/html")
jobInfo.decoder = new HTMLDecoder();
else
jobInfo.decoder = new PlainTextDecoder();
_jobSession.insert(job, jobInfo);
connect(job, SIGNAL(dataReq(KIO::Job*,QByteArray&)),
this, SLOT(jobDataRequested(KIO::Job*,QByteArray&)));
connect(job, SIGNAL(result(KJob*)),
this, SLOT(jobResult(KJob*)));
}
dialog->deleteLater();
}
void SaveHistoryTask::jobDataRequested(KIO::Job* job , QByteArray& data)
{
// TODO - Report progress information for the job
// PERFORMANCE: Do some tests and tweak this value to get faster saving
const int LINES_PER_REQUEST = 500;
SaveJob& info = _jobSession[job];
// transfer LINES_PER_REQUEST lines from the session's history
// to the save location
if (info.session) {
// note: when retrieving lines from the emulation,
// the first line is at index 0.
int sessionLines = info.session->emulation()->lineCount();
if (sessionLines - 1 == info.lastLineFetched)
return; // if there is no more data to transfer then stop the job
int copyUpToLine = qMin(info.lastLineFetched + LINES_PER_REQUEST ,
sessionLines - 1);
QTextStream stream(&data, QIODevice::ReadWrite);
info.decoder->begin(&stream);
info.session->emulation()->writeToStream(info.decoder , info.lastLineFetched + 1 , copyUpToLine);
info.decoder->end();
info.lastLineFetched = copyUpToLine;
}
}
void SaveHistoryTask::jobResult(KJob* job)
{
if (job->error()) {
KMessageBox::sorry(0 , i18n("A problem occurred when saving the output.\n%1", job->errorString()));
}
TerminalCharacterDecoder * decoder = _jobSession[job].decoder;
_jobSession.remove(job);
delete decoder;
// notify the world that the task is done
emit completed(true);
if (autoDelete())
deleteLater();
}
void SearchHistoryTask::addScreenWindow(Session* session , ScreenWindow* searchWindow)
{
_windows.insert(session, searchWindow);
}
void SearchHistoryTask::execute()
{
QMapIterator< SessionPtr , ScreenWindowPtr > iter(_windows);
while (iter.hasNext()) {
iter.next();
executeOnScreenWindow(iter.key() , iter.value());
}
}
void SearchHistoryTask::executeOnScreenWindow(SessionPtr session , ScreenWindowPtr window)
{
Q_ASSERT(session);
Q_ASSERT(window);
Emulation* emulation = session->emulation();
int selectionColumn = 0;
int selectionLine = 0;
window->getSelectionEnd(selectionColumn , selectionLine);
if (!_regExp.isEmpty()) {
int pos = -1;
const bool forwards = (_direction == ForwardsSearch);
int startLine = selectionLine + window->currentLine() + (forwards ? 1 : -1);
// Temporary fix for #205495
if (startLine < 0) startLine = 0;
const int lastLine = window->lineCount() - 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 <delta> lines.
do {
// ensure that application does not appear to hang
// if searching through a lengthy output
QApplication::processEvents();
// calculate lines to search in this iteration
if (hasWrapped) {
if (endLine == lastLine)
line = 0;
else if (endLine == 0)
line = lastLine;
endLine += delta;
if (forwards)
endLine = qMin(startLine , endLine);
else
endLine = qMax(startLine , endLine);
} else {
endLine += delta;
if (endLine > lastLine) {
hasWrapped = true;
endLine = lastLine;
} else if (endLine < 0) {
hasWrapped = true;
endLine = 0;
}
}
decoder.begin(&searchStream);
emulation->writeToStream(&decoder, qMin(endLine, line) , qMax(endLine, line));
decoder.end();
// line number search below assumes that the buffer ends with a new-line
string.append('\n');
pos = -1;
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<int> 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.
//kDebug() << "Found result at line " << findPos;
//update display to show area of history containing selection
window->scrollTo(findPos);
window->setSelectionStart(0 , findPos - window->currentLine() , false);
window->setSelectionEnd(window->columnCount() , findPos - window->currentLine());
window->setTrackOutput(false);
window->notifyOutputChanged();
}
SearchHistoryTask::SearchHistoryTask(QObject* parent)
: SessionTask(parent)
, _direction(ForwardsSearch)
{
}
void SearchHistoryTask::setSearchDirection(SearchDirection direction)
{
_direction = direction;
}
SearchHistoryTask::SearchDirection SearchHistoryTask::searchDirection() const
{
return _direction;
}
void SearchHistoryTask::setRegExp(const QRegExp& expression)
{
_regExp = expression;
}
QRegExp SearchHistoryTask::regExp() const
{
return _regExp;
}
QString SessionController::userTitle() const
{
if (_session) {
return _session->userTitle();
} else {
return QString();
}
}
#include "SessionController.moc"