diff --git a/src/Gui/PythonConsole.cpp b/src/Gui/PythonConsole.cpp index 2c62d316a..f82b27063 100644 --- a/src/Gui/PythonConsole.cpp +++ b/src/Gui/PythonConsole.cpp @@ -59,7 +59,29 @@ using namespace Gui; namespace Gui { -static size_t promptLength = 4; //< length of prompt string: ">>> " or "... ", in either case 4 characters +static const QChar promptEnd( QLatin1Char(' ') ); //< char for detecting prompt end + +inline int promptLength( const QString &lineStr ) + { return lineStr.indexOf( promptEnd ) + 1; } + +inline QString stripPromptFrom( const QString &lineStr ) + { return lineStr.mid( promptLength(lineStr) ); } + +/** + * cursorBeyond checks if cursor is at a valid position to accept keyEvents. + * @param cursor - cursor to check + * @param limit - cursor that marks the begin of the input region + * @param shift - offset for shifting the limit for non-selection cursors [default: 0] + * @return true if a keyEvent is ok at cursor's position, false otherwise + */ +inline bool cursorBeyond( const QTextCursor &cursor, const QTextCursor &limit, int shift = 0 ) +{ + int pos = limit.position(); + if (cursor.hasSelection()) + return (cursor.selectionStart() >= pos && cursor.selectionEnd() >= pos); + else + return cursor.position() >= (pos + shift); +} struct PythonConsoleP { @@ -341,7 +363,7 @@ void InteractiveInterpreter::clearBuffer() * Constructs a PythonConsole which is a child of 'parent'. */ PythonConsole::PythonConsole(QWidget *parent) - : TextEdit(parent), WindowParameter( "Editor" ) + : TextEdit(parent), WindowParameter( "Editor" ), _sourceDrain(NULL) { d = new PythonConsoleP(); d->interactive = false; @@ -445,7 +467,7 @@ void PythonConsole::keyPressEvent(QKeyEvent * e) QTextCursor cursor = this->textCursor(); QTextCursor inputLineBegin = this->inputBegin(); - if (cursor < inputLineBegin) + if (!cursorBeyond( cursor, inputLineBegin )) { /** * The cursor is placed not on the input line (or within the prompt string) @@ -460,6 +482,7 @@ void PythonConsole::keyPressEvent(QKeyEvent * e) case Qt::Key_Return: case Qt::Key_Enter: case Qt::Key_Escape: + case Qt::Key_Backspace: this->moveCursor( QTextCursor::End ); break; @@ -488,19 +511,20 @@ void PythonConsole::keyPressEvent(QKeyEvent * e) * - 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 + QString inputLine = inputBlock.text(); + QString inputStrg = stripPromptFrom( inputLine ); switch (e->key()) { case Qt::Key_Escape: { - // disable current input line - i.e. put it to history but don't execute it. - if (!inputLine.isEmpty()) + // disable current input string - i.e. put it to history but don't execute it. + if (!inputStrg.isEmpty()) { - d->history.append( QLatin1String("# ") + inputLine ); //< put line to history ... - inputLineBegin.insertText( QString::fromAscii("# ") ); //< but comment it on console + d->history.append( QLatin1String("# ") + inputStrg ); //< put commented string to history ... + inputLineBegin.insertText( QString::fromAscii("# ") ); //< and comment it on console setTextCursor( inputLineBegin ); - printPrompt(d->interpreter->hasPendingInput() //< print adequate prompt + printPrompt(d->interpreter->hasPendingInput() //< print adequate prompt ? PythonConsole::Incomplete : PythonConsole::Complete); } @@ -509,8 +533,8 @@ void PythonConsole::keyPressEvent(QKeyEvent * e) case Qt::Key_Return: case Qt::Key_Enter: { - runSource( inputLine ); //< commit input line - d->history.append( inputLine ); //< put statement to history + d->history.append( inputStrg ); //< put statement to history + runSource( inputStrg ); //< commit input string } break; case Qt::Key_Period: @@ -518,14 +542,14 @@ void PythonConsole::keyPressEvent(QKeyEvent * e) // analyse context and show available call tips int contextLength = cursor.position() - inputLineBegin.position(); TextEdit::keyPressEvent(e); - d->callTipsList->showTips( inputLine.left( contextLength ) ); + d->callTipsList->showTips( inputStrg.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 ); + cursor.setPosition( inputLineBegin.position(), mode ); setTextCursor( cursor ); ensureCursorVisible(); } break; @@ -533,7 +557,7 @@ void PythonConsole::keyPressEvent(QKeyEvent * e) case Qt::Key_Up: { // if possible, move back in history - if (d->history.prev( inputLine )) + if (d->history.prev( inputStrg )) { overrideCursor( d->history.value() ); } restartHistory = false; } break; @@ -561,7 +585,7 @@ void PythonConsole::keyPressEvent(QKeyEvent * e) case Qt::Key_Backspace: { - if (cursor > inputLineBegin) + if (cursorBeyond( cursor, inputLineBegin, +1 )) { TextEdit::keyPressEvent(e); } } break; @@ -576,7 +600,7 @@ void PythonConsole::keyPressEvent(QKeyEvent * e) { d->callTipsList->validateCursor(); } // disable history restart if input line changed - restartHistory &= (inputLine != inputBlock.text().mid(promptLength)); + restartHistory &= (inputLine != inputBlock.text()); } // any cursor move resets the history to its latest item. if (restartHistory) @@ -623,37 +647,40 @@ void PythonConsole::printPrompt(PythonConsole::Prompt mode) d->error = QString::null; } - // Append the prompt string - QTextCursor cursor = textCursor(); - cursor.beginEditBlock(); - cursor.movePosition(QTextCursor::End); - QTextBlock block = cursor.block(); - - // Python's print command appends a trailing '\n' to the system output. - // In this case, however, we should not add a new text block. We force - // the current block to be normal text (user state = 0) to be highlighted - // correctly and append the '>>> ' or '... ' to this block. - if (block.length() > 1) - cursor.insertBlock(cursor.blockFormat(), cursor.charFormat()); - else - block.setUserState(0); - - switch (mode) + if (mode != PythonConsole::Special) { - case PythonConsole::Incomplete: - cursor.insertText(QString::fromAscii("... ")); - break; - case PythonConsole::Complete: - cursor.insertText(QString::fromAscii(">>> ")); - break; - default: - break; - } - cursor.endEditBlock(); + // Append the prompt string + QTextCursor cursor = textCursor(); + cursor.beginEditBlock(); + cursor.movePosition(QTextCursor::End); + QTextBlock block = cursor.block(); + + // Python's print command appends a trailing '\n' to the system output. + // In this case, however, we should not add a new text block. We force + // the current block to be normal text (user state = 0) to be highlighted + // correctly and append the '>>> ' or '... ' to this block. + if (block.length() > 1) + cursor.insertBlock(cursor.blockFormat(), cursor.charFormat()); + else + block.setUserState(0); + + switch (mode) + { + case PythonConsole::Incomplete: + cursor.insertText(QString::fromAscii("... ")); + break; + case PythonConsole::Complete: + cursor.insertText(QString::fromAscii(">>> ")); + break; + default: + break; + } + cursor.endEditBlock(); - // move cursor to the end - cursor.movePosition(QTextCursor::End); - setTextCursor(cursor); + // move cursor to the end + cursor.movePosition(QTextCursor::End); + setTextCursor(cursor); + } } /** @@ -683,6 +710,17 @@ void PythonConsole::appendOutput(const QString& output, int state) */ void PythonConsole::runSource(const QString& line) { + /** + * Check if there's a "source drain", which want's to consume the source in another way then just executing it. + * If so, put the source to the drain and emit a signal to notify the consumer, whoever this may be. + */ + if (this->_sourceDrain) + { + *this->_sourceDrain = line; + Q_EMIT pendingSource(); + return; + } + bool incomplete = false; Base::PyGILStateLocker lock; PyObject* default_stdout = PySys_GetObject("stdout"); @@ -692,8 +730,11 @@ void PythonConsole::runSource(const QString& line) d->interactive = true; try { + d->history.markScratch(); //< mark current history position ... // launch the command now incomplete = d->interpreter->push(line.toUtf8()); + if (!incomplete) + { d->history.doScratch(); } //< ... and scratch history entries that might have been added by executing the line. setFocus(); // if focus was lost } catch (const Base::SystemExitException&) { @@ -923,7 +964,8 @@ QTextCursor PythonConsole::inputBegin( void ) const QTextCursor inputLineBegin( this->textCursor() ); inputLineBegin.movePosition( QTextCursor::End ); inputLineBegin.movePosition( QTextCursor::StartOfLine ); - inputLineBegin.movePosition( QTextCursor::Right, QTextCursor::MoveAnchor, promptLength ); + // ... and move cursor right beyond the prompt. + inputLineBegin.movePosition( QTextCursor::Right, QTextCursor::MoveAnchor, promptLength( inputLineBegin.block().text() ) ); return inputLineBegin; } @@ -948,10 +990,7 @@ QMimeData * PythonConsole::createMimeDataFromSelection () const int pos = b.position(); if ( pos >= s && pos <= e ) { if (b.userState() > -1 && b.userState() < pythonSyntax->maximumUserState()) { - QString line = b.text(); - // and skip the prompt characters consisting of either ">>> " or "... " - line = line.mid(promptLength); - lines << line; + lines << stripPromptFrom( b.text() ); } } } @@ -1020,8 +1059,7 @@ void PythonConsole::runSourceFromMimeData(const QString& source) QString select = cursor.selectedText(); cursor.removeSelectedText(); last = last + select; - line = cursor.block().text(); - line = line.mid(promptLength); + line = stripPromptFrom( cursor.block().text() ); } // put statement to the history @@ -1073,12 +1111,10 @@ void PythonConsole::runSourceFromMimeData(const QString& source) void PythonConsole::overrideCursor(const QString& txt) { // Go to the last line and the fourth position, right after the prompt - QTextCursor cursor = textCursor(); - QTextBlock block = cursor.block(); - cursor.movePosition(QTextCursor::End); - cursor.movePosition(QTextCursor::StartOfLine); - cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, promptLength); - cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, block.text().length()); + QTextCursor cursor = this->inputBegin(); + int blockLength = this->textCursor().block().text().length(); + + cursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, blockLength ); //<< select text to override cursor.removeSelectedText(); cursor.insertText(txt); // move cursor to the end @@ -1090,12 +1126,7 @@ void PythonConsole::contextMenuEvent ( QContextMenuEvent * e ) { QMenu menu(this); QAction *a; - // construct reference cursor at begin of input line ... - QTextCursor cursor = this->textCursor(); - QTextCursor inputLineBegin = cursor; - inputLineBegin.movePosition(QTextCursor::End); - inputLineBegin.movePosition(QTextCursor::StartOfLine); - inputLineBegin.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, promptLength); + bool mayPasteHere = cursorBeyond( this->textCursor(), this->inputBegin() ); a = menu.addAction(tr("&Copy"), this, SLOT(copy()), Qt::CTRL+Qt::Key_C); a->setEnabled(textCursor().hasSelection()); @@ -1113,7 +1144,7 @@ void PythonConsole::contextMenuEvent ( QContextMenuEvent * e ) a = menu.addAction(tr("&Paste"), this, SLOT(paste()), Qt::CTRL+Qt::Key_V); const QMimeData *md = QApplication::clipboard()->mimeData(); - a->setEnabled(cursor >= inputLineBegin && md && canInsertFromMimeData(md)); + a->setEnabled( mayPasteHere && md && canInsertFromMimeData(md)); a = menu.addAction(tr("Select All"), this, SLOT(selectAll()), Qt::CTRL+Qt::Key_A); a->setEnabled(!document()->isEmpty()); @@ -1195,6 +1226,20 @@ void PythonConsole::onCopyCommand() d->type = PythonConsoleP::Normal; } +QString PythonConsole::readline( void ) +{ + QEventLoop loop; + QString inputBuffer; + + printPrompt( PythonConsole::Special ); + this->_sourceDrain = &inputBuffer; //< enable source drain ... + // ... and wait until we get notified about pendingSource + QObject::connect( this, SIGNAL(pendingSource()), &loop, SLOT(quit()) ); + loop.exec(); + this->_sourceDrain = NULL; //< disable source drain + return inputBuffer.append(QChar::fromAscii('\n')); //< pass a newline here, since the readline-caller may need it! +} + // --------------------------------------------------------------------- PythonConsoleHighlighter::PythonConsoleHighlighter(QObject* parent) @@ -1245,6 +1290,7 @@ void PythonConsoleHighlighter::colorChanged(const QString& type, const QColor& c // --------------------------------------------------------------------- ConsoleHistory::ConsoleHistory() +: _scratchBegin(0) { _it = _history.end(); } @@ -1345,6 +1391,28 @@ void ConsoleHistory::restart( void ) _it = _history.end(); } +/** + * markScratch stores the current end index of the history list. + * Note: with simply remembering a start index, it does not work to nest scratch regions. + * However, just replace the index keeping by a stack - in case this is be a concern. + */ +void ConsoleHistory::markScratch( void ) +{ + _scratchBegin = _history.length(); +} + +/** + * doScratch removes the tail of the history list, starting from the index marked lately. + */ +void ConsoleHistory::doScratch( void ) +{ + if (_scratchBegin < _history.length()) + { + _history.erase( _history.begin() + _scratchBegin, _history.end() ); + this->restart(); + } +} + // ----------------------------------------------------- /* TRANSLATOR Gui::PythonInputField */ diff --git a/src/Gui/PythonConsole.h b/src/Gui/PythonConsole.h index ceda0a5ec..b949894d9 100644 --- a/src/Gui/PythonConsole.h +++ b/src/Gui/PythonConsole.h @@ -79,10 +79,13 @@ public: void append(const QString &inputLine); const QStringList& values() const; void restart(); + void markScratch( void ); + void doScratch( void ); private: QStringList _history; QStringList::ConstIterator _it; + int _scratchBegin; QString _prefix; }; @@ -99,7 +102,8 @@ public: enum Prompt { Complete = 0, Incomplete = 1, - Flush = 2 + Flush = 2, + Special = 3 }; PythonConsole(QWidget *parent = 0); @@ -107,6 +111,7 @@ public: void OnChange( Base::Subject &rCaller,const char* rcReason ); void printStatement( const QString& cmd ); + QString readline( void ); public Q_SLOTS: void onSaveHistoryAs(); @@ -126,7 +131,7 @@ protected: void dragEnterEvent ( QDragEnterEvent * e ); void dragMoveEvent ( QDragMoveEvent * e ); void changeEvent ( QEvent * e ); - void mouseReleaseEvent( QMouseEvent * e ); + void mouseReleaseEvent( QMouseEvent * e ); void overrideCursor(const QString& txt); @@ -146,6 +151,9 @@ private: void runSourceFromMimeData(const QString&); void appendOutput(const QString&, int); +Q_SIGNALS: + void pendingSource( void ); + private: struct PythonConsoleP* d; @@ -154,6 +162,7 @@ private: private: PythonConsoleHighlighter* pythonSyntax; + QString *_sourceDrain; }; /** diff --git a/src/Gui/PythonConsolePy.cpp b/src/Gui/PythonConsolePy.cpp index 5e9d18817..b9fe2a5f0 100644 --- a/src/Gui/PythonConsolePy.cpp +++ b/src/Gui/PythonConsolePy.cpp @@ -317,16 +317,5 @@ Py::Object PythonStdin::repr() Py::Object PythonStdin::readline(const Py::Tuple& args) { - if (console) - console->onFlush(); - timer->stop(); - QEventLoop loop; - QObject::connect(editField, SIGNAL(textEntered()), &loop, SLOT(quit())); - editField->clear(); - editField->show(); - editField->setFocus(); - loop.exec(); - QString txt = editField->getText(); - timer->start(); - return Py::String((const char*)txt.toAscii()); + return Py::String( (const char *)console->readline().toAscii() ); }