##// END OF EJS Templates
Working snapshot.
Working snapshot.

File last commit:

r0:1aa783210b8e default
r5:483cfe27e044 Working snapshot default
Show More
SimpleConsole.cpp
730 lines | 19.3 KiB | text/x-c | CppLexer
/*! \file SimpleConsole.cpp
* \brief implementation of SimpleConsole
* \author "Melven Zoellner" <melven@topen.org>
*
*/
#include "SimpleConsole.h"
#include <QScrollBar>
#include <QTextBlock>
#include <QKeyEvent>
#include <QCompleter>
#include <QAbstractItemView>
#include <QStringListModel>
#include <QMenu>
#include <QEventLoop>
#include <QDebug>
#include <QClipboard>
#include <QApplication>
SimpleConsole::SimpleConsole(QWidget *parent) :
QPlainTextEdit(parent)
{
_prompt = "py > ";
_prompt2 = "...";
_inputIndex = 0;
_historyPosition = 0;
_completer = NULL;
_state = UndefinedConsoleState;
_userInputEventLoop = new QEventLoop(this);
setUndoRedoEnabled(false);
// set up font and cursor
QFont font;
font.setStyleHint(QFont::Monospace);
font.setFamily("Monospace");
font.setPointSize(font.pointSize());
setFont(font);
connect(this, SIGNAL(cursorPositionChanged()),
this, SLOT(updateLastValidCursor()));
// resize so it is not as small
//resize(500,200);
}
void SimpleConsole::keyPressEvent(QKeyEvent *event)
{
// don't handle any input if not in user input state
if( !(_state == ShowCommandPrompt || _state == WaitingForUserInput) )
return;
bool handled = true;
bool completing = (
state() == ShowCommandPrompt &&
_completer != NULL &&
_completer->popup()->isVisible() );
if( completing )
{
// let the completer handle some stuff
if( event->key() == Qt::Key_Enter ||
event->key() == Qt::Key_Return ||
event->key() == Qt::Key_Escape ||
event->key() == Qt::Key_Tab ||
event->key() == Qt::Key_Backtab )
{
event->ignore();
return;
}
// check if we need to hide the completer...
int notCtrlOrShift = ~(Qt::ControlModifier | Qt::ShiftModifier | Qt::NoModifier);
QString c = event->text();
bool beginNewWord = !(c.isEmpty() || (c.length() == 1 && ( c[0].isLetterOrNumber() || c == "_" )) );
if( event->modifiers() & notCtrlOrShift ||
beginNewWord )
{
_completer->popup()->hide();
completing = false;
}
}
// check if cursor is invalid, then reset it to a valid cursor
QTextCursor cursor = textCursor();
// if( !modificationAllowed(cursor) )
// {
// setTextCursor(_lastValidCursor);
// setReadOnly(false);
// cursor = textCursor();
// }
if( (!completing) && event->matches(QKeySequence::InsertLineSeparator) )
{
// command over multiple lines
if( _state == ShowCommandPrompt )
extendMultilineCommand();
else // _state == WaitingForUserInput
_userInputEventLoop->exit();
}
else if( (!completing) && event->matches(QKeySequence::InsertParagraphSeparator) )
{
// on return emit signal to execute line
if( _state == ShowCommandPrompt )
executeCurrentCommand();
else // _state == WaitingForUserInput
_userInputEventLoop->exit();
}
// look up in history
else if( event->matches(QKeySequence::MoveToPreviousLine) &&
_state == ShowCommandPrompt && (!completing) )
historyUp();
// look down in history
else if( event->matches(QKeySequence::MoveToNextLine) &&
_state == ShowCommandPrompt && (!completing) )
historyDown();
// backspace: delete previous character
else if( event->key() == Qt::Key_Backspace &&
(event->modifiers() == Qt::NoModifier ||
event->modifiers() == Qt::ShiftModifier) )
{
// create cursor to test if allowed
if( !cursor.hasSelection() )
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor);
if( modificationAllowed(cursor) )
QPlainTextEdit::keyPressEvent(event);
}
// delete character
else if( event->matches(QKeySequence::Delete) )
{
// create cursor to test if allowed
if( !cursor.hasSelection() )
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
if( modificationAllowed(cursor) )
QPlainTextEdit::keyPressEvent(event);
}
// delete several characters at once
else if( event->matches(QKeySequence::DeleteEndOfWord) ||
event->matches(QKeySequence::DeleteEndOfLine) ||
event->matches(QKeySequence::DeleteStartOfWord) )
{
if( event->matches(QKeySequence::DeleteStartOfWord) )
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
if( modificationAllowed(cursor) )
QPlainTextEdit::keyPressEvent(event);
}
// autocomplete
else if( event->key() == Qt::Key_Tab &&
event->modifiers() == Qt::NoModifier &&
_state == ShowCommandPrompt )
{
emit autocompletionRequested();
requestAutocompletion();
}
else if( event->matches(QKeySequence::Copy))
{
copy();
}
else if( event->matches(QKeySequence::Paste))
{
QStringList lines= qApp->clipboard()->text().split("\n");
for(int i=0;i<lines.count();i++)
{
insertPrompt(false);
this->insertPlainText(lines.at(i));
historyAdd(currentLine());
}
}
else if( event->matches(QKeySequence::Cut))
{
cut();
}
// only send pure text to parent
else if( event->text().length() == 1 && event->text()[0].isPrint() )
{
if( !modificationAllowed(cursor) )
{
setTextCursor(_lastValidCursor);
setReadOnly(false);
cursor = textCursor();
}
QPlainTextEdit::keyPressEvent(event);
}
else
{
// at last check if the user wants some cursor movement
handled = positionCursor(event);
// special case for completion:
// close completer popup if we moved for more than a word
if( handled && completing )
{
bool movedOutsideOfWord = false;
cursor.movePosition(QTextCursor::StartOfWord);
if( textCursor() < cursor )
movedOutsideOfWord = true;
cursor.movePosition(QTextCursor::EndOfWord);
if( textCursor() > cursor )
movedOutsideOfWord = true;
if( movedOutsideOfWord )
{
_completer->popup()->hide();
completing = false;
}
}
}
// if we handled the event, scroll there
if( handled )
ensureCursorVisible();
// if still completing, update completer
if( completing )
requestAutocompletion();
// mark the event as handled
event->setAccepted(handled);
}
void SimpleConsole::contextMenuEvent(QContextMenuEvent *event)
{
// create a new context menu
QMenu *menu = new QMenu(this);
QTextCursor cursor = textCursor();
if( modificationAllowed(cursor) )
{
// cut, copy and paste
menu->addAction(tr("Cut"), this, SLOT(cut()));
menu->addAction(tr("Copy"),this, SLOT(copy()));
menu->addAction(tr("Paste"), this, SLOT(paste()));
}
else
{
// only copy if in readonly part
menu->addAction(tr("Copy"),this, SLOT(copy()));
}
// show the menu
menu->exec(event->globalPos());
// we can delete it now
delete menu;
}
void SimpleConsole::setCompleter(QCompleter *c)
{
// disconnect all old connections
if( _completer )
disconnect(_completer, 0, this, 0);
_completer = c;
// setup the completer
if( _completer )
{
_completer->setWidget(this);
_completer->setCompletionMode(QCompleter::PopupCompletion);
connect(_completer, SIGNAL(activated(QString)),
this, SLOT(insertCompletion(QString)));
}
}
/* TODO AJE: fix multilines commands */
void SimpleConsole::insertPrompt(bool newBlock)
{
if( !setState(ShowCommandPrompt) )
return;
if( newBlock )
{
// a bit of intelligence here, reuse existing block if it is empty, prevents unncecessary new lines
if( textCursor().block().text().length() != 0 )
textCursor().insertBlock();
//textCursor().beginEditBlock();
textCursor().insertText(_prompt);
_inputIndex = _prompt.length();
_lastValidCursor = textCursor();
}
else
{
//textCursor().beginEditBlock();
textCursor().insertHtml("<br>"+_prompt2);
//textCursor().insertText("\n");
//textCursor().endEditBlock();
_inputIndex = _prompt2.length();
}
ensureCursorVisible();
}
void SimpleConsole::updateLastValidCursor()
{
QTextCursor cursor = textCursor();
if( modificationAllowed(cursor) )
{
_lastValidCursor = cursor;
setReadOnly(false);
}
else
{
setReadOnly(true);
}
}
void SimpleConsole::executeCurrentCommand()
{
if( !setState(ExecutingCommand) )
return;
// first move the cursor to the end of the document
moveCursor(QTextCursor::End);
// first add current line to the history
historyAdd(currentLine());
// extract command
QString cmd = currentCommand();
// put output in new block
textCursor().insertBlock();
// execute it
emit execute(cmd);
// append new prompt
insertPrompt(true);
}
void SimpleConsole::executeCurrentCommand(QString CMD)
{
if( !setState(ExecutingCommand) )
return;
// first move the cursor to the end of the document
moveCursor(QTextCursor::End);
// first add current line to the history
historyAdd(currentLine());
// extract command
// put output in new block
textCursor().insertBlock();
// execute it
emit execute(CMD);
// append new prompt
insertPrompt(true);
}
void SimpleConsole::extendMultilineCommand()
{
// append line to history
historyAdd(currentLine());
// prompt for multiline command
insertPrompt(false);
}
QString SimpleConsole::currentCommand() const
{
// compose current command:
QStringList lines = textCursor().block().text().split(QChar::LineSeparator);
QString cmd = lines[0].mid(_prompt.length());
for( int i = 1; i < lines.size(); i++ )
cmd = cmd + "\n" + lines[i].mid(_prompt2.length());
return cmd;
}
QString SimpleConsole::currentLine() const
{
// use a cursor to get the current line
QTextCursor cursor = textCursor();
cursor.movePosition(QTextCursor::StartOfLine);
cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
// the current line
QString line = cursor.selectedText().mid(_inputIndex);
// we still need to remove the prompt
return line;
}
QString SimpleConsole::waitForUserInput()
{
// try to change state to user input!
if( !setState(WaitingForUserInput) )
return QString();
// get length of previous text in the current line
// and set the cursor to the end
// and force update of lastValidCursor,
// this indirectly sets readonly to false
QTextCursor cursor = textCursor();
cursor.movePosition(QTextCursor::End);
setTextCursor(cursor);
updateLastValidCursor();
cursor.movePosition(QTextCursor::StartOfLine);
_inputIndex = textCursor().position() - cursor.position();
// start a new eventloop
_userInputEventLoop->exec();
// user input finished, so read current line
QString userInput = currentLine();
// set state back to executing
setState(ExecutingCommand);
// append new line after input
htmlOutput("<br>");
return userInput;
}
void SimpleConsole::setCurrentLine(QString newLine)
{
// use a cursor to get the current line
QTextCursor cursor = textCursor();
cursor.movePosition(QTextCursor::StartOfLine);
cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
// save the current line
QString line = cursor.selectedText();
// we still need the prompt, so replace the rest
line = line.left(_inputIndex) + newLine;
// now replace the text under the cursor
cursor.insertText(line);
// update the cursor
setTextCursor(cursor);
}
void SimpleConsole::historyAdd(QString line)
{
if( line == "" )
return;
if( _history.empty() || _history.last() != line )
_history.append(line);
_historyPosition = _history.size();
}
void SimpleConsole::historyUp()
{
if( _historyPosition > 0 )
setCurrentLine(_history.at(--_historyPosition));
}
void SimpleConsole::historyDown()
{
if( _historyPosition+1 < _history.size() )
setCurrentLine(_history.at(++_historyPosition));
}
bool SimpleConsole::modificationAllowed(const QTextCursor &cursor) const
{
if( !(_state == ShowCommandPrompt ||
_state == WaitingForUserInput) )
return false;
int posOfLastLine = cursorPositionOfLastLine();
// now check if we can modify it (it is in the last line?)
return cursor.position() >= posOfLastLine &&
cursor.anchor() >= posOfLastLine;
}
bool SimpleConsole::positionCursor(const QKeyEvent *event)
{
bool handled = true;
// apply cursor movement
QTextCursor cursor = textCursor();
if( event->matches(QKeySequence::MoveToEndOfLine) )
cursor.movePosition(QTextCursor::EndOfLine);
else if( event->matches(QKeySequence::MoveToNextWord) )
cursor.movePosition(QTextCursor::NextWord);
else if( event->matches(QKeySequence::MoveToNextChar) )
cursor.movePosition(QTextCursor::NextCharacter);
else if( event->matches(QKeySequence::SelectEndOfLine) )
cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
else if( event->matches(QKeySequence::SelectNextWord) )
cursor.movePosition(QTextCursor::NextWord, QTextCursor::KeepAnchor);
else if( event->matches(QKeySequence::SelectNextChar) )
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
else if( event->matches(QKeySequence::MoveToPreviousWord) )
cursor.movePosition(QTextCursor::PreviousWord);
else if( event->matches(QKeySequence::MoveToPreviousChar) )
cursor.movePosition(QTextCursor::PreviousCharacter);
else if( event->matches(QKeySequence::SelectPreviousWord) )
cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
else if( event->matches(QKeySequence::SelectPreviousChar) )
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
// special cases for start of line
else if( event->matches(QKeySequence::MoveToStartOfLine) )
{
cursor.movePosition(QTextCursor::StartOfLine);
cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, _prompt.length());
}
else if( event->matches(QKeySequence::SelectStartOfLine) )
{
cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor);
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, _prompt.length());
}
else
{
handled = false;
}
// check if movement is allowed
if( modificationAllowed(cursor) )
setTextCursor(cursor);
return handled;
}
void SimpleConsole::insertCompletion(QString word)
{
// replace word under cursor
QTextCursor cursor = textCursor();
cursor.select(QTextCursor::WordUnderCursor);
if( modificationAllowed(cursor) )
{
cursor.insertText( word );
}
}
void SimpleConsole::requestAutocompletion()
{
// check if we have a completer
if( !_completer )
return;
// select current word
QTextCursor cursor = textCursor();
cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
// popup / update the completer
if( modificationAllowed(cursor) )
{
// update completion prefix
if( _completer->completionPrefix() != cursor.selectedText() ||
!_completer->popup()->isVisible() )
{
_completer->setCompletionPrefix( cursor.selectedText() );
_completer->popup()->setCurrentIndex(_completer->completionModel()->index(0, 0));
}
//qDebug() << "requestAutocompletion with prefix: " << cursor.selectedText();
// position the _completer popup correctly
QRect cr = cursorRect(cursor);
cr.setWidth(_completer->popup()->sizeHintForColumn(0) +
_completer->popup()->verticalScrollBar()->sizeHint().width());
_completer->complete(cr);
}
else
{
// hide the completer
_completer->popup()->hide();
}
}
bool SimpleConsole::setState(SimpleConsole::ConsoleState newState)
{
// if the new state is the same as before, everything is ok...
if( newState == _state )
return true;
// at first we must show a command prompt
if( _state == UndefinedConsoleState && newState == ShowCommandPrompt )
{
_state = newState;
return true;
}
// if a command prompt is shown, we can execute something
if( _state == ShowCommandPrompt && newState == ExecutingCommand )
{
_state = newState;
return true;
}
// an executed command may need user input
if( _state == ExecutingCommand && newState == WaitingForUserInput )
{
_state = newState;
return true;
}
// after user input, execution continues
if( _state == WaitingForUserInput && newState == ExecutingCommand )
{
_state = newState;
return true;
}
// when the command execution finishes, we show a command prompt
if( _state == ExecutingCommand && newState == ShowCommandPrompt )
{
_state = newState;
return true;
}
// no other transitions possible
qDebug() << "in SimpleConsole::insertPrompt: cannot switch from ConsoleState " << _state << " to " << newState << "!";
return false;
}
int SimpleConsole::cursorPositionOfLastLine() const
{
// get position of last line
QTextCursor helperCursor = textCursor();
helperCursor.movePosition(QTextCursor::End);
helperCursor.movePosition(QTextCursor::StartOfLine);
if( _state == ShowCommandPrompt || _state == WaitingForUserInput )
helperCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, _inputIndex);
return helperCursor.position();
}
void SimpleConsole::setMaximumHistorySize(int maxSize)
{
_maxHistSize = maxSize;
// truncate history if necessary
if( _history.size() > _maxHistSize )
{
QStringList::iterator start = _history.begin();
QStringList::Iterator end = start + (_history.size() - _maxHistSize);
_history.erase(start, end);
_historyPosition = _history.size();
}
}
void SimpleConsole::setHistory(QStringList newHistory)
{
_history = newHistory;
// truncate history if necessary
setMaximumHistorySize(_maxHistSize);
_historyPosition = _history.size();
}
void SimpleConsole::output(QString s)
{
if( _state == ExecutingCommand || _state == UndefinedConsoleState )
textCursor().insertText(s);
else
qDebug() << "Cannot handle console output when not in ExecutingCommand state! Output:\n" << s;
}
void SimpleConsole::htmlOutput(QString s)
{
if( _state == ExecutingCommand || _state == UndefinedConsoleState )
textCursor().insertHtml(s);
else
qDebug() << "Cannot handle console output when not in ExecutingCommand state! Output:\n" << s;
}