/* 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" // Qt #include #include // KDE #include #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 "IncrementalSearchBar.h" #include "RenameTabsDialog.h" #include "ScreenWindow.h" #include "Session.h" #include "ProfileList.h" #include "TerminalDisplay.h" #include "SessionManager.h" // for SaveHistoryTask #include #include #include #include "TerminalCharacterDecoder.h" using namespace Konsole; KIcon SessionController::_activityIcon; KIcon SessionController::_silenceIcon; QSet SessionController::_allControllers; QPointer SearchHistoryTask::_thread; 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) , _searchToggleAction(0) , _findNextAction(0) , _findPreviousAction(0) , _urlFilterUpdateRequired(false) , _codecAction(0) , _changeProfileMenu(0) , _listenForScreenWindowUpdates(false) , _preventClose(false) { _allControllers.insert(this); Q_ASSERT( session ); Q_ASSERT( view ); // handle user interface related to session (menus etc.) if (isKonsolePart()) setXMLFile("konsole/partui.rc"); else setXMLFile("konsole/sessionui.rc"); setupActions(); 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()) ); // 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 QTimer* activityTimer = new QTimer(_session); activityTimer->setSingleShot(true); activityTimer->setInterval(2000); connect( _view , SIGNAL(keyPressedSignal(QKeyEvent*)) , activityTimer , SLOT(start()) ); connect( activityTimer , SIGNAL(timeout()) , this , SLOT(snapshot()) ); } void SessionController::updateSearchFilter() { if ( _searchFilter ) { Q_ASSERT( searchBar() && searchBar()->isVisible() ); _view->processFilters(); } } 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+ 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::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('*'); } updateSessionIcon(); // apply new title if ( !title.isEmpty() ) _session->setTitle(Session::DisplayedTitleRole,title); else _session->setTitle(Session::DisplayedTitleRole,_session->title(Session::NameRole)); } 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" ) { _session->emulation()->sendText("ssh "); if ( url.port() > -1 ) _session->emulation()->sendText("-p " + QString::number(url.port()) + ' ' ); if ( url.hasUser() ) _session->emulation()->sendText(url.user() + '@'); if ( url.hasHost() ) _session->emulation()->sendText(url.host() + '\r'); } else if ( url.protocol() == "telnet" ) { _session->emulation()->sendText("telnet "); if ( url.hasUser() ) _session->emulation()->sendText("-l " + url.user() + ' '); if ( url.hasHost() ) _session->emulation()->sendText(url.host() + ' '); if ( url.port() > -1 ) _session->emulation()->sendText(QString::number(url.port())); _session->emulation()->sendText("\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(1211) << "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"); // these actions are meaningful only when primary screen is used. clearAction->setEnabled(use); resetAction->setEnabled(use); } void SessionController::updateCopyAction( const QString & text ) { KActionCollection * collection = actionCollection() ; QAction * copyAction = collection->action("edit_copy"); // copy action is meaningful only when some text is selcted. copyAction->setEnabled(!text.isEmpty()); } 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(_copyToAllTabsAction->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::setupActions() { KAction* action = 0; KToggleAction* toggleAction = 0; KActionCollection* collection = actionCollection(); // Close Session action = collection->addAction("close-session", this, SLOT(closeSession())); action->setIcon(KIcon("tab-close")); action->setText(i18n("&Close Tab")); 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(pasteSelection())); action->setText(i18n("Paste Selection")); action->setShortcut(QKeySequence(Qt::CTRL+Qt::SHIFT+Qt::Key_Insert)); // Rename Session action = collection->addAction("rename-session", this, SLOT(renameSession())); action->setText( i18n("&Rename Tab...") ); action->setShortcut( QKeySequence(Qt::CTRL+Qt::ALT+Qt::Key_S) ); // Copy Input To -> All Tabs in Current Window _copyToAllTabsAction = collection->addAction("copy-input-to-all-tabs", this, SLOT(copyInputToAllTabs())); _copyToAllTabsAction->setText(i18n("&All Tabs in Current Window")); _copyToAllTabsAction->setCheckable(true); // Copy Input To -> Select Tabs _copyToSelectedAction = collection->addAction("copy-input-to-selected-tabs", this, SLOT(copyInputToSelectedTabs())); _copyToSelectedAction->setShortcut( QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Period) ); _copyToSelectedAction->setText(i18n("&Select Tabs...")); _copyToSelectedAction->setCheckable(true); // Copy Input To -> None _copyToNoneAction = collection->addAction("copy-input-to-none", this, SLOT(copyInputToNone())); _copyToNoneAction->setText(i18nc("@action:inmenu Do not select any tabs", "&None")); _copyToNoneAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Slash)); _copyToNoneAction->setCheckable(true); _copyToNoneAction->setChecked(true); 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")); action->setShortcut(KShortcut(Qt::CTRL | Qt::Key_Plus)); 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)); // History _searchToggleAction = KStandardAction::find(this, NULL, collection); _searchToggleAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_F)); _searchToggleAction->setCheckable(true); connect(_searchToggleAction, SIGNAL(toggled(bool)), this, SLOT(searchHistory(bool))); _findNextAction = KStandardAction::findNext(this, SLOT(findNextInHistory()), collection); _findNextAction->setEnabled(false); _findPreviousAction = KStandardAction::findPrev(this, SLOT(findPreviousInHistory()), collection); _findPreviousAction->setShortcut(QKeySequence(Qt::SHIFT + Qt::Key_F3)); _findPreviousAction->setEnabled(false); action = KStandardAction::saveAs(this, SLOT(saveHistory()), collection); action->setText(i18n("Save Output &As...")); action = collection->addAction("configure-history", this, SLOT(showHistoryOptions())); action->setText(i18n("Configure 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_X)); // Profile Options action = collection->addAction("edit-current-profile", this, SLOT(editCurrentProfile())); action->setText(i18n("Configure Current Profile...")); action->setIcon(KIcon("document-properties") ); _changeProfileMenu = new KActionMenu(i18n("Change Profile"), _view); collection->addAction("change-profile", _changeProfileMenu); connect(_changeProfileMenu->menu(), SIGNAL(aboutToShow()), this, SLOT(prepareChangeProfileMenu())); } void SessionController::changeProfile(Profile::Ptr profile) { SessionManager::instance()->setSessionProfile(_session,profile); } void SessionController::prepareChangeProfileMenu() { if (_changeProfileMenu->menu()->isEmpty()) { _profileList = new ProfileList(false,this); connect(_profileList, SIGNAL(profileSelected(Profile::Ptr)), this, SLOT(changeProfile(Profile::Ptr))); } _changeProfileMenu->menu()->clear(); _changeProfileMenu->menu()->addActions(_profileList->actions()); } void SessionController::updateCodecAction() { _codecAction->setCurrentCodec(QString(_session->emulation()->codec()->name())); } 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 dialog(new RenameTabsDialog(QApplication::activeWindow())); dialog->setTabTitleText(_session->tabTitleFormat(Session::LocalTabTitle)); dialog->setRemoteTabTitleText(_session->tabTitleFormat(Session::RemoteTabTitle)); if (!_session->isRemote()) { dialog->focusTabTitleText(); } else { dialog->focusRemoteTabTitleText(); } QPointer 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(); } } void SessionController::saveSession() { Q_ASSERT(0); // not implemented yet //SaveSessionDialog dialog(_view); //int result = dialog.exec(); } 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; } void SessionController::closeSession() { if (_preventClose) return; if (confirmClose()) _session->close(); } // 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->copyClipboard(); } void SessionController::paste() { _view->pasteClipboard(); } void SessionController::pasteSelection() { _view->pasteSelection(); } static const KXmlGuiWindow* findWindow(const QObject* object) { // Walk up the QObject hierarchy to find a KXmlGuiWindow. while(object != NULL) { const KXmlGuiWindow* window = dynamic_cast(object); if(window != NULL) { return(window); } object = object->parent(); } return(NULL); } static bool hasTerminalDisplayInSameWindow(const Session* session, const KXmlGuiWindow* window) { // Iterate all TerminalDisplays of this Session ... QListIterator terminalDisplayIterator(session->views()); while(terminalDisplayIterator.hasNext()) { const TerminalDisplay* terminalDisplay = terminalDisplayIterator.next(); // ... and check whether a TerminalDisplay has the same // window as given in the parameter if(window == findWindow(terminalDisplay)) { return(true); } } return(false); } void SessionController::copyInputToAllTabs() { if(!_copyToGroup) { _copyToGroup = new SessionGroup(this); } // Find our window ... const KXmlGuiWindow* myWindow = findWindow(_view); QSet group = QSet::fromList(SessionManager::instance()->sessions()); for(QSet::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(); _copyToAllTabsAction->setChecked(true); _copyToSelectedAction->setChecked(false); _copyToNoneAction->setChecked(false); } void SessionController::copyInputToSelectedTabs() { if (!_copyToGroup) { _copyToGroup = new SessionGroup(this); _copyToGroup->addSession(_session); _copyToGroup->setMasterStatus(_session,true); _copyToGroup->setMasterMode(SessionGroup::CopyInputToAll); } CopyInputDialog* 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) return; if (result) { 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(); } delete dialog; _copyToAllTabsAction->setChecked(false); _copyToSelectedAction->setChecked(true); _copyToNoneAction->setChecked(false); } void SessionController::copyInputToNone() { if (!_copyToGroup) // No 'Copy To' is active return; QSet group = QSet::fromList(SessionManager::instance()->sessions()); for(QSet::iterator iterator = group.begin(); iterator != group.end(); ++iterator) { Session* session = *iterator; if(session != _session) { _copyToGroup->removeSession(*iterator); } } delete _copyToGroup; _copyToGroup = NULL; snapshot(); _copyToAllTabsAction->setChecked(false); _copyToSelectedAction->setChecked(false); _copyToNoneAction->setChecked(true); } void SessionController::searchClosed() { _searchToggleAction->toggle(); } #if 0 void SessionController::searchHistory() { searchHistory(true); } #endif 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; } // 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.trimmed() , 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() { HistorySizeDialog* dialog = new HistorySizeDialog( QApplication::activeWindow() ); const HistoryType& currentHistory = _session->historyType(); if ( currentHistory.isEnabled() ) { if ( currentHistory.isUnlimited() ) { dialog->setMode( HistorySizeDialog::UnlimitedHistory ); } else { dialog->setMode( HistorySizeDialog::FixedSizeHistory ); dialog->setLineCount( currentHistory.maximumLineCount() ); } } else { dialog->setMode( HistorySizeDialog::NoHistory ); } connect( dialog , SIGNAL(optionsChanged(int,int,bool)) , this , SLOT(scrollBackOptionsChanged(int,int,bool)) ); dialog->show(); } void SessionController::sessionResizeRequest(const QSize& size) { //kDebug(1211) << "View resize requested to " << size; _view->setSize(size.width(),size.height()); } void SessionController::scrollBackOptionsChanged(int mode, int lines, bool saveToCurrentProfile ) { switch (mode) { case HistorySizeDialog::NoHistory: _session->setHistoryType( HistoryTypeNone() ); break; case HistorySizeDialog::FixedSizeHistory: _session->setHistoryType( CompactHistoryType(lines) ); break; case HistorySizeDialog::UnlimitedHistory: _session->setHistoryType( HistoryTypeFile() ); break; } if (saveToCurrentProfile) { Profile::Ptr profile = SessionManager::instance()->sessionProfile(_session); switch (mode) { case HistorySizeDialog::NoHistory: profile->setProperty(Profile::HistoryMode , Profile::DisableHistory); break; case HistorySizeDialog::FixedSizeHistory: profile->setProperty(Profile::HistoryMode , Profile::FixedSizeHistory); profile->setProperty(Profile::HistorySize , lines); break; case HistorySizeDialog::UnlimitedHistory: profile->setProperty(Profile::HistoryMode , Profile::UnlimitedHistory); break; } SessionManager::instance()->changeProfile(profile, profile->setProperties()); } } 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->property(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); //TODO - Save this setting as a session default } void SessionController::decreaseTextSize() { static const qreal MinimumFontSize = 6; QFont font = _view->getVTFont(); font.setPointSizeF( qMax(font.pointSizeF()-1,MinimumFontSize) ); _view->setVTFont(font); //TODO - Save this setting as a session default } 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(KIcon("emblem-important")); } else { // 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 ); } 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(1211) << "Created xmlgui factory" << factory; } QPointer popup = qobject_cast(factory()->container("session-popup-menu",this)); if (popup) { // prepend content-specific actions such as "Open Link", "Copy Email Address" etc. QList contentActions = _view->filterActions(position); QAction* contentSeparator = new QAction(popup); contentSeparator->setSeparator(true); contentActions << contentSeparator; _preventClose = true; popup->insertActions(popup->actions().value(0,0),contentActions); 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; _previousState = state; // TODO - Replace the icon choices below when suitable icons for silence and activity // are available if ( state == NOTIFYACTIVITY ) { if (_activityIcon.isNull()) { _activityIcon = KIcon("dialog-information"); } setIcon(_activityIcon); } else if ( state == NOTIFYSILENCE ) { if (_silenceIcon.isNull()) { _silenceIcon = KIcon("dialog-information"); } setIcon(_silenceIcon); } else if ( state == NOTIFYNORMAL ) { if ( _sessionIconName != _session->iconName() ) { _sessionIconName = _session->iconName(); _sessionIcon = KIcon( _sessionIconName ); } updateSessionIcon(); } } 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("

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; } QString zmodem = KStandardDirs::findExe("sz"); if(zmodem.isEmpty()) { zmodem = KStandardDirs::findExe("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 = 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 SessionTask::sessions() const { return _sessions; } SaveHistoryTask::SaveHistoryTask(QObject* parent) : SessionTask(parent) { } SaveHistoryTask::~SaveHistoryTask() { } void SaveHistoryTask::execute() { QListIterator iter(sessions()); // 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 while ( iter.hasNext() ) { SessionPtr session = iter.next(); 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(); // if there are still more lines to process after this request // then insert a new line character // to ensure that the next block of lines begins on a new line // // FIXME - There is still an extra new-line at the end of the save data. if ( copyUpToLine <= sessionLines-1 ) { stream << '\n'; } 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 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 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(1211) << "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; } #include "SessionController.moc"