From 6ed7ad3a15c46f17ad33e8f1e508f92ac28c376f Mon Sep 17 00:00:00 2001 From: wmayer Date: Sat, 10 Dec 2011 14:40:44 +0000 Subject: [PATCH] 0000489: python console: "RFC"s for even more "rapid" prototyping :-) git-svn-id: https://free-cad.svn.sourceforge.net/svnroot/free-cad/trunk@5253 e8eeb9e2-ec13-0410-a4a9-efa5cf37419d --- src/Gui/CallTips.cpp | 64 +++++++- src/Gui/CallTips.h | 1 + src/Gui/PythonConsole.cpp | 299 +++++++++++++++++++++++--------------- src/Gui/PythonConsole.h | 13 +- 4 files changed, 253 insertions(+), 124 deletions(-) diff --git a/src/Gui/CallTips.cpp b/src/Gui/CallTips.cpp index fa76f61c0..25a3ccc11 100644 --- a/src/Gui/CallTips.cpp +++ b/src/Gui/CallTips.cpp @@ -46,10 +46,37 @@ #include #include "CallTips.h" +Q_DECLARE_METATYPE( Gui::CallTip ); //< allows use of QVariant + +namespace Gui +{ + +/** + * template class Temporary. + * Allows variable changes limited to a scope. + */ +template +class Temporary +{ +public: + Temporary( TYPE &var, const TYPE tmpVal ) + : _var(var), _saveVal(var) + { var = tmpVal; } + + ~Temporary( void ) + { _var = _saveVal; } + +private: + TYPE &_var; + TYPE _saveVal; +}; + +} /* namespace Gui */ + using namespace Gui; CallTipsList::CallTipsList(QPlainTextEdit* parent) - : QListWidget(parent), textEdit(parent), cursorPos(0), validObject(true) + : QListWidget(parent), textEdit(parent), cursorPos(0), validObject(true), doCallCompletion(false) { // make the user assume that the widget is active QPalette pal = parent->palette(); @@ -406,7 +433,7 @@ void CallTipsList::showTips(const QString& line) addItem(it.key()); QListWidgetItem *item = this->item(this->count()-1); item->setData(Qt::ToolTipRole, QVariant(it.value().description)); - item->setData(Qt::UserRole, QVariant(it.value().parameter)); + item->setData(Qt::UserRole, qVariantFromValue( it.value() )); //< store full CallTip data switch (it.value().type) { case CallTip::Module: @@ -526,10 +553,16 @@ bool CallTipsList::eventFilter(QObject * watched, QEvent * event) itemActivated(currentItem()); return false; } - else if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Tab) { + else if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { itemActivated(currentItem()); return true; } + else if (ke->key() == Qt::Key_Tab) { + // enable call completion for activating items + Temporary tmp( this->doCallCompletion, true ); //< previous state restored on scope exit + itemActivated( currentItem() ); + return true; + } else if (this->compKeys.indexOf(ke->key()) > -1) { itemActivated(currentItem()); return false; @@ -585,6 +618,29 @@ void CallTipsList::callTipItemActivated(QListWidgetItem *item) cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor); } cursor.insertText( text ); + + // get CallTip from item's UserRole-data + const CallTip &callTip = qVariantValue( item->data(Qt::UserRole) ); + + // if call completion enabled and we've something callable (method or class constructor) ... + if (this->doCallCompletion + && (callTip.type == CallTip::Method || callTip.type == CallTip::Class)) + { + cursor.insertText( QLatin1String("()") ); //< just append parenthesis to identifier even inserted. + + /** + * Try to find out if call needs arguments. + * For this we search the description for appropriate hints ... + */ + QRegExp argumentMatcher( QRegExp::escape( callTip.name ) + QLatin1String("\\s*\\(\\s*\\w+.*\\)") ); + argumentMatcher.setMinimal( true ); //< set regex non-greedy! + if (argumentMatcher.indexIn( callTip.description ) != -1) + { + // if arguments are needed, we just move the cursor one left, to between the parentheses. + cursor.movePosition( QTextCursor::Left, QTextCursor::MoveAnchor, 1 ); + textEdit->setTextCursor( cursor ); + } + } textEdit->ensureCursorVisible(); QRect rect = textEdit->cursorRect(cursor); @@ -593,7 +649,7 @@ void CallTipsList::callTipItemActivated(QListWidgetItem *item) QPoint p(posX, posY); p = textEdit->mapToGlobal(p); - QToolTip::showText(p, item->data(Qt::UserRole).toString()); + QToolTip::showText( p, callTip.parameter ); } QString CallTipsList::stripWhiteSpace(const QString& str) const diff --git a/src/Gui/CallTips.h b/src/Gui/CallTips.h index 4f2b66c82..d74a251b9 100644 --- a/src/Gui/CallTips.h +++ b/src/Gui/CallTips.h @@ -81,6 +81,7 @@ private: QPlainTextEdit* textEdit; int cursorPos; bool validObject; + bool doCallCompletion; QList hideKeys; QList compKeys; }; diff --git a/src/Gui/PythonConsole.cpp b/src/Gui/PythonConsole.cpp index cef3b1a6a..dd8c1ec98 100644 --- a/src/Gui/PythonConsole.cpp +++ b/src/Gui/PythonConsole.cpp @@ -55,7 +55,11 @@ using namespace Gui; -namespace Gui { +namespace Gui +{ + +static size_t promptLength = 4; //< length of prompt string: ">>> " or "... ", in either case 4 characters + struct PythonConsoleP { enum Output {Error = 20, Message = 21}; @@ -310,6 +314,11 @@ bool InteractiveInterpreter::push(const char* line) return false; } +bool InteractiveInterpreter::hasPendingInput( void ) const +{ + return (!d->buffer.isEmpty()); +} + QStringList InteractiveInterpreter::getBuffer() const { return d->buffer; @@ -430,97 +439,125 @@ void PythonConsole::OnChange( Base::Subject &rCaller,const char* sR */ void PythonConsole::keyPressEvent(QKeyEvent * e) { - if (e->modifiers() & Qt::ControlModifier) { - switch( e->key() ) + bool restartHistory = true; + QTextCursor cursor = this->textCursor(); + + // construct reference cursor at begin of input line ... + QTextCursor inputLineBegin = cursor; + inputLineBegin.movePosition( QTextCursor::End ); + inputLineBegin.movePosition( QTextCursor::StartOfLine ); + inputLineBegin.movePosition( QTextCursor::Right, QTextCursor::MoveAnchor, promptLength ); + + if (cursor < inputLineBegin) + { + /** + * The cursor is placed not on the input line (or within the prompt string) + * So we handle key input as follows: + * - don't allow changing previous lines. + * - allow full movement (no prompt restriction) + * - allow copying content (Ctrl+C) + * - "escape" to end of input line + */ + switch (e->key()) { - case Qt::Key_Up: - { - // no modification, just history facility - if (!d->history.isEmpty()) { - if (d->history.prev()) { - QString cmd = d->history.value(); - overrideCursor(cmd); - } return; - } - } break; - case Qt::Key_Down: - { - // no modification, just history facility - if (!d->history.isEmpty()) { - if (d->history.next()) { - QString cmd = d->history.value(); - overrideCursor(cmd); - } return; - } - } break; - default: - break; + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Escape: + this->moveCursor( QTextCursor::End ); + break; + + default: + if (e->text().isEmpty() || e->matches( QKeySequence::Copy )) + { TextEdit::keyPressEvent(e); } + break; } } - - switch (e->key()) + else { - // running Python interpreter? - case Qt::Key_Return: - case Qt::Key_Enter: - { - // make sure to be at the end - QTextCursor cursor = textCursor(); - cursor.movePosition(QTextCursor::End); - - // get the last paragraph's text - QTextBlock block = cursor.block(); - QString line = block.text(); + /** + * The cursor sits somewhere on the input line (after the prompt) + * Here we handle key input a bit different: + * - restrict cursor movement to input line range (excluding the prompt characters) + * - roam the history by Up/Down keys + * - show call tips on period + */ + QTextBlock inputBlock = inputLineBegin.block(); //< get the last paragraph's text + QString inputLine = inputBlock.text().mid(promptLength); //< and skip prompt characters - // and skip the first 4 characters consisting of either ">>> " or "... " - line = line.mid(4); - - // put statement to the history - d->history.append(line); - - // evaluate and run the command - runSource(line); - } break; - case Qt::Key_Period: + switch (e->key()) { - QTextCursor cursor = textCursor(); - QTextBlock block = cursor.block(); - QString text = block.text(); - int length = cursor.position() - block.position(); - TextEdit::keyPressEvent(e); - d->callTipsList->showTips(text.left(length)); - } break; - case Qt::Key_Home: - { - if (e->modifiers() & Qt::ControlModifier) { - TextEdit::keyPressEvent(e); - } else { - QTextCursor::MoveMode mode = e->modifiers() & Qt::ShiftModifier - ? QTextCursor::KeepAnchor - : QTextCursor::MoveAnchor; - QTextCursor cursor = textCursor(); - QTextBlock block = cursor.block(); - QString text = block.text(); - int cursorPos = block.position(); - if (text.startsWith(QLatin1String(">>> ")) || - text.startsWith(QLatin1String("... "))) - cursorPos += 4; - cursor.setPosition(cursorPos, mode); - setTextCursor(cursor); - ensureCursorVisible(); - } - } break; - default: - { - TextEdit::keyPressEvent(e); + case Qt::Key_Escape: + { + // disable current input line - i.e. put it to history but don't execute it. + if (!inputLine.isEmpty()) + { + d->history.append( QLatin1String("# ") + inputLine ); //< put line to history ... + inputLineBegin.insertText( QString::fromAscii("# ") ); //< but comment it on console + setTextCursor( inputLineBegin ); + printPrompt( d->interpreter->hasPendingInput() ); //< print adequate prompt + } + } break; - // This can't be done in CallTipsList::eventFilter() because we must first perform - // the event and afterwards update the list widget - if (d->callTipsList->isVisible()) { - d->callTipsList->validateCursor(); - } - } break; - } + case Qt::Key_Return: + case Qt::Key_Enter: + { + runSource( inputLine ); //< commit input line + d->history.append( inputLine ); //< put statement to history + } break; + + case Qt::Key_Period: + { + // analyse context and show available call tips + int contextLength = cursor.position() - inputLineBegin.position(); + TextEdit::keyPressEvent(e); + d->callTipsList->showTips( inputLine.left( contextLength ) ); + } break; + + case Qt::Key_Home: + { + QTextCursor::MoveMode mode = (e->modifiers() & Qt::ShiftModifier)? QTextCursor::KeepAnchor + /* else */ : QTextCursor::MoveAnchor; + cursor.setPosition( inputBlock.position() + promptLength, mode ); + setTextCursor( cursor ); + ensureCursorVisible(); + } break; + + case Qt::Key_Up: + { + // if possible, move back in history + if (d->history.prev( inputLine )) + { overrideCursor( d->history.value() ); } + restartHistory = false; + } break; + + case Qt::Key_Down: + { + // if possible, move forward in history + if (d->history.next()) + { overrideCursor( d->history.value() ); } + restartHistory = false; + } break; + + case Qt::Key_Backspace: + case Qt::Key_Left: + { + if (cursor > inputLineBegin) + { TextEdit::keyPressEvent(e); } + } break; + + default: + { + TextEdit::keyPressEvent(e); + } break; + } + // This can't be done in CallTipsList::eventFilter() because we must first perform + // the event and afterwards update the list widget + if (d->callTipsList->isVisible()) + { d->callTipsList->validateCursor(); } + } + // any cursor move resets the history to its latest item. + if (restartHistory) + { d->history.restart(); } } /** @@ -844,8 +881,8 @@ QMimeData * PythonConsole::createMimeDataFromSelection () const if ( pos >= s && pos <= e ) { if (b.userState() > -1 && b.userState() < pythonSyntax->maximumUserState()) { QString line = b.text(); - // and skip the first 4 characters consisting of either ">>> " or "... " - line = line.mid(4); + // and skip the prompt characters consisting of either ">>> " or "... " + line = line.mid(promptLength); lines << line; } } @@ -916,7 +953,7 @@ void PythonConsole::runSourceFromMimeData(const QString& source) cursor.removeSelectedText(); last = last + select; line = cursor.block().text(); - line = line.mid(4); + line = line.mid(promptLength); } // put statement to the history @@ -972,7 +1009,7 @@ void PythonConsole::overrideCursor(const QString& txt) QTextBlock block = cursor.block(); cursor.movePosition(QTextCursor::End); cursor.movePosition(QTextCursor::StartOfLine); - cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 4); + cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, promptLength); cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, block.text().length()); cursor.removeSelectedText(); cursor.insertText(txt); @@ -1124,7 +1161,7 @@ void PythonConsoleHighlighter::colorChanged(const QString& type, const QColor& c ConsoleHistory::ConsoleHistory() { - it = _history.end(); + _it = _history.end(); } ConsoleHistory::~ConsoleHistory() @@ -1133,38 +1170,62 @@ ConsoleHistory::~ConsoleHistory() void ConsoleHistory::first() { - it = _history.begin(); + _it = _history.begin(); } bool ConsoleHistory::more() { - return (it != _history.end()); + return (_it != _history.end()); } +/** + * next switches the history pointer to the next item. + * While searching the next item, the routine respects the search prefix set by prev(). + * @return true if the pointer was switched to a later item, false otherwise. + */ bool ConsoleHistory::next() { - if (it != _history.end()) { - for (++it; it != _history.end(); ++it) { - if (!it->isEmpty()) - break; - } - return true; - } + bool wentNext = false; - return false; + // if we didn't reach history's end ... + if (_it != _history.end()) + { + // we go forward until we find an item matching the prefix. + for (++_it; _it != _history.end(); ++_it) + { + if (!_it->isEmpty() && _it->startsWith( _prefix )) + { break; } + } + // we did a step - no matter of a matching prefix. + wentNext = true; + } + return wentNext; } -bool ConsoleHistory::prev() +/** + * prev switches the history pointer to the previous item. + * The optional parameter prefix allows to search the history selectively for commands that start + * with a certain character sequence. + * @param prefix - prefix string for searching backwards in history, empty string by default + * @return true if the pointer was switched to an earlier item, false otherwise. + */ +bool ConsoleHistory::prev( const QString &prefix ) { - if (it != _history.begin()) { - for (--it; it != _history.begin(); --it) { - if (!it->isEmpty()) - break; - } - return true; - } + bool wentPrev = false; - return false; + // store prefix if it's the first history access + if (_it == _history.end()) + { _prefix = prefix; } + + // while we didn't go back or reach history's begin ... + while (!wentPrev && _it != _history.begin()) + { + // go back in history and check if item matches prefix + // Skip empty items + --_it; + wentPrev = (!_it->isEmpty() && _it->startsWith( _prefix )); + } + return wentPrev; } bool ConsoleHistory::isEmpty() const @@ -1172,18 +1233,18 @@ bool ConsoleHistory::isEmpty() const return _history.isEmpty(); } -QString ConsoleHistory::value() const +const QString& ConsoleHistory::value() const { - if ( it != _history.end() ) - return *it; - else - return QString::null; + return ((_it != _history.end())? *_it + /* else */ : _prefix); } void ConsoleHistory::append( const QString& item ) { _history.append( item ); - it = _history.end(); + // reset iterator to make the next history + // access begin with the latest item. + _it = _history.end(); } const QStringList& ConsoleHistory::values() const @@ -1191,6 +1252,14 @@ const QStringList& ConsoleHistory::values() const return this->_history; } +/** + * restart resets the history access to the latest item. + */ +void ConsoleHistory::restart( void ) +{ + _it = _history.end(); +} + // ----------------------------------------------------- /* TRANSLATOR Gui::PythonInputField */ diff --git a/src/Gui/PythonConsole.h b/src/Gui/PythonConsole.h index ff15731c2..39f89d114 100644 --- a/src/Gui/PythonConsole.h +++ b/src/Gui/PythonConsole.h @@ -45,6 +45,7 @@ public: bool push(const char*); int compileCommand(const char*) const; + bool hasPendingInput( void ) const; void setBuffer(const QStringList&); QStringList getBuffer() const; void clearBuffer(); @@ -72,15 +73,17 @@ public: void first(); bool more(); bool next(); - bool prev(); + bool prev(const QString &prefix = QString()); bool isEmpty() const; - QString value() const; - void append( const QString& ); + const QString& value() const; + void append(const QString &inputLine); const QStringList& values() const; + void restart(); private: - QStringList _history; - QStringList::ConstIterator it; + QStringList _history; + QStringList::ConstIterator _it; + QString _prefix; }; /**