1 // Copyright (C) 2007-2021 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_Interp.h"
92 #include "PyConsole_Editor.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>
113 //VSR: uncomment below macro to support unicode text properly in SALOME
114 // current commented out due to regressions
115 //#define PAL22528_UNICODE
119 QString fromUtf8( const char* txt )
121 #ifdef PAL22528_UNICODE
122 return QString::fromUtf8( txt );
124 return QString( txt );
129 static QString READY_PROMPT = ">>> ";
130 static QString DOTS_PROMPT = "... ";
132 void PyConsole_CallbackStdout( void* data, char* c )
134 if(!((PyConsole_Editor*)data)->isSuppressOutput()) {
135 PyConsole_Editor* e = (PyConsole_Editor*)data;
136 e->putLog( fromUtf8(c) );
137 QApplication::postEvent( e, new PyConsole_PrintEvent( fromUtf8(c), false ) );
141 void PyConsole_CallbackStderr( void* data, char* c )
143 if(!((PyConsole_Editor*)data)->isSuppressOutput()) {
144 PyConsole_Editor* e = (PyConsole_Editor*)data;
145 e->putLog( fromUtf8(c) );
146 QApplication::postEvent( e, new PyConsole_PrintEvent( fromUtf8(c), true ) );
151 \brief Default constructor.
153 Creates python editor window.
154 \param theParent parent widget
156 PyConsole_Editor::PyConsole_Editor( QWidget* parent )
157 : QTextEdit( parent )
159 PyConsole_Interp *interp(new PyConsole_Interp);
160 interp->initialize();
168 Creates python editor window.
169 \param parent parent widget
170 \param interp python interper
172 PyConsole_Editor::PyConsole_Editor( QWidget* parent,
173 PyConsole_Interp* interp )
174 : QTextEdit( parent )
176 myInterp.takeRef(interp);
181 void PyConsole_Editor::init()
187 myIsSuppressOutput = false;
188 myMultiLinePaste = false;
189 myAutoCompletion = false;
191 myComplCursorPos = -1;
193 setFont( QFont( "Courier", 11 ) ); // default font
194 setUndoRedoEnabled( false );
196 myPrompt = READY_PROMPT;
197 setLineWrapMode( QTextEdit::WidgetWidth );
198 setWordWrapMode( QTextOption::WrapAnywhere );
199 setAcceptRichText( false );
201 // set callbacks to interpeter
202 myInterp->setvoutcb( PyConsole_CallbackStdout, this );
203 myInterp->setverrcb( PyConsole_CallbackStderr, this );
205 if ( isShowBanner() )
207 // clear command buffer
208 myCommandBuffer.truncate(0);
209 // unset read-only state
210 setReadOnly( false );
211 // unset history browsing mode
216 viewport()->unsetCursor();
222 PyConsole_Editor::~PyConsole_Editor()
227 \brief Get Python interpreter
229 PyConsole_Interp *PyConsole_Editor::getInterp() const
231 return myInterp.iAmATrollConstCast();
235 \brief Get synchronous mode flag value.
238 \return \c true if python console works in synchronous mode
240 bool PyConsole_Editor::isSync() const
246 \brief Set synchronous mode flag value.
248 In synhronous mode the Python commands are executed in the GUI thread
249 and the GUI is blocked until the command is finished. In the asynchronous
250 mode each Python command is executed in the separate thread that does not
251 block the main GUI loop.
253 \param on synhronous mode flag
255 void PyConsole_Editor::setIsSync( const bool on )
261 \brief Get suppress output flag value.
263 \sa setIsSuppressOutput()
264 \return \c true if python console output is suppressed.
266 bool PyConsole_Editor::isSuppressOutput() const
268 return myIsSuppressOutput;
272 \brief Set suppress output flag value.
274 In case if suppress output flag is \c true, the python
275 console output suppressed.
277 \param on suppress output flag
279 void PyConsole_Editor::setIsSuppressOutput( const bool on )
281 myIsSuppressOutput = on;
285 \brief Get 'show banner' flag value.
287 \sa setIsShowBanner()
288 \return \c true if python console shows banner
290 bool PyConsole_Editor::isShowBanner() const
296 \brief Set 'show banner' flag value.
298 The banner is shown in the top of the python console window.
301 \param on 'show banner' flag
303 void PyConsole_Editor::setIsShowBanner( const bool on )
305 if ( myShowBanner != on ) {
312 \brief Switch on/off commands auto-completion feature
315 void PyConsole_Editor::setAutoCompletion( bool on )
317 myAutoCompletion = on;
318 document()->setUndoRedoEnabled( myAutoCompletion );
322 \brief Returns \c true if auto-completion feature is switched on
323 or \c false otherwise
324 \sa setAutoCompletion()
326 bool PyConsole_Editor::autoCompletion() const
328 return myAutoCompletion;
332 \brief Check if trace logging is switched on.
334 \sa startLog(), stopLog()
335 \return \c true if trace logging is switched on
337 bool PyConsole_Editor::isLogging() const
339 return !myLogFile.isEmpty();
343 \brief Get size hint for the Python console window
344 \return size hint value
346 QSize PyConsole_Editor::sizeHint() const
348 QFontMetrics fm( font() );
349 int nbLines = ( isShowBanner() ? banner().split("\n").count() : 0 ) + 1;
350 QSize s(100, fm.lineSpacing()*nbLines);
355 \brief Put the string \a str to the python editor.
356 \param str string to be put in the command line of the editor
357 \param newBlock if \c true, then the string is printed on a new line
358 \param isError if \c true, the text is printed in dark red
360 void PyConsole_Editor::addText( const QString& str,
364 QTextCursor aCursor = textCursor();
367 moveCursor( QTextCursor::End );
369 aCursor.insertBlock();
371 cf.setForeground( QBrush( Qt::red ) );
373 cf.setForeground( QBrush( Qt::black ) );
374 aCursor.insertText( str, cf );
375 moveCursor( QTextCursor::End );
376 ensureCursorVisible();
380 \brief Convenient method for executing a Python command,
381 as if the user typed it manually.
382 \param command python command to be executed
384 !!! WARNING: doesn't work properly with multi-line commands. !!!
386 void PyConsole_Editor::exec( const QString& command )
388 if ( isReadOnly() ) {
389 // some interactive command is being executed in this editor...
390 // shedule the command to the queue
391 myQueue.push_back( command );
396 moveCursor( QTextCursor::End );
397 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
398 textCursor().removeSelectedText();
400 // set "ready" prompt
401 myPrompt = READY_PROMPT;
403 // clear command buffer
404 myCommandBuffer.truncate( 0 );
406 // unset history browsing mode
409 // print command line by line
410 QString cmd = command;
411 if ( !cmd.endsWith( "\n" ) ) cmd += "\n";
412 QStringList lines = command.split( "\n" );
413 for ( int i = 0; i < lines.size(); i++ ) {
414 if ( !lines[i].trimmed().isEmpty() )
415 myHistory.push_back( lines[i] );
416 addText( ( i == 0 ? READY_PROMPT : DOTS_PROMPT ) + lines[i], i != 0 );
417 putLog( QString( "%1%2\n" ).arg( i == 0 ? READY_PROMPT : DOTS_PROMPT ).arg( lines[i] ) );
423 // set read-only mode
427 setCursor( Qt::BusyCursor );
429 // post a request to execute Python command;
430 // editor will be informed via a custom event that execution has been completed
431 PyInterp_Dispatcher::Get()->Exec( createCmdRequest( cmd ) );
435 \brief Create request to the python dispatcher for the command execution.
436 \param command python command to be executed
438 PyInterp_Request* PyConsole_Editor::createCmdRequest( const QString& command )
440 return new PyConsole_ExecCommand( myInterp, command, this, isSync() );
444 \brief Create the Python request that will be posted to the interpreter to
446 \param input line entered by the user at the time <TAB> was pressed
447 \return completion command
448 \sa CompletionCommand
450 PyInterp_Request* PyConsole_Editor::createTabRequest( const QString& input )
453 static QStringList separators;
454 if ( separators.isEmpty() ) {
455 separators << " " << "(" << "[" << "+" << "-" << "*" << "/" << ";" << "^" << "=";
458 // parse input to extract on what part the dir() has to be executed
460 QString input2( input );
462 // split up to the last syntaxical separator
464 foreach ( QString separator, separators ) {
465 int j = input2.lastIndexOf( separator );
470 input2 = input.mid( lastSp + 1 );
472 // detect a qualified name (with a point)
473 int lastPt = input2.lastIndexOf( "." );
475 if ( lastPt != -1 ) {
476 // split the 2 surrounding parts of the qualified name
477 myComplBeforePoint = input2.left( lastPt );
478 myComplAfterPoint = input2.mid( lastPt+1 );
481 // no point found - do a global matching
482 // (the following will call dir() with an empty string)
483 myComplAfterPoint = input2;
484 myComplBeforePoint = "";
487 return new PyConsole_CompletionCommand( myInterp, myComplBeforePoint,
488 myComplAfterPoint, this, isSync() );
492 \brief Execute command in the python interpreter
493 and wait until it is finished.
495 \param command python command to be executed
497 void PyConsole_Editor::execAndWait( const QString& command )
503 // create new event loop
504 bool sync = isSync();
506 myEventLoop = new QEventLoop( this );
515 // delete event loop after command is processed
522 \brief Process <Enter> key press event.
524 Execute the command entered by the user.
526 void PyConsole_Editor::handleReturn()
528 // Position cursor at the end
529 QTextCursor aCursor = textCursor();
530 aCursor.movePosition( QTextCursor::End );
531 setTextCursor( aCursor );
534 QTextBlock par = document()->end().previous();
535 if ( !par.isValid() ) return;
538 QString cmd = par.text().remove( 0, promptSize() );
540 // extend the command buffer with the current command
541 myCommandBuffer.append( cmd );
543 // add command to the history
544 if ( !cmd.trimmed().isEmpty() )
545 myHistory.push_back( cmd );
546 putLog( QString( "%1%2\n" ).arg( myPrompt ).arg( cmd ) );
551 // set read-only mode
555 setCursor( Qt::BusyCursor );
557 // post a request to execute Python command;
558 // editor will be informed via a custom event that execution has been completed
559 PyInterp_Dispatcher::Get()->Exec( createCmdRequest( myCommandBuffer ) );
563 \brief Process <Tab> key press event.
565 Perform auto-completion of the currently entered command, if this feature is enabled
567 void PyConsole_Editor::handleTab()
569 if ( !autoCompletion() )
570 return; // auto-completion feature is disabled
573 return; // already in tab mode
575 QTextCursor aCursor = textCursor();
577 // move cursor to the end of input
578 aCursor.movePosition( QTextCursor::End );
579 setTextCursor( aCursor );
581 // save cursor position if needed
582 if ( myComplCursorPos == -1 )
583 myComplCursorPos = textCursor().position();
586 QTextBlock par = document()->end().previous();
587 if ( !par.isValid() ) return; // empty line
589 // switch to completion mode
592 // get currently entered command
593 QString cmd = par.text().mid( promptSize() );
595 // post completion request
596 // editor will be informed that completion has been done via a custom event
597 PyInterp_Dispatcher::Get()->Exec( createTabRequest( cmd ) );
601 \brief Process <Ctrl><Tab> key press event.
603 Undoe last auto-completion
605 void PyConsole_Editor::handleBackTab()
607 if ( !autoCompletion() )
608 return; // auto-completion feature is disabled
610 QTextCursor aCursor = textCursor();
612 if ( myComplCursorPos == -1 )
613 return; // invalid cursor position
615 // ensure cursor is at the end of command line
616 aCursor.setPosition( myComplCursorPos );
617 aCursor.movePosition( QTextCursor::EndOfLine );
618 //setCursor( aCursor );
620 // delete last completed text
621 int i = aCursor.position() - myComplCursorPos;
622 aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, i );
623 aCursor.removeSelectedText();
624 myComplCursorPos = -1;
628 \brief Process drop event.
631 \param event drop event
633 void PyConsole_Editor::dropEvent( QDropEvent* event )
635 // get the initial drop position
636 QPoint pos = event->pos();
637 QTextCursor aCursor = cursorForPosition( event->pos() );
639 // if the position is not in the last line move it to the end of the command line
640 if ( aCursor.position() < document()->end().previous().position() + promptSize() ) {
641 moveCursor( QTextCursor::End );
642 pos = cursorRect().center();
645 // create new drop event and use it instead of the original
647 event->possibleActions(),
649 event->mouseButtons(),
650 event->keyboardModifiers(),
652 QTextEdit::dropEvent( &de );
654 // accept the original event
655 event->acceptProposedAction();
659 \brief Process mouse press event
661 Clear the completion when any mouse button is pressed.
663 \param e mouse press event
665 void PyConsole_Editor::mousePressEvent( QMouseEvent* event )
667 if ( autoCompletion() ) {
669 myComplCursorPos = -1;
671 QTextEdit::mousePressEvent( event );
675 \brief Process mouse button release event.
677 Left mouse button: copy selection to the clipboard.
678 Middle mouse button: paste clipboard's contents.
680 \param event mouse event
682 void PyConsole_Editor::mouseReleaseEvent( QMouseEvent* event )
684 if ( event->button() == Qt::LeftButton ) {
685 QTextEdit::mouseReleaseEvent( event );
687 else if ( event->button() == Qt::MidButton ) {
688 QTextCursor aCursor = cursorForPosition( event->pos() );
689 // if the position is not in the last line move it to the end of the command line
690 if ( aCursor.position() < document()->end().previous().position() + promptSize() ) {
691 moveCursor( QTextCursor::End );
694 setTextCursor( aCursor );
696 const QMimeData* md = QApplication::clipboard()->mimeData( QApplication::clipboard()->supportsSelection() ?
697 QClipboard::Selection : QClipboard::Clipboard );
699 insertFromMimeData( md );
702 QTextEdit::mouseReleaseEvent( event );
707 \brief Check if the string is command.
709 Return \c true if the string \a str is likely to be the command
710 (i.e. it is started from the '>>>' or '...').
711 \param str string to be checked
713 bool PyConsole_Editor::isCommand( const QString& str ) const
715 return str.startsWith( READY_PROMPT ) || str.startsWith( DOTS_PROMPT );
719 \brief Handle keyboard event.
721 Implement navigation, history browsing, copy/paste and other common
724 \param event keyboard event
726 void PyConsole_Editor::keyPressEvent( QKeyEvent* event )
728 // get cursor position
729 QTextCursor aCursor = textCursor();
730 int curLine = aCursor.blockNumber();
731 int curCol = aCursor.columnNumber();
733 // get last edited line
734 int endLine = document()->blockCount()-1;
736 // get pressed key code
737 int aKey = event->key();
739 // check if <Ctrl> is pressed
740 bool ctrlPressed = event->modifiers() & Qt::ControlModifier;
741 // check if <Shift> is pressed
742 bool shftPressed = event->modifiers() & Qt::ShiftModifier;
744 if ( autoCompletion() ) {
745 // auto-completion support
746 if ( aKey == Qt::Key_Tab && !shftPressed ) {
748 if ( !ctrlPressed ) {
758 // If <Ctrl> is not pressed (or if something else is pressed with <Ctrl>),
759 // or if <Ctrl> is not pressed alone, we have to clear completion
760 if ( !ctrlPressed || ( ctrlPressed && aKey != Qt::Key_Control ) ) {
762 myComplCursorPos = -1;
765 // Discard <Ctrl> pressed alone:
766 if ( aKey == Qt::Key_Control )
770 if ( aKey == Qt::Key_Escape || ( ctrlPressed && aKey == -1 ) ) {
771 // process <Ctrl>+<Break> key-binding and <Escape> key: clear current command
772 myCommandBuffer.truncate( 0 );
773 myPrompt = READY_PROMPT;
774 addText( myPrompt, true );
775 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
778 else if ( ctrlPressed && aKey == Qt::Key_C ) {
779 // process <Ctrl>+<C> key-binding : copy
783 else if ( ctrlPressed && aKey == Qt::Key_X ) {
784 // process <Ctrl>+<X> key-binding : cut
788 else if ( ctrlPressed && aKey == Qt::Key_V ) {
789 // process <Ctrl>+<V> key-binding : paste
794 // check for printed key
795 // #### aKey = ( aKey < Qt::Key_Space || aKey > Qt::Key_ydiaeresis ) ? aKey : 0;
797 aKey = !(QChar(aKey).isPrint()) ? aKey : 0;
801 // any printed key: just print it
803 if ( curLine < endLine || curCol < promptSize() ) {
804 moveCursor( QTextCursor::End );
806 QTextEdit::keyPressEvent( event );
811 // <Enter> key: process the current command
817 // <Up> arrow key: process as follows:
818 // - without <Ctrl>, <Shift> modifiers: previous command in history
819 // - with <Ctrl> modifier key pressed: move cursor one row up without selection
820 // - with <Shift> modifier key pressed: move cursor one row up with selection
821 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one row up
823 if ( ctrlPressed && shftPressed ) {
824 int value = verticalScrollBar()->value();
825 int spacing = fontMetrics().lineSpacing();
826 verticalScrollBar()->setValue( value > spacing ? value-spacing : 0 );
828 else if ( shftPressed || ctrlPressed ) {
830 moveCursor( QTextCursor::Up,
831 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
834 if ( myCmdInHistory < 0 && myHistory.count() > 0 ) {
835 // set history browsing mode
836 myCmdInHistory = myHistory.count();
837 // remember current command
838 QTextBlock par = document()->end().previous();
839 myCurrentCommand = par.text().remove( 0, promptSize() );
841 if ( myCmdInHistory > 0 ) {
843 // get previous command in the history
844 QString previousCommand = myHistory.at( myCmdInHistory );
845 // print previous command
846 moveCursor( QTextCursor::End );
847 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
848 textCursor().removeSelectedText();
849 addText( myPrompt + previousCommand );
850 // move cursor to the end
851 moveCursor( QTextCursor::End );
857 // <Down> arrow key: process as follows:
858 // - without <Ctrl>, <Shift> modifiers: next command in history
859 // - with <Ctrl> modifier key pressed: move cursor one row down without selection
860 // - with <Shift> modifier key pressed: move cursor one row down with selection
861 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one row down
863 if ( ctrlPressed && shftPressed ) {
864 int value = verticalScrollBar()->value();
865 int maxval = verticalScrollBar()->maximum();
866 int spacing = fontMetrics().lineSpacing();
867 verticalScrollBar()->setValue( value+spacing < maxval ? value+spacing : maxval );
869 else if ( shftPressed || ctrlPressed) {
870 if ( curLine < endLine )
871 moveCursor( QTextCursor::Down,
872 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
875 if ( myCmdInHistory >= 0 ) {
876 // get next command in the history
879 if ( myCmdInHistory < myHistory.count() ) {
880 // next command in history
881 nextCommand = myHistory.at( myCmdInHistory );
884 // end of history is reached
885 // last printed command
886 nextCommand = myCurrentCommand;
887 // unset history browsing mode
890 // print next or current command
891 moveCursor( QTextCursor::End );
892 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
893 textCursor().removeSelectedText();
894 addText( myPrompt + nextCommand );
895 // move cursor to the end
896 moveCursor( QTextCursor::End );
902 // <Left> arrow key: process as follows:
903 // - without <Ctrl>, <Shift> modifiers: move one symbol left (taking into account prompt)
904 // - with <Ctrl> modifier key pressed: move one word left (taking into account prompt)
905 // - with <Shift> modifier key pressed: move one symbol left with selection
906 // - with <Ctrl>+<Shift> modifier keys pressed: move one word left with selection
908 QString txt = textCursor().block().text();
909 if ( !shftPressed && isCommand( txt ) && curCol <= promptSize() ) {
910 moveCursor( QTextCursor::Up );
911 moveCursor( QTextCursor::EndOfBlock );
914 QTextEdit::keyPressEvent( event );
919 // <Right> arrow key: process as follows:
920 // - without <Ctrl>, <Shift> modifiers: move one symbol right (taking into account prompt)
921 // - with <Ctrl> modifier key pressed: move one word right (taking into account prompt)
922 // - with <Shift> modifier key pressed: move one symbol right with selection
923 // - with <Ctrl>+<Shift> modifier keys pressed: move one word right with selection
925 QString txt = textCursor().block().text();
926 if ( !shftPressed ) {
927 if ( curCol < txt.length() ) {
928 if ( isCommand( txt ) && curCol < promptSize() ) {
929 aCursor.setPosition( aCursor.block().position() + promptSize() );
930 setTextCursor( aCursor );
935 if ( curLine < endLine && isCommand( textCursor().block().next().text() ) ) {
936 aCursor.setPosition( aCursor.position() + promptSize()+1 );
937 setTextCursor( aCursor );
938 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
943 QTextEdit::keyPressEvent( event );
947 // <PageUp> key: process as follows:
948 // - without <Ctrl>, <Shift> modifiers: first command in history
949 // - with <Ctrl> modifier key pressed: move cursor one page up without selection
950 // - with <Shift> modifier key pressed: move cursor one page up with selection
951 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one page up
953 if ( ctrlPressed && shftPressed ) {
954 verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub);
956 else if ( shftPressed || ctrlPressed ) {
958 qreal lastY = cursorRect( aCursor ).top();
960 // move using movePosition to keep the cursor's x
962 qreal y = cursorRect( aCursor ).top();
963 distance += qAbs( y - lastY );
965 moved = aCursor.movePosition( QTextCursor::Up,
966 shftPressed ? QTextCursor::KeepAnchor :
967 QTextCursor::MoveAnchor );
968 } while ( moved && distance < viewport()->height() );
970 aCursor.movePosition( QTextCursor::Down,
971 shftPressed ? QTextCursor::KeepAnchor :
972 QTextCursor::MoveAnchor );
973 verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub );
975 setTextCursor( aCursor );
978 if ( myCmdInHistory < 0 && myHistory.count() > 0 ) {
979 // set history browsing mode
980 myCmdInHistory = myHistory.count();
981 // remember current command
982 QTextBlock par = document()->end().previous();
983 myCurrentCommand = par.text().remove( 0, promptSize() );
985 if ( myCmdInHistory > 0 ) {
987 // get very first command in the history
988 QString firstCommand = myHistory.at( myCmdInHistory );
989 // print first command
990 moveCursor( QTextCursor::End );
991 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
992 textCursor().removeSelectedText();
993 addText( myPrompt + firstCommand );
994 // move cursor to the end
995 moveCursor( QTextCursor::End );
1000 case Qt::Key_PageDown:
1001 // <PageDown> key: process as follows:
1002 // - without <Ctrl>, <Shift> modifiers: last command in history
1003 // - with <Ctrl> modifier key pressed: move cursor one page down without selection
1004 // - with <Shift> modifier key pressed: move cursor one page down with selection
1005 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one page down
1007 if ( ctrlPressed && shftPressed ) {
1008 verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd);
1010 else if ( shftPressed || ctrlPressed ) {
1012 qreal lastY = cursorRect( aCursor ).top();
1014 // move using movePosition to keep the cursor's x
1016 qreal y = cursorRect( aCursor ).top();
1017 distance += qAbs( y - lastY );
1019 moved = aCursor.movePosition( QTextCursor::Down,
1020 shftPressed ? QTextCursor::KeepAnchor :
1021 QTextCursor::MoveAnchor );
1022 } while ( moved && distance < viewport()->height() );
1024 aCursor.movePosition( QTextCursor::Up,
1025 shftPressed ? QTextCursor::KeepAnchor :
1026 QTextCursor::MoveAnchor );
1027 verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub );
1029 setTextCursor( aCursor );
1032 if ( myCmdInHistory >= 0 ) {
1033 // unset history browsing mode
1034 myCmdInHistory = -1;
1035 // print current command
1036 moveCursor( QTextCursor::End );
1037 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
1038 textCursor().removeSelectedText();
1039 addText( myPrompt + myCurrentCommand );
1040 // move cursor to the end
1041 moveCursor( QTextCursor::End );
1047 // <Home> key: process as follows:
1048 // - without <Ctrl>, <Shift> modifiers: move cursor to the beginning of the current line without selection
1049 // - with <Ctrl> modifier key pressed: move cursor to the very first symbol without selection
1050 // - with <Shift> modifier key pressed: move cursor to the beginning of the current line with selection
1051 // - with <Ctrl>+<Shift> modifier keys pressed: move cursor to the very first symbol with selection
1053 if ( ctrlPressed ) {
1054 moveCursor( QTextCursor::Start,
1055 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1058 QString txt = textCursor().block().text();
1059 if ( isCommand( txt ) ) {
1060 if ( shftPressed ) {
1061 if ( curCol > promptSize() ) {
1062 aCursor.movePosition( QTextCursor::StartOfLine, QTextCursor::KeepAnchor );
1063 aCursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, promptSize() );
1067 aCursor.movePosition( QTextCursor::StartOfLine );
1068 aCursor.movePosition( QTextCursor::Right, QTextCursor::MoveAnchor, promptSize() );
1070 setTextCursor( aCursor );
1073 moveCursor( QTextCursor::StartOfBlock,
1074 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1076 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1081 // <End> key: process as follows:
1082 // - without <Ctrl>, <Shift> modifiers: move cursor to the end of the current line without selection
1083 // - with <Ctrl> modifier key pressed: move cursor to the very last symbol without selection
1084 // - with <Shift> modifier key pressed: move cursor to the end of the current line with selection
1085 // - with <Ctrl>+<Shift> modifier keys pressed: move cursor to the very last symbol with selection
1087 moveCursor( ctrlPressed ? QTextCursor::End : QTextCursor::EndOfBlock,
1088 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1091 case Qt::Key_Backspace :
1092 // <Backspace> key: process as follows
1093 // - without any modifiers : delete symbol before the cursor / selection (taking into account prompt)
1094 // - with <Shift> modifier key pressed: delete previous word
1095 // - with <Ctrl> modifier key pressed: delete text from the cursor to the line beginning
1096 // works only for last (command) line
1098 if ( aCursor.hasSelection() ) {
1101 else if ( aCursor.position() > document()->end().previous().position() + promptSize() ) {
1102 if ( shftPressed ) {
1103 moveCursor( QTextCursor::PreviousWord, QTextCursor::KeepAnchor );
1104 textCursor().removeSelectedText();
1106 else if ( ctrlPressed ) {
1107 aCursor.setPosition( document()->end().previous().position() + promptSize(),
1108 QTextCursor::KeepAnchor );
1109 setTextCursor( aCursor );
1110 textCursor().removeSelectedText();
1113 QTextEdit::keyPressEvent( event );
1117 aCursor.setPosition( document()->end().previous().position() + promptSize() );
1118 setTextCursor( aCursor );
1119 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1123 case Qt::Key_Delete :
1124 // <Delete> key: process as follows
1125 // - without any modifiers : delete symbol after the cursor / selection (taking into account prompt)
1126 // - with <Shift> modifier key pressed: delete next word
1127 // - with <Ctrl> modifier key pressed: delete text from the cursor to the end of line
1128 // works only for last (command) line
1130 if ( aCursor.hasSelection() ) {
1133 else if ( aCursor.position() > document()->end().previous().position() + promptSize()-1 ) {
1134 if ( shftPressed ) {
1135 moveCursor( QTextCursor::NextWord, QTextCursor::KeepAnchor );
1136 textCursor().removeSelectedText();
1138 else if ( ctrlPressed ) {
1139 moveCursor( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
1140 textCursor().removeSelectedText();
1143 QTextEdit::keyPressEvent( event );
1147 aCursor.setPosition( document()->end().previous().position() + promptSize() );
1148 setTextCursor( aCursor );
1149 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1153 case Qt::Key_Insert :
1154 // <Insert> key: process as follows
1155 // - with <Ctrl> modifier key pressed: copy()
1156 // - with <Shift> modifier key pressed: paste() to the command line
1158 if ( ctrlPressed ) {
1161 else if ( shftPressed ) {
1165 QTextEdit::keyPressEvent( event );
1174 \brief Handle notification event coming from Python dispatcher.
1175 \param event notification event
1177 void PyConsole_Editor::customEvent( QEvent* event )
1179 switch( (int) event->type() )
1181 case PyConsole_PrintEvent::EVENT_ID:
1183 PyConsole_PrintEvent* pe = (PyConsole_PrintEvent*)event;
1184 addText( pe->text(), false, pe->isError() );
1187 case PyConsole_CompletionEvent::EVENT_ID:
1189 PyConsole_CompletionEvent* ce = (PyConsole_CompletionEvent*)event;
1190 bool status = ce->status();
1191 QStringList matches = ce->matches();
1192 QString doc = ce->doc();
1195 // completion was successful
1196 QTextCursor aCursor = textCursor();
1198 if ( matches.isEmpty() ) {
1199 // completion successful but there are no matches.
1201 myComplCursorPos = -1;
1205 if ( matches.size() == 1 ) {
1206 // there's only one match - complete directly and update doc string window
1207 aCursor.insertText( matches[0].mid( myComplAfterPoint.size() ) );
1209 if ( doc.isEmpty() )
1210 emit updateDoc( formatDocHTML( QString( "(%1)\n" ).arg( tr( "NO_DOC_AVAILABLE" ) ) ) );
1212 emit updateDoc( formatDocHTML( doc ) );
1215 // there are several matches
1217 // detect if there is a common base to all available completion
1218 // in this case append this base to the text
1219 QString base = extractCommon( matches );
1220 aCursor.insertText( base.mid( myComplAfterPoint.size() ) );
1222 // if this happens to match exactly the first completion
1224 if ( base == matches[0] )
1225 emit updateDoc( formatDocHTML( doc ) );
1227 // print all matching completion in a "undo-able" block
1228 int cursorPos = aCursor.position();
1229 aCursor.insertBlock();
1230 aCursor.beginEditBlock();
1232 // insert all matches
1234 cf.setForeground( QBrush( Qt::darkGreen ) );
1235 aCursor.setCharFormat( cf );
1236 aCursor.insertText( formatCompletion( matches ) );
1237 aCursor.endEditBlock();
1239 // position cursor where it was before inserting the completion list
1240 aCursor.setPosition( cursorPos );
1241 setTextCursor( aCursor );
1245 // completion failed
1247 myComplCursorPos = -1;
1251 case PyInterp_Event::ES_OK:
1252 case PyInterp_Event::ES_ERROR:
1254 // clear command buffer
1255 myCommandBuffer.truncate( 0 );
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 );
1263 // set "ready" prompt
1264 myPrompt = READY_PROMPT;
1265 addText( myPrompt );
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();
1275 case PyInterp_Event::ES_INCOMPLETE:
1277 // extend command buffer (multi-line command)
1278 myCommandBuffer.append( "\n" );
1279 // add caret return line if necessary
1280 QTextBlock par = document()->end().previous();
1281 QString txt = par.text();
1282 txt.truncate( txt.length() - 1 );
1283 // IPAL19397 : addText moved to handleReturn() method
1284 //if ( !txt.isEmpty() )
1285 // addText( "", true );
1287 myPrompt = DOTS_PROMPT;
1288 addText( myPrompt/*, true*/ ); // IPAL19397
1289 // unset busy cursor
1291 // stop event loop (if running)
1293 myEventLoop->exit();
1294 // if we are in multi_paste_mode, process the next item
1295 multiLineProcessNextLine();
1299 QTextEdit::customEvent( event );
1302 // unset read-only state
1303 setReadOnly( false );
1304 // unset history browsing mode
1305 myCmdInHistory = -1;
1307 if ( (int)event->type() == (int)PyInterp_Event::ES_OK && myQueue.count() > 0 )
1309 // process the next sheduled command from the queue (if there is any)
1310 QString nextcmd = myQueue[0];
1311 myQueue.pop_front();
1317 \brief "Copy" operation.
1319 Reimplemented from Qt.
1320 Warning! In Qt this method is not virtual.
1322 void PyConsole_Editor::cut()
1324 QTextCursor aCursor = textCursor();
1325 if ( aCursor.hasSelection() ) {
1326 QApplication::clipboard()->setText( aCursor.selectedText() );
1327 int startSelection = aCursor.selectionStart();
1328 if ( startSelection < document()->end().previous().position() + promptSize() )
1329 startSelection = document()->end().previous().position() + promptSize();
1330 int endSelection = aCursor.selectionEnd();
1331 if ( endSelection < document()->end().previous().position() + promptSize() )
1332 endSelection = document()->end().previous().position() + promptSize();
1333 aCursor.setPosition( startSelection );
1334 aCursor.setPosition( endSelection, QTextCursor::KeepAnchor );
1335 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1336 setTextCursor( aCursor );
1337 textCursor().removeSelectedText();
1342 \brief "Paste" operation.
1344 Reimplemented from Qt.
1345 Warning! In Qt this method is not virtual.
1347 void PyConsole_Editor::paste()
1349 QTextCursor aCursor = textCursor();
1350 if ( aCursor.hasSelection() ) {
1351 int startSelection = aCursor.selectionStart();
1352 if ( startSelection < document()->end().previous().position() + promptSize() )
1353 startSelection = document()->end().previous().position() + promptSize();
1354 int endSelection = aCursor.selectionEnd();
1355 if ( endSelection < document()->end().previous().position() + promptSize() )
1356 endSelection = document()->end().previous().position() + promptSize();
1357 aCursor.setPosition( startSelection );
1358 aCursor.setPosition( endSelection, QTextCursor::KeepAnchor );
1359 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1360 setTextCursor( aCursor );
1361 textCursor().removeSelectedText();
1363 if ( textCursor().position() < document()->end().previous().position() + promptSize() )
1364 moveCursor( QTextCursor::End );
1369 \brief "Clear" operation.
1371 Reimplemented from Qt.
1372 Warning! In Qt this method is not virtual.
1374 void PyConsole_Editor::clear()
1377 if ( isShowBanner() )
1378 addText( banner() );
1379 myPrompt = READY_PROMPT;
1380 addText( myPrompt );
1384 \brief Dumps recorded Python commands to the file
1385 \param fileName path to the dump file
1386 \return \c true if dump operation succeeded or \c false otherwise
1388 bool PyConsole_Editor::dump( const QString& fileName )
1391 if ( !fileName.isEmpty() ) {
1392 QFile file( fileName );
1393 if ( file.open( QFile::WriteOnly ) ) {
1394 QTextStream out( &file );
1395 for ( int i = 0; i < myHistory.count(); i++ ) {
1396 #if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
1397 out << myHistory[i] << Qt::endl;
1399 out << myHistory[i] << endl;
1410 \brief Dump menu action slot
1412 void PyConsole_Editor::dump()
1416 QString fileName = getDumpFileName();
1418 if ( fileName.isEmpty() )
1421 if ( dump( fileName ) )
1424 QMessageBox::warning( this,
1426 tr( "ERR_FILE_NOT_WRITEABLE" ) );
1431 \brief Get file name for Dump commands operation.
1433 This function can be redefined in successor classes to show application
1434 specific dialog box.
1436 \return path to the dump file
1438 QString PyConsole_Editor::getDumpFileName()
1440 return QFileDialog::getSaveFileName( this,
1441 tr( "GET_DUMP_COMMANDS_FILENAME" ),
1443 QString( "%1 (*.py)" ).arg( tr( "PYTHON_SCRIPTS" ) ) );
1447 \brief Get file name for Log Python trace operation.
1449 This function can be redefined in successor classes to show application
1450 specific dialog box.
1452 \return path to the log file
1454 QString PyConsole_Editor::getLogFileName()
1456 return QFileDialog::getSaveFileName( this,
1457 tr( "GET_PYTHON_TRACE_FILENAME" ),
1459 QString( "%1 (*.log *.txt)" ).arg( tr( "LOG_FILES" ) ) );
1463 \brief Start python trace logging
1464 \param fileName the path to the log file
1465 \return \c true if operation succeeded or \c false otherwise
1466 (for example, if file is not writeable)
1469 bool PyConsole_Editor::startLog( const QString& fileName )
1471 // stop possibly already running logging
1476 if ( !fileName.isEmpty() ) {
1477 QFile file( fileName );
1478 if ( file.open( QFile::WriteOnly ) ) {
1480 myLogFile = fileName;
1488 \brief Start log action slot
1490 void PyConsole_Editor::startLog()
1494 QString fileName = getLogFileName();
1496 if ( fileName.isEmpty() )
1499 if ( startLog( fileName ) )
1502 QMessageBox::warning( this,
1504 tr( "File is not writable" ) );
1509 \brief Stop log action slot
1511 Stops Python trace logging.
1513 void PyConsole_Editor::stopLog()
1515 myLogFile = QString();
1519 \brief Put data to the log file
1521 void PyConsole_Editor::putLog( const QString& s )
1523 if ( !myLogFile.isEmpty() ) {
1524 QFile file( myLogFile );
1525 if ( !file.open( QFile::Append ) )
1528 QTextStream out( &file );
1536 \brief Handle properly multi-line pasting. Qt documentation recommends overriding this function.
1537 If the pasted text doesn't contain a line return, no special treatment is done.
1540 void PyConsole_Editor::insertFromMimeData(const QMimeData* source)
1542 if ( myMultiLinePaste )
1545 if ( source->hasText() ) {
1546 QString s = source->text();
1547 if ( s.contains( "\n" ) )
1548 multilinePaste( s );
1550 QTextEdit::insertFromMimeData( source );
1553 QTextEdit::insertFromMimeData( source );
1558 Start multi-line paste operation
1561 void PyConsole_Editor::multilinePaste( const QString& s )
1563 // Turn on multi line pasting mode
1564 myMultiLinePaste = true;
1566 // Split string data to lines
1568 s2.replace( "\r", "" ); // Windows string format converted to Unix style
1569 #if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
1570 QStringList lst = s2.split( QChar('\n'), Qt::KeepEmptyParts );
1572 QStringList lst = s2.split( QChar('\n'), QString::KeepEmptyParts );
1575 // Perform the proper paste operation for the first line to handle the case where
1576 // something was already there
1578 source.setText( lst[0] );
1579 QTextEdit::insertFromMimeData( &source );
1581 // Prepare what will have to be executed after the first line
1582 myMultiLineContent.clear();
1583 for ( int i = 1; i < lst.size(); ++i )
1584 myMultiLineContent.enqueue( lst[i] );
1586 // Trigger the execution of the first (mixed) line
1589 // See customEvent() and multiLineProcessNext() for the rest of the handling.
1593 \brief Process the next line in the queue of multi-line paste operation; called
1594 from customEvent() function
1597 void PyConsole_Editor::multiLineProcessNextLine()
1599 // not in multi-line paste mode
1600 if ( !myMultiLinePaste || myMultiLineContent.isEmpty() )
1603 QString line = myMultiLineContent.dequeue();
1604 if ( myMultiLineContent.empty() )
1606 // this isa last line in the queue, just paste it
1607 addText( line, false, false );
1608 myMultiLinePaste = false;
1612 // paste the line and simulate a <RETURN> key stroke
1613 addText( line, false, false );
1619 \brief Clear results of completion
1621 void PyConsole_Editor::clearCompletion()
1623 // delete completion text if present
1625 // remove completion display
1627 // remove trailing line return:
1628 QTextCursor tc( textCursor() );
1629 tc.setPosition( document()->characterCount()-1 );
1630 setTextCursor( tc );
1631 textCursor().deletePreviousChar();
1632 // TODO: before wait for any <Tab> event to be completed
1638 \brief Format completion results - this is where we should create 3 columns etc ...
1639 \param matches list of possible completions
1640 \return result string
1642 QString PyConsole_Editor::formatCompletion( const QStringList& matches ) const
1644 static const int MAX_COMPLETIONS = 70;
1647 int sz = matches.size();
1649 if ( sz > MAX_COMPLETIONS )
1650 result.append( QString( "[%1]" ).arg( tr( "TOO_MANY_MATCHES" ) ) );
1652 for ( int i = 0; i < qMin( sz, MAX_COMPLETIONS); ++i )
1653 result.append( matches[i] );
1655 return result.join( "\n" );
1659 \brief Format the doc string in HTML format with the first line in bold blue
1660 \param doc initial doc string
1663 QString PyConsole_Editor::formatDocHTML( const QString& doc ) const
1665 static const char* templ = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" " \
1666 "\"http://www.w3.org/TR/REC-html40/strict.dtd\">\n" \
1667 "<html><head><meta name=\"qrichtext\" content=\"1\" /> " \
1668 "<style type=\"text/css\">\np, li { white-space: pre-wrap; }\n</style> " \
1669 "</head><body style=\" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;\">\n" \
1670 "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"> " \
1671 "<span style=\" font-weight:600; color:#0000ff;\">%1</span></p> " \
1672 "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">%2</p> " \
1675 QString fst, rest("");
1677 // Extract first line of doc
1678 int idx = doc.indexOf( "\n" );
1680 fst = doc.left( idx );
1681 rest = doc.mid( idx+1 );
1687 fst = fst.replace( "\n", " " );
1688 rest = rest.replace(" \n", " " );
1689 return QString( templ ).arg( fst ).arg( rest );
1693 \fn void PyConsole_Editor::updateDoc( const QString& doc);
1694 \brief Signal emitted by the editor widget when the doc string should be updated.
1695 \param doc a HTML block with the formatted doc string.
1696 \todo currently this signal is left uncaught.
1700 \brief Extract the common leading part of all strings in matches.
1704 QString PyConsole_Editor::extractCommon( const QStringList& matches ) const
1706 QString result = "";
1708 if ( matches.size() > 1 ) {
1711 while ( ok && l+1 < matches[0].size() ) {
1712 QString match = matches[0].left( l+1 );
1713 for ( int j = 1; j < matches.size() && ok; j++ )
1714 ok = matches[j].startsWith( match );
1718 result = matches[0].left( l );
1725 \brief Useful method to get banner from Python interpreter
1728 QString PyConsole_Editor::banner() const
1730 return myInterp->getBanner().c_str();