1 // Copyright (C) 2007-2016 CEA/DEN, EDF R&D, OPEN CASCADE
3 // Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN,
4 // CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS
6 // This library is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU Lesser General Public
8 // License as published by the Free Software Foundation; either
9 // version 2.1 of the License, or (at your option) any later version.
11 // This library is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 // Lesser General Public License for more details.
16 // You should have received a copy of the GNU Lesser General Public
17 // License along with this library; if not, write to the Free Software
18 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 // See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
22 // File : PyConsole_Editor.cxx
23 // Author : Vadim SANDLER, Open CASCADE S.A.S. (vadim.sandler@opencascade.com)
26 \class PyConsole_Editor
27 \brief Python command line interpreter front-end GUI widget.
29 This class provides simple GUI interface to the Python interpreter, including basic
30 navigation operations, executing commands (both interactively and programmatically),
31 copy-paste operations, history of the commands and so on.
33 Here below is the shortcut keyboard boundings used for navigation and other operations:
34 - <Enter> : execute current command
35 - <Ctrl><Break> : clear current command
36 - <Escape> : clear current command
37 - <Up> : previous command in the history
38 - <Shift><Up> : move cursor one row up with selection
39 - <Ctrl><Up> : move cursor one row up without selection
40 - <Ctrl><Shift><Up> : move cursor one row up with selection
41 - <Down> : next command in the history
42 - <Shift><Down> : move cursor one row down with selection
43 - <Ctrl><Down> : move cursor one row down without selection
44 - <Ctrl><Shift><Down> : move cursor one row down with selection
45 - <Left> : move one symbol left without selection
46 - <Shift><Left> : move one symbol left with selection
47 - <Ctrl><Left> : move one word left without selection
48 - <Ctrl><Shift><Left> : move one word left with selection
49 - <Right> : move one symbol right without selection
50 - <Shift><Right> : move one symbol right with selection
51 - <Ctrl><Right> : move one word right without selection
52 - <Ctrl><Shift><Right> : move one word right with selection
53 - <PgUp> : first command in the history
54 - <Shift><PgUp> : move one page up with selection
55 - <Ctrl><PgUp> : move one page up without selection
56 - <Ctrl><Shift><PgUp> : scroll one page up
57 - <PgDn> : last command in the history
58 - <Shift><PgDn> : move one page down with selection
59 - <Ctrl><PgDn> : move one page down without selection
60 - <Ctrl><Shift><PgDn> : scroll one page down
61 - <Home> : move to the beginning of the line without selection
62 - <Shift><Home> : move to the beginning of the line with selection
63 - <Ctrl><Home> : move to the very first symbol without selection
64 - <Ctrl><Shift><Home> : move to the very first symbol with selection
65 - <End> : move to the end of the line without selection
66 - <Shift><End> : move to the end of the line with selection
67 - <Ctrl><End> : move to the very last symbol without selection
68 - <Ctrl><Shift><End> : move to the very last symbol with selection
69 - <Backspace> : delete symbol before the cursor
70 / remove selected text and put it to the clipboard (cut)
71 - <Shift><Backspace> : delete previous word
72 / remove selected text and put it to the clipboard (cut)
73 - <Ctrl><Backspace> : delete text from the cursor to the beginning of the line
74 / remove selected text and put it to the clipboard (cut)
75 - <Delete> : delete symbol after the cursor
76 / remove selected text and put it to the clipboard (cut)
77 - <Shift><Delete> : delete next word
78 / remove selected text and put it to the clipboard (cut)
79 - <Ctrl><Delete> : delete text from the cursor to the end of the line
80 / remove selected text and put it to the clipboard (cut)
81 - <Ctrl><Insert> : copy
82 - <Shift><Insert> : paste
87 - <Tab> : performs auto-completion
88 - <Ctrl><Tab> : undoes auto-completion
91 #include "PyConsole_Editor.h"
92 #include "PyConsole_Interp.h"
93 #include "PyConsole_Event.h"
94 #include "PyInterp_Dispatcher.h"
95 #include "PyConsole_Request.h"
97 #include <QApplication>
103 #include <QMouseEvent>
104 #include <QScrollBar>
105 #include <QTextBlock>
106 #include <QTextCursor>
107 #include <QTextStream>
109 #include <QFileDialog>
110 #include <QMessageBox>
112 //VSR: uncomment below macro to support unicode text properly in SALOME
113 // current commented out due to regressions
114 //#define PAL22528_UNICODE
118 QString fromUtf8( const char* txt )
120 #ifdef PAL22528_UNICODE
121 return QString::fromUtf8( txt );
123 return QString( txt );
128 static QString READY_PROMPT = ">>> ";
129 static QString DOTS_PROMPT = "... ";
131 void staticCallbackStdout( void* data, char* c )
133 if(!((PyConsole_Editor*)data)->isSuppressOutput()) {
134 PyConsole_Editor* e = (PyConsole_Editor*)data;
135 QApplication::postEvent( e, new PyConsole_PrintEvent( fromUtf8(c), false ) );
139 void staticCallbackStderr( void* data, char* c )
141 if(!((PyConsole_Editor*)data)->isSuppressOutput()) {
142 PyConsole_Editor* e = (PyConsole_Editor*)data;
143 QApplication::postEvent( e, new PyConsole_PrintEvent( fromUtf8(c), true ) );
150 Creates python editor window.
151 \param theInterp python interper
152 \param theParent parent widget
154 PyConsole_Editor::PyConsole_Editor( PyConsole_Interp* theInterp,
156 : QTextEdit( theParent ),
157 myInterp( theInterp ),
158 myCmdInHistory( -1 ),
160 myShowBanner( true ),
162 myIsSuppressOutput( false ),
163 myMultiLinePaste( false ),
164 myAutoCompletion( false ),
166 myComplCursorPos( -1 )
168 setFont( QFont( "Courier", 11 ) ); // default font
169 setUndoRedoEnabled( false );
171 myPrompt = READY_PROMPT;
172 setLineWrapMode( QTextEdit::WidgetWidth );
173 setWordWrapMode( QTextOption::WrapAnywhere );
174 setAcceptRichText( false );
176 // set callbacks to interpeter
177 myInterp->setvoutcb( staticCallbackStdout, this );
178 myInterp->setverrcb( staticCallbackStderr, this );
180 if ( isShowBanner() )
182 // clear command buffer
183 myCommandBuffer.truncate(0);
184 // unset read-only state
185 setReadOnly( false );
186 // unset history browsing mode
191 viewport()->unsetCursor();
197 PyConsole_Editor::~PyConsole_Editor()
203 \brief Get Python interpreter
205 PyConsole_Interp* PyConsole_Editor::getInterp() const
211 \brief Get synchronous mode flag value.
214 \return \c true if python console works in synchronous mode
216 bool PyConsole_Editor::isSync() const
222 \brief Set synchronous mode flag value.
224 In synhronous mode the Python commands are executed in the GUI thread
225 and the GUI is blocked until the command is finished. In the asynchronous
226 mode each Python command is executed in the separate thread that does not
227 block the main GUI loop.
229 \param on synhronous mode flag
231 void PyConsole_Editor::setIsSync( const bool on )
237 \brief Get suppress output flag value.
239 \sa setIsSuppressOutput()
240 \return \c true if python console output is suppressed.
242 bool PyConsole_Editor::isSuppressOutput() const
244 return myIsSuppressOutput;
248 \brief Set suppress output flag value.
250 In case if suppress output flag is \c true, the python
251 console output suppressed.
253 \param on suppress output flag
255 void PyConsole_Editor::setIsSuppressOutput( const bool on )
257 myIsSuppressOutput = on;
261 \brief Get 'show banner' flag value.
263 \sa setIsShowBanner()
264 \return \c true if python console shows banner
266 bool PyConsole_Editor::isShowBanner() const
272 \brief Set 'show banner' flag value.
274 The banner is shown in the top of the python console window.
277 \param on 'show banner' flag
279 void PyConsole_Editor::setIsShowBanner( const bool on )
281 if ( myShowBanner != on ) {
288 \brief Switch on/off commands auto-completion feature
291 void PyConsole_Editor::setAutoCompletion( bool on )
293 myAutoCompletion = on;
294 document()->setUndoRedoEnabled( myAutoCompletion );
298 \brief Returns \c true if auto-completion feature is switched on
299 or \c false otherwise
300 \sa setAutoCompletion()
302 bool PyConsole_Editor::autoCompletion() const
304 return myAutoCompletion;
308 \brief Check if trace logging is switched on.
310 \sa startLog(), stopLog()
311 \return \c true if trace logging is switched on
313 bool PyConsole_Editor::isLogging() const
315 return !myLogFile.isEmpty();
319 \brief Get size hint for the Python console window
320 \return size hint value
322 QSize PyConsole_Editor::sizeHint() const
324 QFontMetrics fm( font() );
325 int nbLines = ( isShowBanner() ? banner().split("\n").count() : 0 ) + 1;
326 QSize s(100, fm.lineSpacing()*nbLines);
331 \brief Put the string \a str to the python editor.
332 \param str string to be put in the command line of the editor
333 \param newBlock if \c true, then the string is printed on a new line
334 \param isError if \c true, the text is printed in dark red
336 void PyConsole_Editor::addText( const QString& str,
340 QTextCursor aCursor = textCursor();
343 moveCursor( QTextCursor::End );
345 aCursor.insertBlock();
347 cf.setForeground( QBrush( Qt::red ) );
349 cf.setForeground( QBrush( Qt::black ) );
350 aCursor.insertText( str, cf );
351 moveCursor( QTextCursor::End );
352 ensureCursorVisible();
356 \brief Convenient method for executing a Python command,
357 as if the user typed it manually.
358 \param command python command to be executed
360 !!! WARNING: doesn't work properly with multi-line commands. !!!
362 void PyConsole_Editor::exec( const QString& command )
364 if ( isReadOnly() ) {
365 // some interactive command is being executed in this editor...
366 // shedule the command to the queue
367 myQueue.push_back( command );
372 moveCursor( QTextCursor::End );
373 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
374 textCursor().removeSelectedText();
376 // set "ready" prompt
377 myPrompt = READY_PROMPT;
379 // clear command buffer
380 myCommandBuffer.truncate( 0 );
382 // unset history browsing mode
385 // print command line by line
386 QString cmd = command;
387 if ( !cmd.endsWith( "\n" ) ) cmd += "\n";
388 QStringList lines = command.split( "\n" );
389 for ( int i = 0; i < lines.size(); i++ ) {
390 if ( !lines[i].trimmed().isEmpty() )
391 myHistory.push_back( lines[i] );
392 addText( ( i == 0 ? READY_PROMPT : DOTS_PROMPT ) + lines[i], i != 0 );
393 putLog( QString( "%1%2\n" ).arg( i == 0 ? READY_PROMPT : DOTS_PROMPT ).arg( lines[i] ) );
399 // set read-only mode
403 setCursor( Qt::BusyCursor );
405 // post a request to execute Python command;
406 // editor will be informed via a custom event that execution has been completed
407 PyInterp_Dispatcher::Get()->Exec( createCmdRequest( cmd ) );
411 \brief Create request to the python dispatcher for the command execution.
412 \param command python command to be executed
414 PyInterp_Request* PyConsole_Editor::createCmdRequest( const QString& command )
416 return new PyConsole_ExecCommand( myInterp, command, this, isSync() );
420 \brief Create the Python request that will be posted to the interpreter to
422 \param input line entered by the user at the time <TAB> was pressed
423 \return completion command
424 \sa CompletionCommand
426 PyInterp_Request* PyConsole_Editor::createTabRequest( const QString& input )
429 static QStringList separators;
430 if ( separators.isEmpty() ) {
431 separators << " " << "(" << "[" << "+" << "-" << "*" << "/" << ";" << "^" << "=";
434 // parse input to extract on what part the dir() has to be executed
436 QString input2( input );
438 // split up to the last syntaxical separator
440 foreach ( QString separator, separators ) {
441 int j = input2.lastIndexOf( separator );
446 input2 = input.mid( lastSp + 1 );
448 // detect a qualified name (with a point)
449 int lastPt = input2.lastIndexOf( "." );
451 if ( lastPt != -1 ) {
452 // split the 2 surrounding parts of the qualified name
453 myComplBeforePoint = input2.left( lastPt );
454 myComplAfterPoint = input2.mid( lastPt+1 );
457 // no point found - do a global matching
458 // (the following will call dir() with an empty string)
459 myComplAfterPoint = input2;
460 myComplBeforePoint = "";
463 return new PyConsole_CompletionCommand( myInterp, myComplBeforePoint,
464 myComplAfterPoint, this, isSync() );
468 \brief Execute command in the python interpreter
469 and wait until it is finished.
471 \param command python command to be executed
473 void PyConsole_Editor::execAndWait( const QString& command )
479 // create new event loop
480 bool sync = isSync();
482 myEventLoop = new QEventLoop( this );
491 // delete event loop after command is processed
498 \brief Process <Enter> key press event.
500 Execute the command entered by the user.
502 void PyConsole_Editor::handleReturn()
504 // Position cursor at the end
505 QTextCursor aCursor = textCursor();
506 aCursor.movePosition( QTextCursor::End );
507 setTextCursor( aCursor );
510 QTextBlock par = document()->end().previous();
511 if ( !par.isValid() ) return;
514 QString cmd = par.text().remove( 0, promptSize() );
516 // extend the command buffer with the current command
517 myCommandBuffer.append( cmd );
519 // add command to the history
520 if ( !cmd.trimmed().isEmpty() )
521 myHistory.push_back( cmd );
522 putLog( QString( "%1%2\n" ).arg( myPrompt ).arg( cmd ) );
527 // set read-only mode
531 setCursor( Qt::BusyCursor );
533 // post a request to execute Python command;
534 // editor will be informed via a custom event that execution has been completed
535 PyInterp_Dispatcher::Get()->Exec( createCmdRequest( myCommandBuffer ) );
539 \brief Process <Tab> key press event.
541 Perform auto-completion of the currently entered command, if this feature is enabled
543 void PyConsole_Editor::handleTab()
545 if ( !autoCompletion() )
546 return; // auto-completion feature is disabled
549 return; // already in tab mode
551 QTextCursor aCursor = textCursor();
553 // move cursor to the end of input
554 aCursor.movePosition( QTextCursor::End );
555 setTextCursor( aCursor );
557 // save cursor position if needed
558 if ( myComplCursorPos == -1 )
559 myComplCursorPos = textCursor().position();
562 QTextBlock par = document()->end().previous();
563 if ( !par.isValid() ) return; // empty line
565 // switch to completion mode
568 // get currently entered command
569 QString cmd = par.text().mid( promptSize() );
571 // post completion request
572 // editor will be informed that completion has been done via a custom event
573 PyInterp_Dispatcher::Get()->Exec( createTabRequest( cmd ) );
577 \brief Process <Ctrl><Tab> key press event.
579 Undoe last auto-completion
581 void PyConsole_Editor::handleBackTab()
583 if ( !autoCompletion() )
584 return; // auto-completion feature is disabled
586 QTextCursor aCursor = textCursor();
588 if ( myComplCursorPos == -1 )
589 return; // invalid cursor position
591 // ensure cursor is at the end of command line
592 aCursor.setPosition( myComplCursorPos );
593 aCursor.movePosition( QTextCursor::EndOfLine );
594 //setCursor( aCursor );
596 // delete last completed text
597 int i = aCursor.position() - myComplCursorPos;
598 aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, i );
599 aCursor.removeSelectedText();
600 myComplCursorPos = -1;
604 \brief Process drop event.
607 \param event drop event
609 void PyConsole_Editor::dropEvent( QDropEvent* event )
611 // get the initial drop position
612 QPoint pos = event->pos();
613 QTextCursor aCursor = cursorForPosition( event->pos() );
615 // if the position is not in the last line move it to the end of the command line
616 if ( aCursor.position() < document()->end().previous().position() + promptSize() ) {
617 moveCursor( QTextCursor::End );
618 pos = cursorRect().center();
621 // create new drop event and use it instead of the original
623 event->possibleActions(),
625 event->mouseButtons(),
626 event->keyboardModifiers(),
628 QTextEdit::dropEvent( &de );
630 // accept the original event
631 event->acceptProposedAction();
635 \brief Process mouse press event
637 Clear the completion when any mouse button is pressed.
639 \param e mouse press event
641 void PyConsole_Editor::mousePressEvent( QMouseEvent* event )
643 if ( autoCompletion() ) {
645 myComplCursorPos = -1;
647 QTextEdit::mousePressEvent( event );
651 \brief Process mouse button release event.
653 Left mouse button: copy selection to the clipboard.
654 Middle mouse button: paste clipboard's contents.
656 \param event mouse event
658 void PyConsole_Editor::mouseReleaseEvent( QMouseEvent* event )
660 if ( event->button() == Qt::LeftButton ) {
661 QTextEdit::mouseReleaseEvent( event );
663 else if ( event->button() == Qt::MidButton ) {
664 QTextCursor aCursor = cursorForPosition( event->pos() );
665 // if the position is not in the last line move it to the end of the command line
666 if ( aCursor.position() < document()->end().previous().position() + promptSize() ) {
667 moveCursor( QTextCursor::End );
670 setTextCursor( aCursor );
672 const QMimeData* md = QApplication::clipboard()->mimeData( QApplication::clipboard()->supportsSelection() ?
673 QClipboard::Selection : QClipboard::Clipboard );
675 insertFromMimeData( md );
678 QTextEdit::mouseReleaseEvent( event );
683 \brief Check if the string is command.
685 Return \c true if the string \a str is likely to be the command
686 (i.e. it is started from the '>>>' or '...').
687 \param str string to be checked
689 bool PyConsole_Editor::isCommand( const QString& str ) const
691 return str.startsWith( READY_PROMPT ) || str.startsWith( DOTS_PROMPT );
695 \brief Handle keyboard event.
697 Implement navigation, history browsing, copy/paste and other common
700 \param event keyboard event
702 void PyConsole_Editor::keyPressEvent( QKeyEvent* event )
704 // get cursor position
705 QTextCursor aCursor = textCursor();
706 int curLine = aCursor.blockNumber();
707 int curCol = aCursor.columnNumber();
709 // get last edited line
710 int endLine = document()->blockCount()-1;
712 // get pressed key code
713 int aKey = event->key();
715 // check if <Ctrl> is pressed
716 bool ctrlPressed = event->modifiers() & Qt::ControlModifier;
717 // check if <Shift> is pressed
718 bool shftPressed = event->modifiers() & Qt::ShiftModifier;
720 if ( autoCompletion() ) {
721 // auto-completion support
722 if ( aKey == Qt::Key_Tab && !shftPressed ) {
724 if ( !ctrlPressed ) {
734 // If <Ctrl> is not pressed (or if something else is pressed with <Ctrl>),
735 // or if <Ctrl> is not pressed alone, we have to clear completion
736 if ( !ctrlPressed || ( ctrlPressed && aKey != Qt::Key_Control ) ) {
738 myComplCursorPos = -1;
741 // Discard <Ctrl> pressed alone:
742 if ( aKey == Qt::Key_Control )
746 if ( aKey == Qt::Key_Escape || ( ctrlPressed && aKey == -1 ) ) {
747 // process <Ctrl>+<Break> key-binding and <Escape> key: clear current command
748 myCommandBuffer.truncate( 0 );
749 myPrompt = READY_PROMPT;
750 addText( myPrompt, true );
751 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
754 else if ( ctrlPressed && aKey == Qt::Key_C ) {
755 // process <Ctrl>+<C> key-binding : copy
759 else if ( ctrlPressed && aKey == Qt::Key_X ) {
760 // process <Ctrl>+<X> key-binding : cut
764 else if ( ctrlPressed && aKey == Qt::Key_V ) {
765 // process <Ctrl>+<V> key-binding : paste
770 // check for printed key
771 // #### aKey = ( aKey < Qt::Key_Space || aKey > Qt::Key_ydiaeresis ) ? aKey : 0;
773 aKey = !(QChar(aKey).isPrint()) ? aKey : 0;
777 // any printed key: just print it
779 if ( curLine < endLine || curCol < promptSize() ) {
780 moveCursor( QTextCursor::End );
782 QTextEdit::keyPressEvent( event );
787 // <Enter> key: process the current command
793 // <Up> arrow key: process as follows:
794 // - without <Ctrl>, <Shift> modifiers: previous command in history
795 // - with <Ctrl> modifier key pressed: move cursor one row up without selection
796 // - with <Shift> modifier key pressed: move cursor one row up with selection
797 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one row up
799 if ( ctrlPressed && shftPressed ) {
800 int value = verticalScrollBar()->value();
801 int spacing = fontMetrics().lineSpacing();
802 verticalScrollBar()->setValue( value > spacing ? value-spacing : 0 );
804 else if ( shftPressed || ctrlPressed ) {
806 moveCursor( QTextCursor::Up,
807 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
810 if ( myCmdInHistory < 0 && myHistory.count() > 0 ) {
811 // set history browsing mode
812 myCmdInHistory = myHistory.count();
813 // remember current command
814 QTextBlock par = document()->end().previous();
815 myCurrentCommand = par.text().remove( 0, promptSize() );
817 if ( myCmdInHistory > 0 ) {
819 // get previous command in the history
820 QString previousCommand = myHistory.at( myCmdInHistory );
821 // print previous command
822 moveCursor( QTextCursor::End );
823 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
824 textCursor().removeSelectedText();
825 addText( myPrompt + previousCommand );
826 // move cursor to the end
827 moveCursor( QTextCursor::End );
833 // <Down> arrow key: process as follows:
834 // - without <Ctrl>, <Shift> modifiers: next command in history
835 // - with <Ctrl> modifier key pressed: move cursor one row down without selection
836 // - with <Shift> modifier key pressed: move cursor one row down with selection
837 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one row down
839 if ( ctrlPressed && shftPressed ) {
840 int value = verticalScrollBar()->value();
841 int maxval = verticalScrollBar()->maximum();
842 int spacing = fontMetrics().lineSpacing();
843 verticalScrollBar()->setValue( value+spacing < maxval ? value+spacing : maxval );
845 else if ( shftPressed || ctrlPressed) {
846 if ( curLine < endLine )
847 moveCursor( QTextCursor::Down,
848 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
851 if ( myCmdInHistory >= 0 ) {
852 // get next command in the history
855 if ( myCmdInHistory < myHistory.count() ) {
856 // next command in history
857 nextCommand = myHistory.at( myCmdInHistory );
860 // end of history is reached
861 // last printed command
862 nextCommand = myCurrentCommand;
863 // unset history browsing mode
866 // print next or current command
867 moveCursor( QTextCursor::End );
868 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
869 textCursor().removeSelectedText();
870 addText( myPrompt + nextCommand );
871 // move cursor to the end
872 moveCursor( QTextCursor::End );
878 // <Left> arrow key: process as follows:
879 // - without <Ctrl>, <Shift> modifiers: move one symbol left (taking into account prompt)
880 // - with <Ctrl> modifier key pressed: move one word left (taking into account prompt)
881 // - with <Shift> modifier key pressed: move one symbol left with selection
882 // - with <Ctrl>+<Shift> modifier keys pressed: move one word left with selection
884 QString txt = textCursor().block().text();
885 if ( !shftPressed && isCommand( txt ) && curCol <= promptSize() ) {
886 moveCursor( QTextCursor::Up );
887 moveCursor( QTextCursor::EndOfBlock );
890 QTextEdit::keyPressEvent( event );
895 // <Right> arrow key: process as follows:
896 // - without <Ctrl>, <Shift> modifiers: move one symbol right (taking into account prompt)
897 // - with <Ctrl> modifier key pressed: move one word right (taking into account prompt)
898 // - with <Shift> modifier key pressed: move one symbol right with selection
899 // - with <Ctrl>+<Shift> modifier keys pressed: move one word right with selection
901 QString txt = textCursor().block().text();
902 if ( !shftPressed ) {
903 if ( curCol < txt.length() ) {
904 if ( isCommand( txt ) && curCol < promptSize() ) {
905 aCursor.setPosition( aCursor.block().position() + promptSize() );
906 setTextCursor( aCursor );
911 if ( curLine < endLine && isCommand( textCursor().block().next().text() ) ) {
912 aCursor.setPosition( aCursor.position() + promptSize()+1 );
913 setTextCursor( aCursor );
914 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
919 QTextEdit::keyPressEvent( event );
923 // <PageUp> key: process as follows:
924 // - without <Ctrl>, <Shift> modifiers: first command in history
925 // - with <Ctrl> modifier key pressed: move cursor one page up without selection
926 // - with <Shift> modifier key pressed: move cursor one page up with selection
927 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one page up
929 if ( ctrlPressed && shftPressed ) {
930 verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub);
932 else if ( shftPressed || ctrlPressed ) {
934 qreal lastY = cursorRect( aCursor ).top();
936 // move using movePosition to keep the cursor's x
938 qreal y = cursorRect( aCursor ).top();
939 distance += qAbs( y - lastY );
941 moved = aCursor.movePosition( QTextCursor::Up,
942 shftPressed ? QTextCursor::KeepAnchor :
943 QTextCursor::MoveAnchor );
944 } while ( moved && distance < viewport()->height() );
946 aCursor.movePosition( QTextCursor::Down,
947 shftPressed ? QTextCursor::KeepAnchor :
948 QTextCursor::MoveAnchor );
949 verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub );
951 setTextCursor( aCursor );
954 if ( myCmdInHistory < 0 && myHistory.count() > 0 ) {
955 // set history browsing mode
956 myCmdInHistory = myHistory.count();
957 // remember current command
958 QTextBlock par = document()->end().previous();
959 myCurrentCommand = par.text().remove( 0, promptSize() );
961 if ( myCmdInHistory > 0 ) {
963 // get very first command in the history
964 QString firstCommand = myHistory.at( myCmdInHistory );
965 // print first command
966 moveCursor( QTextCursor::End );
967 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
968 textCursor().removeSelectedText();
969 addText( myPrompt + firstCommand );
970 // move cursor to the end
971 moveCursor( QTextCursor::End );
976 case Qt::Key_PageDown:
977 // <PageDown> key: process as follows:
978 // - without <Ctrl>, <Shift> modifiers: last command in history
979 // - with <Ctrl> modifier key pressed: move cursor one page down without selection
980 // - with <Shift> modifier key pressed: move cursor one page down with selection
981 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one page down
983 if ( ctrlPressed && shftPressed ) {
984 verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd);
986 else if ( shftPressed || ctrlPressed ) {
988 qreal lastY = cursorRect( aCursor ).top();
990 // move using movePosition to keep the cursor's x
992 qreal y = cursorRect( aCursor ).top();
993 distance += qAbs( y - lastY );
995 moved = aCursor.movePosition( QTextCursor::Down,
996 shftPressed ? QTextCursor::KeepAnchor :
997 QTextCursor::MoveAnchor );
998 } while ( moved && distance < viewport()->height() );
1000 aCursor.movePosition( QTextCursor::Up,
1001 shftPressed ? QTextCursor::KeepAnchor :
1002 QTextCursor::MoveAnchor );
1003 verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub );
1005 setTextCursor( aCursor );
1008 if ( myCmdInHistory >= 0 ) {
1009 // unset history browsing mode
1010 myCmdInHistory = -1;
1011 // print current command
1012 moveCursor( QTextCursor::End );
1013 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
1014 textCursor().removeSelectedText();
1015 addText( myPrompt + myCurrentCommand );
1016 // move cursor to the end
1017 moveCursor( QTextCursor::End );
1023 // <Home> key: process as follows:
1024 // - without <Ctrl>, <Shift> modifiers: move cursor to the beginning of the current line without selection
1025 // - with <Ctrl> modifier key pressed: move cursor to the very first symbol without selection
1026 // - with <Shift> modifier key pressed: move cursor to the beginning of the current line with selection
1027 // - with <Ctrl>+<Shift> modifier keys pressed: move cursor to the very first symbol with selection
1029 if ( ctrlPressed ) {
1030 moveCursor( QTextCursor::Start,
1031 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1034 QString txt = textCursor().block().text();
1035 if ( isCommand( txt ) ) {
1036 if ( shftPressed ) {
1037 if ( curCol > promptSize() ) {
1038 aCursor.movePosition( QTextCursor::StartOfLine, QTextCursor::KeepAnchor );
1039 aCursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, promptSize() );
1043 aCursor.movePosition( QTextCursor::StartOfLine );
1044 aCursor.movePosition( QTextCursor::Right, QTextCursor::MoveAnchor, promptSize() );
1046 setTextCursor( aCursor );
1049 moveCursor( QTextCursor::StartOfBlock,
1050 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1052 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1057 // <End> key: process as follows:
1058 // - without <Ctrl>, <Shift> modifiers: move cursor to the end of the current line without selection
1059 // - with <Ctrl> modifier key pressed: move cursor to the very last symbol without selection
1060 // - with <Shift> modifier key pressed: move cursor to the end of the current line with selection
1061 // - with <Ctrl>+<Shift> modifier keys pressed: move cursor to the very last symbol with selection
1063 moveCursor( ctrlPressed ? QTextCursor::End : QTextCursor::EndOfBlock,
1064 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1067 case Qt::Key_Backspace :
1068 // <Backspace> key: process as follows
1069 // - without any modifiers : delete symbol before the cursor / selection (taking into account prompt)
1070 // - with <Shift> modifier key pressed: delete previous word
1071 // - with <Ctrl> modifier key pressed: delete text from the cursor to the line beginning
1072 // works only for last (command) line
1074 if ( aCursor.hasSelection() ) {
1077 else if ( aCursor.position() > document()->end().previous().position() + promptSize() ) {
1078 if ( shftPressed ) {
1079 moveCursor( QTextCursor::PreviousWord, QTextCursor::KeepAnchor );
1080 textCursor().removeSelectedText();
1082 else if ( ctrlPressed ) {
1083 aCursor.setPosition( document()->end().previous().position() + promptSize(),
1084 QTextCursor::KeepAnchor );
1085 setTextCursor( aCursor );
1086 textCursor().removeSelectedText();
1089 QTextEdit::keyPressEvent( event );
1093 aCursor.setPosition( document()->end().previous().position() + promptSize() );
1094 setTextCursor( aCursor );
1095 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1099 case Qt::Key_Delete :
1100 // <Delete> key: process as follows
1101 // - without any modifiers : delete symbol after the cursor / selection (taking into account prompt)
1102 // - with <Shift> modifier key pressed: delete next word
1103 // - with <Ctrl> modifier key pressed: delete text from the cursor to the end of line
1104 // works only for last (command) line
1106 if ( aCursor.hasSelection() ) {
1109 else if ( aCursor.position() > document()->end().previous().position() + promptSize()-1 ) {
1110 if ( shftPressed ) {
1111 moveCursor( QTextCursor::NextWord, QTextCursor::KeepAnchor );
1112 textCursor().removeSelectedText();
1114 else if ( ctrlPressed ) {
1115 moveCursor( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
1116 textCursor().removeSelectedText();
1119 QTextEdit::keyPressEvent( event );
1123 aCursor.setPosition( document()->end().previous().position() + promptSize() );
1124 setTextCursor( aCursor );
1125 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1129 case Qt::Key_Insert :
1130 // <Insert> key: process as follows
1131 // - with <Ctrl> modifier key pressed: copy()
1132 // - with <Shift> modifier key pressed: paste() to the command line
1134 if ( ctrlPressed ) {
1137 else if ( shftPressed ) {
1141 QTextEdit::keyPressEvent( event );
1150 \brief Handle notification event coming from Python dispatcher.
1151 \param event notification event
1153 void PyConsole_Editor::customEvent( QEvent* event )
1155 switch( event->type() )
1157 case PyConsole_PrintEvent::EVENT_ID:
1159 PyConsole_PrintEvent* pe = (PyConsole_PrintEvent*)event;
1160 putLog( pe->text());
1161 addText( pe->text(), false, pe->isError() );
1164 case PyConsole_CompletionEvent::EVENT_ID:
1166 PyConsole_CompletionEvent* ce = (PyConsole_CompletionEvent*)event;
1167 bool status = ce->status();
1168 QStringList matches = ce->matches();
1169 QString doc = ce->doc();
1172 // completion was successful
1173 QTextCursor aCursor = textCursor();
1175 if ( matches.isEmpty() ) {
1176 // completion successful but there are no matches.
1178 myComplCursorPos = -1;
1182 if ( matches.size() == 1 ) {
1183 // there's only one match - complete directly and update doc string window
1184 aCursor.insertText( matches[0].mid( myComplAfterPoint.size() ) );
1186 if ( doc.isEmpty() )
1187 emit updateDoc( formatDocHTML( QString( "(%1)\n" ).arg( tr( "NO_DOC_AVAILABLE" ) ) ) );
1189 emit updateDoc( formatDocHTML( doc ) );
1192 // there are several matches
1194 // detect if there is a common base to all available completion
1195 // in this case append this base to the text
1196 QString base = extractCommon( matches );
1197 aCursor.insertText( base.mid( myComplAfterPoint.size() ) );
1199 // if this happens to match exactly the first completion
1201 if ( base == matches[0] )
1202 emit updateDoc( formatDocHTML( doc ) );
1204 // print all matching completion in a "undo-able" block
1205 int cursorPos = aCursor.position();
1206 aCursor.insertBlock();
1207 aCursor.beginEditBlock();
1209 // insert all matches
1211 cf.setForeground( QBrush( Qt::darkGreen ) );
1212 aCursor.setCharFormat( cf );
1213 aCursor.insertText( formatCompletion( matches ) );
1214 aCursor.endEditBlock();
1216 // position cursor where it was before inserting the completion list
1217 aCursor.setPosition( cursorPos );
1218 setTextCursor( aCursor );
1222 // completion failed
1224 myComplCursorPos = -1;
1228 case PyInterp_Event::ES_OK:
1229 case PyInterp_Event::ES_ERROR:
1231 // clear command buffer
1232 myCommandBuffer.truncate( 0 );
1233 // add caret return line if necessary
1234 QTextBlock par = document()->end().previous();
1235 QString txt = par.text();
1236 txt.truncate( txt.length() - 1 );
1237 // IPAL19397 : addText moved to handleReturn() method
1238 //if ( !txt.isEmpty() )
1239 // addText( "", true );
1240 // set "ready" prompt
1241 myPrompt = READY_PROMPT;
1242 addText( myPrompt );
1243 // unset busy cursor
1245 // stop event loop (if running)
1247 myEventLoop->exit();
1248 // if we are in multi_paste_mode, process the next item
1249 multiLineProcessNextLine();
1252 case PyInterp_Event::ES_INCOMPLETE:
1254 // extend command buffer (multi-line command)
1255 myCommandBuffer.append( "\n" );
1256 // add caret return line if necessary
1257 QTextBlock par = document()->end().previous();
1258 QString txt = par.text();
1259 txt.truncate( txt.length() - 1 );
1260 // IPAL19397 : addText moved to handleReturn() method
1261 //if ( !txt.isEmpty() )
1262 // addText( "", true );
1264 myPrompt = DOTS_PROMPT;
1265 addText( myPrompt/*, true*/ ); // IPAL19397
1266 // unset busy cursor
1268 // stop event loop (if running)
1270 myEventLoop->exit();
1271 // if we are in multi_paste_mode, process the next item
1272 multiLineProcessNextLine();
1276 QTextEdit::customEvent( event );
1279 // unset read-only state
1280 setReadOnly( false );
1281 // unset history browsing mode
1282 myCmdInHistory = -1;
1284 if ( (int)event->type() == (int)PyInterp_Event::ES_OK && myQueue.count() > 0 )
1286 // process the next sheduled command from the queue (if there is any)
1287 QString nextcmd = myQueue[0];
1288 myQueue.pop_front();
1294 \brief "Copy" operation.
1296 Reimplemented from Qt.
1297 Warning! In Qt this method is not virtual.
1299 void PyConsole_Editor::cut()
1301 QTextCursor aCursor = textCursor();
1302 if ( aCursor.hasSelection() ) {
1303 QApplication::clipboard()->setText( aCursor.selectedText() );
1304 int startSelection = aCursor.selectionStart();
1305 if ( startSelection < document()->end().previous().position() + promptSize() )
1306 startSelection = document()->end().previous().position() + promptSize();
1307 int endSelection = aCursor.selectionEnd();
1308 if ( endSelection < document()->end().previous().position() + promptSize() )
1309 endSelection = document()->end().previous().position() + promptSize();
1310 aCursor.setPosition( startSelection );
1311 aCursor.setPosition( endSelection, QTextCursor::KeepAnchor );
1312 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1313 setTextCursor( aCursor );
1314 textCursor().removeSelectedText();
1319 \brief "Paste" operation.
1321 Reimplemented from Qt.
1322 Warning! In Qt this method is not virtual.
1324 void PyConsole_Editor::paste()
1326 QTextCursor aCursor = textCursor();
1327 if ( aCursor.hasSelection() ) {
1328 int startSelection = aCursor.selectionStart();
1329 if ( startSelection < document()->end().previous().position() + promptSize() )
1330 startSelection = document()->end().previous().position() + promptSize();
1331 int endSelection = aCursor.selectionEnd();
1332 if ( endSelection < document()->end().previous().position() + promptSize() )
1333 endSelection = document()->end().previous().position() + promptSize();
1334 aCursor.setPosition( startSelection );
1335 aCursor.setPosition( endSelection, QTextCursor::KeepAnchor );
1336 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1337 setTextCursor( aCursor );
1338 textCursor().removeSelectedText();
1340 if ( textCursor().position() < document()->end().previous().position() + promptSize() )
1341 moveCursor( QTextCursor::End );
1346 \brief "Clear" operation.
1348 Reimplemented from Qt.
1349 Warning! In Qt this method is not virtual.
1351 void PyConsole_Editor::clear()
1354 if ( isShowBanner() )
1355 addText( banner() );
1356 myPrompt = READY_PROMPT;
1357 addText( myPrompt );
1361 \brief Dumps recorded Python commands to the file
1362 \param fileName path to the dump file
1363 \return \c true if dump operation succeeded or \c false otherwise
1365 bool PyConsole_Editor::dump( const QString& fileName )
1368 if ( !fileName.isEmpty() ) {
1369 QFile file( fileName );
1370 if ( file.open( QFile::WriteOnly ) ) {
1371 QTextStream out( &file );
1372 for ( int i = 0; i < myHistory.count(); i++ ) {
1373 out << myHistory[i] << endl;
1383 \brief Dump menu action slot
1385 void PyConsole_Editor::dump()
1389 QString fileName = getDumpFileName();
1391 if ( fileName.isEmpty() )
1394 if ( dump( fileName ) )
1397 QMessageBox::warning( this,
1399 tr( "ERR_FILE_NOT_WRITEABLE" ) );
1404 \brief Get file name for Dump commands operation.
1406 This function can be redefined in successor classes to show application
1407 specific dialog box.
1409 \return path to the dump file
1411 QString PyConsole_Editor::getDumpFileName()
1413 return QFileDialog::getSaveFileName( this,
1414 tr( "GET_DUMP_COMMANDS_FILENAME" ),
1416 QString( "%1 (*.py)" ).arg( tr( "PYTHON_SCRIPTS" ) ) );
1420 \brief Get file name for Log Python trace operation.
1422 This function can be redefined in successor classes to show application
1423 specific dialog box.
1425 \return path to the log file
1427 QString PyConsole_Editor::getLogFileName()
1429 return QFileDialog::getSaveFileName( this,
1430 tr( "GET_PYTHON_TRACE_FILENAME" ),
1432 QString( "%1 (*.log *.txt)" ).arg( tr( "LOG_FILES" ) ) );
1436 \brief Start python trace logging
1437 \param fileName the path to the log file
1438 \return \c true if operation succeeded or \c false otherwise
1439 (for example, if file is not writeable)
1442 bool PyConsole_Editor::startLog( const QString& fileName )
1444 // stop possibly already running logging
1449 if ( !fileName.isEmpty() ) {
1450 QFile file( fileName );
1451 if ( file.open( QFile::WriteOnly ) ) {
1453 myLogFile = fileName;
1461 \brief Start log action slot
1463 void PyConsole_Editor::startLog()
1467 QString fileName = getLogFileName();
1469 if ( fileName.isEmpty() )
1472 if ( startLog( fileName ) )
1475 QMessageBox::warning( this,
1477 tr( "File is not writable" ) );
1482 \brief Stop log action slot
1484 Stops Python trace logging.
1486 void PyConsole_Editor::stopLog()
1488 myLogFile = QString();
1492 \brief Put data to the log file
1494 void PyConsole_Editor::putLog( const QString& s )
1496 if ( !myLogFile.isEmpty() ) {
1497 QFile file( myLogFile );
1498 if ( !file.open( QFile::Append ) )
1501 QTextStream out( &file );
1509 \brief Handle properly multi-line pasting. Qt documentation recommends overriding this function.
1510 If the pasted text doesn't contain a line return, no special treatment is done.
1513 void PyConsole_Editor::insertFromMimeData(const QMimeData* source)
1515 if ( myMultiLinePaste )
1518 if ( source->hasText() ) {
1519 QString s = source->text();
1520 if ( s.contains( "\n" ) )
1521 multilinePaste( s );
1523 QTextEdit::insertFromMimeData( source );
1526 QTextEdit::insertFromMimeData( source );
1531 Start multi-line paste operation
1534 void PyConsole_Editor::multilinePaste( const QString& s )
1536 // Turn on multi line pasting mode
1537 myMultiLinePaste = true;
1539 // Split string data to lines
1541 s2.replace( "\r", "" ); // Windows string format converted to Unix style
1542 QStringList lst = s2.split( QChar('\n'), QString::KeepEmptyParts );
1544 // Perform the proper paste operation for the first line to handle the case where
1545 // something was already there
1547 source.setText( lst[0] );
1548 QTextEdit::insertFromMimeData( &source );
1550 // Prepare what will have to be executed after the first line
1551 myMultiLineContent.clear();
1552 for ( int i = 1; i < lst.size(); ++i )
1553 myMultiLineContent.enqueue( lst[i] );
1555 // Trigger the execution of the first (mixed) line
1558 // See customEvent() and multiLineProcessNext() for the rest of the handling.
1562 \brief Process the next line in the queue of multi-line paste operation; called
1563 from customEvent() function
1566 void PyConsole_Editor::multiLineProcessNextLine()
1568 // not in multi-line paste mode
1569 if ( !myMultiLinePaste || myMultiLineContent.isEmpty() )
1572 QString line = myMultiLineContent.dequeue();
1573 if ( myMultiLineContent.empty() )
1575 // this isa last line in the queue, just paste it
1576 addText( line, false, false );
1577 myMultiLinePaste = false;
1581 // paste the line and simulate a <RETURN> key stroke
1582 addText( line, false, false );
1588 \brief Clear results of completion
1590 void PyConsole_Editor::clearCompletion()
1592 // delete completion text if present
1594 // remove completion display
1596 // remove trailing line return:
1597 QTextCursor tc( textCursor() );
1598 tc.setPosition( document()->characterCount()-1 );
1599 setTextCursor( tc );
1600 textCursor().deletePreviousChar();
1601 // TODO: before wait for any <Tab> event to be completed
1607 \brief Format completion results - this is where we should create 3 columns etc ...
1608 \param matches list of possible completions
1609 \return result string
1611 QString PyConsole_Editor::formatCompletion( const QStringList& matches ) const
1613 static const int MAX_COMPLETIONS = 70;
1616 int sz = matches.size();
1618 if ( sz > MAX_COMPLETIONS )
1619 result.append( QString( "[%1]" ).arg( tr( "TOO_MANY_MATCHES" ) ) );
1621 for ( int i = 0; i < qMin( sz, MAX_COMPLETIONS); ++i )
1622 result.append( matches[i] );
1624 return result.join( "\n" );
1628 \brief Format the doc string in HTML format with the first line in bold blue
1629 \param doc initial doc string
1632 QString PyConsole_Editor::formatDocHTML( const QString& doc ) const
1634 static const char* templ = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" " \
1635 "\"http://www.w3.org/TR/REC-html40/strict.dtd\">\n" \
1636 "<html><head><meta name=\"qrichtext\" content=\"1\" /> " \
1637 "<style type=\"text/css\">\np, li { white-space: pre-wrap; }\n</style> " \
1638 "</head><body style=\" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;\">\n" \
1639 "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"> " \
1640 "<span style=\" font-weight:600; color:#0000ff;\">%1</span></p> " \
1641 "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">%2</p> " \
1644 QString fst, rest("");
1646 // Extract first line of doc
1647 int idx = doc.indexOf( "\n" );
1649 fst = doc.left( idx );
1650 rest = doc.mid( idx+1 );
1656 fst = fst.replace( "\n", " " );
1657 rest = rest.replace(" \n", " " );
1658 return QString( templ ).arg( fst ).arg( rest );
1662 \fn void PyConsole_Editor::updateDoc( const QString& doc);
1663 \brief Signal emitted by the editor widget when the doc string should be updated.
1664 \param doc a HTML block with the formatted doc string.
1665 \todo currently this signal is left uncaught.
1669 \brief Extract the common leading part of all strings in matches.
1673 QString PyConsole_Editor::extractCommon( const QStringList& matches ) const
1675 QString result = "";
1677 if ( matches.size() > 1 ) {
1680 while ( ok && l+1 < matches[0].size() ) {
1681 QString match = matches[0].left( l+1 );
1682 for ( int j = 1; j < matches.size() && ok; j++ )
1683 ok = matches[j].startsWith( match );
1687 result = matches[0].left( l );
1694 \brief Useful method to get banner from Python interpreter
1697 QString PyConsole_Editor::banner() const
1699 return myInterp->getBanner().c_str();