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 PyConsole_CallbackStdout( void* data, char* c )
133 if(!((PyConsole_Editor*)data)->isSuppressOutput()) {
134 PyConsole_Editor* e = (PyConsole_Editor*)data;
135 e->putLog( fromUtf8(c) );
136 QApplication::postEvent( e, new PyConsole_PrintEvent( fromUtf8(c), false ) );
140 void PyConsole_CallbackStderr( void* data, char* c )
142 if(!((PyConsole_Editor*)data)->isSuppressOutput()) {
143 PyConsole_Editor* e = (PyConsole_Editor*)data;
144 e->putLog( fromUtf8(c) );
145 QApplication::postEvent( e, new PyConsole_PrintEvent( fromUtf8(c), true ) );
150 \brief Default constructor.
152 Creates python editor window.
153 \param theParent parent widget
155 PyConsole_Editor::PyConsole_Editor( QWidget* parent )
156 : QTextEdit( parent )
158 PyConsole_Interp *interp(new PyConsole_Interp);
159 interp->initialize();
167 Creates python editor window.
168 \param parent parent widget
169 \param interp python interper
171 PyConsole_Editor::PyConsole_Editor( QWidget* parent,
172 PyConsole_Interp* interp )
173 : QTextEdit( parent )
175 myInterp.takeRef(interp);
180 void PyConsole_Editor::init()
186 myIsSuppressOutput = false;
187 myMultiLinePaste = false;
188 myAutoCompletion = false;
190 myComplCursorPos = -1;
192 setFont( QFont( "Courier", 11 ) ); // default font
193 setUndoRedoEnabled( false );
195 myPrompt = READY_PROMPT;
196 setLineWrapMode( QTextEdit::WidgetWidth );
197 setWordWrapMode( QTextOption::WrapAnywhere );
198 setAcceptRichText( false );
200 // set callbacks to interpeter
201 myInterp->setvoutcb( PyConsole_CallbackStdout, this );
202 myInterp->setverrcb( PyConsole_CallbackStderr, this );
204 if ( isShowBanner() )
206 // clear command buffer
207 myCommandBuffer.truncate(0);
208 // unset read-only state
209 setReadOnly( false );
210 // unset history browsing mode
215 viewport()->unsetCursor();
221 PyConsole_Editor::~PyConsole_Editor()
226 \brief Get Python interpreter
228 PyConsole_Interp *PyConsole_Editor::getInterp() const
230 return myInterp.iAmATrollConstCast();
234 \brief Get synchronous mode flag value.
237 \return \c true if python console works in synchronous mode
239 bool PyConsole_Editor::isSync() const
245 \brief Set synchronous mode flag value.
247 In synhronous mode the Python commands are executed in the GUI thread
248 and the GUI is blocked until the command is finished. In the asynchronous
249 mode each Python command is executed in the separate thread that does not
250 block the main GUI loop.
252 \param on synhronous mode flag
254 void PyConsole_Editor::setIsSync( const bool on )
260 \brief Get suppress output flag value.
262 \sa setIsSuppressOutput()
263 \return \c true if python console output is suppressed.
265 bool PyConsole_Editor::isSuppressOutput() const
267 return myIsSuppressOutput;
271 \brief Set suppress output flag value.
273 In case if suppress output flag is \c true, the python
274 console output suppressed.
276 \param on suppress output flag
278 void PyConsole_Editor::setIsSuppressOutput( const bool on )
280 myIsSuppressOutput = on;
284 \brief Get 'show banner' flag value.
286 \sa setIsShowBanner()
287 \return \c true if python console shows banner
289 bool PyConsole_Editor::isShowBanner() const
295 \brief Set 'show banner' flag value.
297 The banner is shown in the top of the python console window.
300 \param on 'show banner' flag
302 void PyConsole_Editor::setIsShowBanner( const bool on )
304 if ( myShowBanner != on ) {
311 \brief Switch on/off commands auto-completion feature
314 void PyConsole_Editor::setAutoCompletion( bool on )
316 myAutoCompletion = on;
317 document()->setUndoRedoEnabled( myAutoCompletion );
321 \brief Returns \c true if auto-completion feature is switched on
322 or \c false otherwise
323 \sa setAutoCompletion()
325 bool PyConsole_Editor::autoCompletion() const
327 return myAutoCompletion;
331 \brief Check if trace logging is switched on.
333 \sa startLog(), stopLog()
334 \return \c true if trace logging is switched on
336 bool PyConsole_Editor::isLogging() const
338 return !myLogFile.isEmpty();
342 \brief Get size hint for the Python console window
343 \return size hint value
345 QSize PyConsole_Editor::sizeHint() const
347 QFontMetrics fm( font() );
348 int nbLines = ( isShowBanner() ? banner().split("\n").count() : 0 ) + 1;
349 QSize s(100, fm.lineSpacing()*nbLines);
354 \brief Put the string \a str to the python editor.
355 \param str string to be put in the command line of the editor
356 \param newBlock if \c true, then the string is printed on a new line
357 \param isError if \c true, the text is printed in dark red
359 void PyConsole_Editor::addText( const QString& str,
363 QTextCursor aCursor = textCursor();
366 moveCursor( QTextCursor::End );
368 aCursor.insertBlock();
370 cf.setForeground( QBrush( Qt::red ) );
372 cf.setForeground( QBrush( Qt::black ) );
373 aCursor.insertText( str, cf );
374 moveCursor( QTextCursor::End );
375 ensureCursorVisible();
379 \brief Convenient method for executing a Python command,
380 as if the user typed it manually.
381 \param command python command to be executed
383 !!! WARNING: doesn't work properly with multi-line commands. !!!
385 void PyConsole_Editor::exec( const QString& command )
387 if ( isReadOnly() ) {
388 // some interactive command is being executed in this editor...
389 // shedule the command to the queue
390 myQueue.push_back( command );
395 moveCursor( QTextCursor::End );
396 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
397 textCursor().removeSelectedText();
399 // set "ready" prompt
400 myPrompt = READY_PROMPT;
402 // clear command buffer
403 myCommandBuffer.truncate( 0 );
405 // unset history browsing mode
408 // print command line by line
409 QString cmd = command;
410 if ( !cmd.endsWith( "\n" ) ) cmd += "\n";
411 QStringList lines = command.split( "\n" );
412 for ( int i = 0; i < lines.size(); i++ ) {
413 if ( !lines[i].trimmed().isEmpty() )
414 myHistory.push_back( lines[i] );
415 addText( ( i == 0 ? READY_PROMPT : DOTS_PROMPT ) + lines[i], i != 0 );
416 putLog( QString( "%1%2\n" ).arg( i == 0 ? READY_PROMPT : DOTS_PROMPT ).arg( lines[i] ) );
422 // set read-only mode
426 setCursor( Qt::BusyCursor );
428 // post a request to execute Python command;
429 // editor will be informed via a custom event that execution has been completed
430 PyInterp_Dispatcher::Get()->Exec( createCmdRequest( cmd ) );
434 \brief Create request to the python dispatcher for the command execution.
435 \param command python command to be executed
437 PyInterp_Request* PyConsole_Editor::createCmdRequest( const QString& command )
439 return new PyConsole_ExecCommand( myInterp, command, this, isSync() );
443 \brief Create the Python request that will be posted to the interpreter to
445 \param input line entered by the user at the time <TAB> was pressed
446 \return completion command
447 \sa CompletionCommand
449 PyInterp_Request* PyConsole_Editor::createTabRequest( const QString& input )
452 static QStringList separators;
453 if ( separators.isEmpty() ) {
454 separators << " " << "(" << "[" << "+" << "-" << "*" << "/" << ";" << "^" << "=";
457 // parse input to extract on what part the dir() has to be executed
459 QString input2( input );
461 // split up to the last syntaxical separator
463 foreach ( QString separator, separators ) {
464 int j = input2.lastIndexOf( separator );
469 input2 = input.mid( lastSp + 1 );
471 // detect a qualified name (with a point)
472 int lastPt = input2.lastIndexOf( "." );
474 if ( lastPt != -1 ) {
475 // split the 2 surrounding parts of the qualified name
476 myComplBeforePoint = input2.left( lastPt );
477 myComplAfterPoint = input2.mid( lastPt+1 );
480 // no point found - do a global matching
481 // (the following will call dir() with an empty string)
482 myComplAfterPoint = input2;
483 myComplBeforePoint = "";
486 return new PyConsole_CompletionCommand( myInterp, myComplBeforePoint,
487 myComplAfterPoint, this, isSync() );
491 \brief Execute command in the python interpreter
492 and wait until it is finished.
494 \param command python command to be executed
496 void PyConsole_Editor::execAndWait( const QString& command )
502 // create new event loop
503 bool sync = isSync();
505 myEventLoop = new QEventLoop( this );
514 // delete event loop after command is processed
521 \brief Process <Enter> key press event.
523 Execute the command entered by the user.
525 void PyConsole_Editor::handleReturn()
527 // Position cursor at the end
528 QTextCursor aCursor = textCursor();
529 aCursor.movePosition( QTextCursor::End );
530 setTextCursor( aCursor );
533 QTextBlock par = document()->end().previous();
534 if ( !par.isValid() ) return;
537 QString cmd = par.text().remove( 0, promptSize() );
539 // extend the command buffer with the current command
540 myCommandBuffer.append( cmd );
542 // add command to the history
543 if ( !cmd.trimmed().isEmpty() )
544 myHistory.push_back( cmd );
545 putLog( QString( "%1%2\n" ).arg( myPrompt ).arg( cmd ) );
550 // set read-only mode
554 setCursor( Qt::BusyCursor );
556 // post a request to execute Python command;
557 // editor will be informed via a custom event that execution has been completed
558 PyInterp_Dispatcher::Get()->Exec( createCmdRequest( myCommandBuffer ) );
562 \brief Process <Tab> key press event.
564 Perform auto-completion of the currently entered command, if this feature is enabled
566 void PyConsole_Editor::handleTab()
568 if ( !autoCompletion() )
569 return; // auto-completion feature is disabled
572 return; // already in tab mode
574 QTextCursor aCursor = textCursor();
576 // move cursor to the end of input
577 aCursor.movePosition( QTextCursor::End );
578 setTextCursor( aCursor );
580 // save cursor position if needed
581 if ( myComplCursorPos == -1 )
582 myComplCursorPos = textCursor().position();
585 QTextBlock par = document()->end().previous();
586 if ( !par.isValid() ) return; // empty line
588 // switch to completion mode
591 // get currently entered command
592 QString cmd = par.text().mid( promptSize() );
594 // post completion request
595 // editor will be informed that completion has been done via a custom event
596 PyInterp_Dispatcher::Get()->Exec( createTabRequest( cmd ) );
600 \brief Process <Ctrl><Tab> key press event.
602 Undoe last auto-completion
604 void PyConsole_Editor::handleBackTab()
606 if ( !autoCompletion() )
607 return; // auto-completion feature is disabled
609 QTextCursor aCursor = textCursor();
611 if ( myComplCursorPos == -1 )
612 return; // invalid cursor position
614 // ensure cursor is at the end of command line
615 aCursor.setPosition( myComplCursorPos );
616 aCursor.movePosition( QTextCursor::EndOfLine );
617 //setCursor( aCursor );
619 // delete last completed text
620 int i = aCursor.position() - myComplCursorPos;
621 aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, i );
622 aCursor.removeSelectedText();
623 myComplCursorPos = -1;
627 \brief Process drop event.
630 \param event drop event
632 void PyConsole_Editor::dropEvent( QDropEvent* event )
634 // get the initial drop position
635 QPoint pos = event->pos();
636 QTextCursor aCursor = cursorForPosition( event->pos() );
638 // if the position is not in the last line move it to the end of the command line
639 if ( aCursor.position() < document()->end().previous().position() + promptSize() ) {
640 moveCursor( QTextCursor::End );
641 pos = cursorRect().center();
644 // create new drop event and use it instead of the original
646 event->possibleActions(),
648 event->mouseButtons(),
649 event->keyboardModifiers(),
651 QTextEdit::dropEvent( &de );
653 // accept the original event
654 event->acceptProposedAction();
658 \brief Process mouse press event
660 Clear the completion when any mouse button is pressed.
662 \param e mouse press event
664 void PyConsole_Editor::mousePressEvent( QMouseEvent* event )
666 if ( autoCompletion() ) {
668 myComplCursorPos = -1;
670 QTextEdit::mousePressEvent( event );
674 \brief Process mouse button release event.
676 Left mouse button: copy selection to the clipboard.
677 Middle mouse button: paste clipboard's contents.
679 \param event mouse event
681 void PyConsole_Editor::mouseReleaseEvent( QMouseEvent* event )
683 if ( event->button() == Qt::LeftButton ) {
684 QTextEdit::mouseReleaseEvent( event );
686 else if ( event->button() == Qt::MidButton ) {
687 QTextCursor aCursor = cursorForPosition( event->pos() );
688 // if the position is not in the last line move it to the end of the command line
689 if ( aCursor.position() < document()->end().previous().position() + promptSize() ) {
690 moveCursor( QTextCursor::End );
693 setTextCursor( aCursor );
695 const QMimeData* md = QApplication::clipboard()->mimeData( QApplication::clipboard()->supportsSelection() ?
696 QClipboard::Selection : QClipboard::Clipboard );
698 insertFromMimeData( md );
701 QTextEdit::mouseReleaseEvent( event );
706 \brief Check if the string is command.
708 Return \c true if the string \a str is likely to be the command
709 (i.e. it is started from the '>>>' or '...').
710 \param str string to be checked
712 bool PyConsole_Editor::isCommand( const QString& str ) const
714 return str.startsWith( READY_PROMPT ) || str.startsWith( DOTS_PROMPT );
718 \brief Handle keyboard event.
720 Implement navigation, history browsing, copy/paste and other common
723 \param event keyboard event
725 void PyConsole_Editor::keyPressEvent( QKeyEvent* event )
727 // get cursor position
728 QTextCursor aCursor = textCursor();
729 int curLine = aCursor.blockNumber();
730 int curCol = aCursor.columnNumber();
732 // get last edited line
733 int endLine = document()->blockCount()-1;
735 // get pressed key code
736 int aKey = event->key();
738 // check if <Ctrl> is pressed
739 bool ctrlPressed = event->modifiers() & Qt::ControlModifier;
740 // check if <Shift> is pressed
741 bool shftPressed = event->modifiers() & Qt::ShiftModifier;
743 if ( autoCompletion() ) {
744 // auto-completion support
745 if ( aKey == Qt::Key_Tab && !shftPressed ) {
747 if ( !ctrlPressed ) {
757 // If <Ctrl> is not pressed (or if something else is pressed with <Ctrl>),
758 // or if <Ctrl> is not pressed alone, we have to clear completion
759 if ( !ctrlPressed || ( ctrlPressed && aKey != Qt::Key_Control ) ) {
761 myComplCursorPos = -1;
764 // Discard <Ctrl> pressed alone:
765 if ( aKey == Qt::Key_Control )
769 if ( aKey == Qt::Key_Escape || ( ctrlPressed && aKey == -1 ) ) {
770 // process <Ctrl>+<Break> key-binding and <Escape> key: clear current command
771 myCommandBuffer.truncate( 0 );
772 myPrompt = READY_PROMPT;
773 addText( myPrompt, true );
774 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
777 else if ( ctrlPressed && aKey == Qt::Key_C ) {
778 // process <Ctrl>+<C> key-binding : copy
782 else if ( ctrlPressed && aKey == Qt::Key_X ) {
783 // process <Ctrl>+<X> key-binding : cut
787 else if ( ctrlPressed && aKey == Qt::Key_V ) {
788 // process <Ctrl>+<V> key-binding : paste
793 // check for printed key
794 // #### aKey = ( aKey < Qt::Key_Space || aKey > Qt::Key_ydiaeresis ) ? aKey : 0;
796 aKey = !(QChar(aKey).isPrint()) ? aKey : 0;
800 // any printed key: just print it
802 if ( curLine < endLine || curCol < promptSize() ) {
803 moveCursor( QTextCursor::End );
805 QTextEdit::keyPressEvent( event );
810 // <Enter> key: process the current command
816 // <Up> arrow key: process as follows:
817 // - without <Ctrl>, <Shift> modifiers: previous command in history
818 // - with <Ctrl> modifier key pressed: move cursor one row up without selection
819 // - with <Shift> modifier key pressed: move cursor one row up with selection
820 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one row up
822 if ( ctrlPressed && shftPressed ) {
823 int value = verticalScrollBar()->value();
824 int spacing = fontMetrics().lineSpacing();
825 verticalScrollBar()->setValue( value > spacing ? value-spacing : 0 );
827 else if ( shftPressed || ctrlPressed ) {
829 moveCursor( QTextCursor::Up,
830 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
833 if ( myCmdInHistory < 0 && myHistory.count() > 0 ) {
834 // set history browsing mode
835 myCmdInHistory = myHistory.count();
836 // remember current command
837 QTextBlock par = document()->end().previous();
838 myCurrentCommand = par.text().remove( 0, promptSize() );
840 if ( myCmdInHistory > 0 ) {
842 // get previous command in the history
843 QString previousCommand = myHistory.at( myCmdInHistory );
844 // print previous command
845 moveCursor( QTextCursor::End );
846 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
847 textCursor().removeSelectedText();
848 addText( myPrompt + previousCommand );
849 // move cursor to the end
850 moveCursor( QTextCursor::End );
856 // <Down> arrow key: process as follows:
857 // - without <Ctrl>, <Shift> modifiers: next command in history
858 // - with <Ctrl> modifier key pressed: move cursor one row down without selection
859 // - with <Shift> modifier key pressed: move cursor one row down with selection
860 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one row down
862 if ( ctrlPressed && shftPressed ) {
863 int value = verticalScrollBar()->value();
864 int maxval = verticalScrollBar()->maximum();
865 int spacing = fontMetrics().lineSpacing();
866 verticalScrollBar()->setValue( value+spacing < maxval ? value+spacing : maxval );
868 else if ( shftPressed || ctrlPressed) {
869 if ( curLine < endLine )
870 moveCursor( QTextCursor::Down,
871 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
874 if ( myCmdInHistory >= 0 ) {
875 // get next command in the history
878 if ( myCmdInHistory < myHistory.count() ) {
879 // next command in history
880 nextCommand = myHistory.at( myCmdInHistory );
883 // end of history is reached
884 // last printed command
885 nextCommand = myCurrentCommand;
886 // unset history browsing mode
889 // print next or current command
890 moveCursor( QTextCursor::End );
891 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
892 textCursor().removeSelectedText();
893 addText( myPrompt + nextCommand );
894 // move cursor to the end
895 moveCursor( QTextCursor::End );
901 // <Left> arrow key: process as follows:
902 // - without <Ctrl>, <Shift> modifiers: move one symbol left (taking into account prompt)
903 // - with <Ctrl> modifier key pressed: move one word left (taking into account prompt)
904 // - with <Shift> modifier key pressed: move one symbol left with selection
905 // - with <Ctrl>+<Shift> modifier keys pressed: move one word left with selection
907 QString txt = textCursor().block().text();
908 if ( !shftPressed && isCommand( txt ) && curCol <= promptSize() ) {
909 moveCursor( QTextCursor::Up );
910 moveCursor( QTextCursor::EndOfBlock );
913 QTextEdit::keyPressEvent( event );
918 // <Right> arrow key: process as follows:
919 // - without <Ctrl>, <Shift> modifiers: move one symbol right (taking into account prompt)
920 // - with <Ctrl> modifier key pressed: move one word right (taking into account prompt)
921 // - with <Shift> modifier key pressed: move one symbol right with selection
922 // - with <Ctrl>+<Shift> modifier keys pressed: move one word right with selection
924 QString txt = textCursor().block().text();
925 if ( !shftPressed ) {
926 if ( curCol < txt.length() ) {
927 if ( isCommand( txt ) && curCol < promptSize() ) {
928 aCursor.setPosition( aCursor.block().position() + promptSize() );
929 setTextCursor( aCursor );
934 if ( curLine < endLine && isCommand( textCursor().block().next().text() ) ) {
935 aCursor.setPosition( aCursor.position() + promptSize()+1 );
936 setTextCursor( aCursor );
937 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
942 QTextEdit::keyPressEvent( event );
946 // <PageUp> key: process as follows:
947 // - without <Ctrl>, <Shift> modifiers: first command in history
948 // - with <Ctrl> modifier key pressed: move cursor one page up without selection
949 // - with <Shift> modifier key pressed: move cursor one page up with selection
950 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one page up
952 if ( ctrlPressed && shftPressed ) {
953 verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub);
955 else if ( shftPressed || ctrlPressed ) {
957 qreal lastY = cursorRect( aCursor ).top();
959 // move using movePosition to keep the cursor's x
961 qreal y = cursorRect( aCursor ).top();
962 distance += qAbs( y - lastY );
964 moved = aCursor.movePosition( QTextCursor::Up,
965 shftPressed ? QTextCursor::KeepAnchor :
966 QTextCursor::MoveAnchor );
967 } while ( moved && distance < viewport()->height() );
969 aCursor.movePosition( QTextCursor::Down,
970 shftPressed ? QTextCursor::KeepAnchor :
971 QTextCursor::MoveAnchor );
972 verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub );
974 setTextCursor( aCursor );
977 if ( myCmdInHistory < 0 && myHistory.count() > 0 ) {
978 // set history browsing mode
979 myCmdInHistory = myHistory.count();
980 // remember current command
981 QTextBlock par = document()->end().previous();
982 myCurrentCommand = par.text().remove( 0, promptSize() );
984 if ( myCmdInHistory > 0 ) {
986 // get very first command in the history
987 QString firstCommand = myHistory.at( myCmdInHistory );
988 // print first command
989 moveCursor( QTextCursor::End );
990 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
991 textCursor().removeSelectedText();
992 addText( myPrompt + firstCommand );
993 // move cursor to the end
994 moveCursor( QTextCursor::End );
999 case Qt::Key_PageDown:
1000 // <PageDown> key: process as follows:
1001 // - without <Ctrl>, <Shift> modifiers: last command in history
1002 // - with <Ctrl> modifier key pressed: move cursor one page down without selection
1003 // - with <Shift> modifier key pressed: move cursor one page down with selection
1004 // - with <Ctrl>+<Shift> modifier keys pressed: scroll one page down
1006 if ( ctrlPressed && shftPressed ) {
1007 verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd);
1009 else if ( shftPressed || ctrlPressed ) {
1011 qreal lastY = cursorRect( aCursor ).top();
1013 // move using movePosition to keep the cursor's x
1015 qreal y = cursorRect( aCursor ).top();
1016 distance += qAbs( y - lastY );
1018 moved = aCursor.movePosition( QTextCursor::Down,
1019 shftPressed ? QTextCursor::KeepAnchor :
1020 QTextCursor::MoveAnchor );
1021 } while ( moved && distance < viewport()->height() );
1023 aCursor.movePosition( QTextCursor::Up,
1024 shftPressed ? QTextCursor::KeepAnchor :
1025 QTextCursor::MoveAnchor );
1026 verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub );
1028 setTextCursor( aCursor );
1031 if ( myCmdInHistory >= 0 ) {
1032 // unset history browsing mode
1033 myCmdInHistory = -1;
1034 // print current command
1035 moveCursor( QTextCursor::End );
1036 moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
1037 textCursor().removeSelectedText();
1038 addText( myPrompt + myCurrentCommand );
1039 // move cursor to the end
1040 moveCursor( QTextCursor::End );
1046 // <Home> key: process as follows:
1047 // - without <Ctrl>, <Shift> modifiers: move cursor to the beginning of the current line without selection
1048 // - with <Ctrl> modifier key pressed: move cursor to the very first symbol without selection
1049 // - with <Shift> modifier key pressed: move cursor to the beginning of the current line with selection
1050 // - with <Ctrl>+<Shift> modifier keys pressed: move cursor to the very first symbol with selection
1052 if ( ctrlPressed ) {
1053 moveCursor( QTextCursor::Start,
1054 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1057 QString txt = textCursor().block().text();
1058 if ( isCommand( txt ) ) {
1059 if ( shftPressed ) {
1060 if ( curCol > promptSize() ) {
1061 aCursor.movePosition( QTextCursor::StartOfLine, QTextCursor::KeepAnchor );
1062 aCursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, promptSize() );
1066 aCursor.movePosition( QTextCursor::StartOfLine );
1067 aCursor.movePosition( QTextCursor::Right, QTextCursor::MoveAnchor, promptSize() );
1069 setTextCursor( aCursor );
1072 moveCursor( QTextCursor::StartOfBlock,
1073 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1075 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1080 // <End> key: process as follows:
1081 // - without <Ctrl>, <Shift> modifiers: move cursor to the end of the current line without selection
1082 // - with <Ctrl> modifier key pressed: move cursor to the very last symbol without selection
1083 // - with <Shift> modifier key pressed: move cursor to the end of the current line with selection
1084 // - with <Ctrl>+<Shift> modifier keys pressed: move cursor to the very last symbol with selection
1086 moveCursor( ctrlPressed ? QTextCursor::End : QTextCursor::EndOfBlock,
1087 shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1090 case Qt::Key_Backspace :
1091 // <Backspace> key: process as follows
1092 // - without any modifiers : delete symbol before the cursor / selection (taking into account prompt)
1093 // - with <Shift> modifier key pressed: delete previous word
1094 // - with <Ctrl> modifier key pressed: delete text from the cursor to the line beginning
1095 // works only for last (command) line
1097 if ( aCursor.hasSelection() ) {
1100 else if ( aCursor.position() > document()->end().previous().position() + promptSize() ) {
1101 if ( shftPressed ) {
1102 moveCursor( QTextCursor::PreviousWord, QTextCursor::KeepAnchor );
1103 textCursor().removeSelectedText();
1105 else if ( ctrlPressed ) {
1106 aCursor.setPosition( document()->end().previous().position() + promptSize(),
1107 QTextCursor::KeepAnchor );
1108 setTextCursor( aCursor );
1109 textCursor().removeSelectedText();
1112 QTextEdit::keyPressEvent( event );
1116 aCursor.setPosition( document()->end().previous().position() + promptSize() );
1117 setTextCursor( aCursor );
1118 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1122 case Qt::Key_Delete :
1123 // <Delete> key: process as follows
1124 // - without any modifiers : delete symbol after the cursor / selection (taking into account prompt)
1125 // - with <Shift> modifier key pressed: delete next word
1126 // - with <Ctrl> modifier key pressed: delete text from the cursor to the end of line
1127 // works only for last (command) line
1129 if ( aCursor.hasSelection() ) {
1132 else if ( aCursor.position() > document()->end().previous().position() + promptSize()-1 ) {
1133 if ( shftPressed ) {
1134 moveCursor( QTextCursor::NextWord, QTextCursor::KeepAnchor );
1135 textCursor().removeSelectedText();
1137 else if ( ctrlPressed ) {
1138 moveCursor( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
1139 textCursor().removeSelectedText();
1142 QTextEdit::keyPressEvent( event );
1146 aCursor.setPosition( document()->end().previous().position() + promptSize() );
1147 setTextCursor( aCursor );
1148 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1152 case Qt::Key_Insert :
1153 // <Insert> key: process as follows
1154 // - with <Ctrl> modifier key pressed: copy()
1155 // - with <Shift> modifier key pressed: paste() to the command line
1157 if ( ctrlPressed ) {
1160 else if ( shftPressed ) {
1164 QTextEdit::keyPressEvent( event );
1173 \brief Handle notification event coming from Python dispatcher.
1174 \param event notification event
1176 void PyConsole_Editor::customEvent( QEvent* event )
1178 switch( event->type() )
1180 case PyConsole_PrintEvent::EVENT_ID:
1182 PyConsole_PrintEvent* pe = (PyConsole_PrintEvent*)event;
1183 addText( pe->text(), false, pe->isError() );
1186 case PyConsole_CompletionEvent::EVENT_ID:
1188 PyConsole_CompletionEvent* ce = (PyConsole_CompletionEvent*)event;
1189 bool status = ce->status();
1190 QStringList matches = ce->matches();
1191 QString doc = ce->doc();
1194 // completion was successful
1195 QTextCursor aCursor = textCursor();
1197 if ( matches.isEmpty() ) {
1198 // completion successful but there are no matches.
1200 myComplCursorPos = -1;
1204 if ( matches.size() == 1 ) {
1205 // there's only one match - complete directly and update doc string window
1206 aCursor.insertText( matches[0].mid( myComplAfterPoint.size() ) );
1208 if ( doc.isEmpty() )
1209 emit updateDoc( formatDocHTML( QString( "(%1)\n" ).arg( tr( "NO_DOC_AVAILABLE" ) ) ) );
1211 emit updateDoc( formatDocHTML( doc ) );
1214 // there are several matches
1216 // detect if there is a common base to all available completion
1217 // in this case append this base to the text
1218 QString base = extractCommon( matches );
1219 aCursor.insertText( base.mid( myComplAfterPoint.size() ) );
1221 // if this happens to match exactly the first completion
1223 if ( base == matches[0] )
1224 emit updateDoc( formatDocHTML( doc ) );
1226 // print all matching completion in a "undo-able" block
1227 int cursorPos = aCursor.position();
1228 aCursor.insertBlock();
1229 aCursor.beginEditBlock();
1231 // insert all matches
1233 cf.setForeground( QBrush( Qt::darkGreen ) );
1234 aCursor.setCharFormat( cf );
1235 aCursor.insertText( formatCompletion( matches ) );
1236 aCursor.endEditBlock();
1238 // position cursor where it was before inserting the completion list
1239 aCursor.setPosition( cursorPos );
1240 setTextCursor( aCursor );
1244 // completion failed
1246 myComplCursorPos = -1;
1250 case PyInterp_Event::ES_OK:
1251 case PyInterp_Event::ES_ERROR:
1253 // clear command buffer
1254 myCommandBuffer.truncate( 0 );
1255 // add caret return line if necessary
1256 QTextBlock par = document()->end().previous();
1257 QString txt = par.text();
1258 txt.truncate( txt.length() - 1 );
1259 // IPAL19397 : addText moved to handleReturn() method
1260 //if ( !txt.isEmpty() )
1261 // addText( "", true );
1262 // set "ready" prompt
1263 myPrompt = READY_PROMPT;
1264 addText( myPrompt );
1265 // unset busy cursor
1267 // stop event loop (if running)
1269 myEventLoop->exit();
1270 // if we are in multi_paste_mode, process the next item
1271 multiLineProcessNextLine();
1274 case PyInterp_Event::ES_INCOMPLETE:
1276 // extend command buffer (multi-line command)
1277 myCommandBuffer.append( "\n" );
1278 // add caret return line if necessary
1279 QTextBlock par = document()->end().previous();
1280 QString txt = par.text();
1281 txt.truncate( txt.length() - 1 );
1282 // IPAL19397 : addText moved to handleReturn() method
1283 //if ( !txt.isEmpty() )
1284 // addText( "", true );
1286 myPrompt = DOTS_PROMPT;
1287 addText( myPrompt/*, true*/ ); // IPAL19397
1288 // unset busy cursor
1290 // stop event loop (if running)
1292 myEventLoop->exit();
1293 // if we are in multi_paste_mode, process the next item
1294 multiLineProcessNextLine();
1298 QTextEdit::customEvent( event );
1301 // unset read-only state
1302 setReadOnly( false );
1303 // unset history browsing mode
1304 myCmdInHistory = -1;
1306 if ( (int)event->type() == (int)PyInterp_Event::ES_OK && myQueue.count() > 0 )
1308 // process the next sheduled command from the queue (if there is any)
1309 QString nextcmd = myQueue[0];
1310 myQueue.pop_front();
1316 \brief "Copy" operation.
1318 Reimplemented from Qt.
1319 Warning! In Qt this method is not virtual.
1321 void PyConsole_Editor::cut()
1323 QTextCursor aCursor = textCursor();
1324 if ( aCursor.hasSelection() ) {
1325 QApplication::clipboard()->setText( aCursor.selectedText() );
1326 int startSelection = aCursor.selectionStart();
1327 if ( startSelection < document()->end().previous().position() + promptSize() )
1328 startSelection = document()->end().previous().position() + promptSize();
1329 int endSelection = aCursor.selectionEnd();
1330 if ( endSelection < document()->end().previous().position() + promptSize() )
1331 endSelection = document()->end().previous().position() + promptSize();
1332 aCursor.setPosition( startSelection );
1333 aCursor.setPosition( endSelection, QTextCursor::KeepAnchor );
1334 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1335 setTextCursor( aCursor );
1336 textCursor().removeSelectedText();
1341 \brief "Paste" operation.
1343 Reimplemented from Qt.
1344 Warning! In Qt this method is not virtual.
1346 void PyConsole_Editor::paste()
1348 QTextCursor aCursor = textCursor();
1349 if ( aCursor.hasSelection() ) {
1350 int startSelection = aCursor.selectionStart();
1351 if ( startSelection < document()->end().previous().position() + promptSize() )
1352 startSelection = document()->end().previous().position() + promptSize();
1353 int endSelection = aCursor.selectionEnd();
1354 if ( endSelection < document()->end().previous().position() + promptSize() )
1355 endSelection = document()->end().previous().position() + promptSize();
1356 aCursor.setPosition( startSelection );
1357 aCursor.setPosition( endSelection, QTextCursor::KeepAnchor );
1358 horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1359 setTextCursor( aCursor );
1360 textCursor().removeSelectedText();
1362 if ( textCursor().position() < document()->end().previous().position() + promptSize() )
1363 moveCursor( QTextCursor::End );
1368 \brief "Clear" operation.
1370 Reimplemented from Qt.
1371 Warning! In Qt this method is not virtual.
1373 void PyConsole_Editor::clear()
1376 if ( isShowBanner() )
1377 addText( banner() );
1378 myPrompt = READY_PROMPT;
1379 addText( myPrompt );
1383 \brief Dumps recorded Python commands to the file
1384 \param fileName path to the dump file
1385 \return \c true if dump operation succeeded or \c false otherwise
1387 bool PyConsole_Editor::dump( const QString& fileName )
1390 if ( !fileName.isEmpty() ) {
1391 QFile file( fileName );
1392 if ( file.open( QFile::WriteOnly ) ) {
1393 QTextStream out( &file );
1394 for ( int i = 0; i < myHistory.count(); i++ ) {
1395 out << myHistory[i] << endl;
1405 \brief Dump menu action slot
1407 void PyConsole_Editor::dump()
1411 QString fileName = getDumpFileName();
1413 if ( fileName.isEmpty() )
1416 if ( dump( fileName ) )
1419 QMessageBox::warning( this,
1421 tr( "ERR_FILE_NOT_WRITEABLE" ) );
1426 \brief Get file name for Dump commands operation.
1428 This function can be redefined in successor classes to show application
1429 specific dialog box.
1431 \return path to the dump file
1433 QString PyConsole_Editor::getDumpFileName()
1435 return QFileDialog::getSaveFileName( this,
1436 tr( "GET_DUMP_COMMANDS_FILENAME" ),
1438 QString( "%1 (*.py)" ).arg( tr( "PYTHON_SCRIPTS" ) ) );
1442 \brief Get file name for Log Python trace operation.
1444 This function can be redefined in successor classes to show application
1445 specific dialog box.
1447 \return path to the log file
1449 QString PyConsole_Editor::getLogFileName()
1451 return QFileDialog::getSaveFileName( this,
1452 tr( "GET_PYTHON_TRACE_FILENAME" ),
1454 QString( "%1 (*.log *.txt)" ).arg( tr( "LOG_FILES" ) ) );
1458 \brief Start python trace logging
1459 \param fileName the path to the log file
1460 \return \c true if operation succeeded or \c false otherwise
1461 (for example, if file is not writeable)
1464 bool PyConsole_Editor::startLog( const QString& fileName )
1466 // stop possibly already running logging
1471 if ( !fileName.isEmpty() ) {
1472 QFile file( fileName );
1473 if ( file.open( QFile::WriteOnly ) ) {
1475 myLogFile = fileName;
1483 \brief Start log action slot
1485 void PyConsole_Editor::startLog()
1489 QString fileName = getLogFileName();
1491 if ( fileName.isEmpty() )
1494 if ( startLog( fileName ) )
1497 QMessageBox::warning( this,
1499 tr( "File is not writable" ) );
1504 \brief Stop log action slot
1506 Stops Python trace logging.
1508 void PyConsole_Editor::stopLog()
1510 myLogFile = QString();
1514 \brief Put data to the log file
1516 void PyConsole_Editor::putLog( const QString& s )
1518 if ( !myLogFile.isEmpty() ) {
1519 QFile file( myLogFile );
1520 if ( !file.open( QFile::Append ) )
1523 QTextStream out( &file );
1531 \brief Handle properly multi-line pasting. Qt documentation recommends overriding this function.
1532 If the pasted text doesn't contain a line return, no special treatment is done.
1535 void PyConsole_Editor::insertFromMimeData(const QMimeData* source)
1537 if ( myMultiLinePaste )
1540 if ( source->hasText() ) {
1541 QString s = source->text();
1542 if ( s.contains( "\n" ) )
1543 multilinePaste( s );
1545 QTextEdit::insertFromMimeData( source );
1548 QTextEdit::insertFromMimeData( source );
1553 Start multi-line paste operation
1556 void PyConsole_Editor::multilinePaste( const QString& s )
1558 // Turn on multi line pasting mode
1559 myMultiLinePaste = true;
1561 // Split string data to lines
1563 s2.replace( "\r", "" ); // Windows string format converted to Unix style
1564 QStringList lst = s2.split( QChar('\n'), QString::KeepEmptyParts );
1566 // Perform the proper paste operation for the first line to handle the case where
1567 // something was already there
1569 source.setText( lst[0] );
1570 QTextEdit::insertFromMimeData( &source );
1572 // Prepare what will have to be executed after the first line
1573 myMultiLineContent.clear();
1574 for ( int i = 1; i < lst.size(); ++i )
1575 myMultiLineContent.enqueue( lst[i] );
1577 // Trigger the execution of the first (mixed) line
1580 // See customEvent() and multiLineProcessNext() for the rest of the handling.
1584 \brief Process the next line in the queue of multi-line paste operation; called
1585 from customEvent() function
1588 void PyConsole_Editor::multiLineProcessNextLine()
1590 // not in multi-line paste mode
1591 if ( !myMultiLinePaste || myMultiLineContent.isEmpty() )
1594 QString line = myMultiLineContent.dequeue();
1595 if ( myMultiLineContent.empty() )
1597 // this isa last line in the queue, just paste it
1598 addText( line, false, false );
1599 myMultiLinePaste = false;
1603 // paste the line and simulate a <RETURN> key stroke
1604 addText( line, false, false );
1610 \brief Clear results of completion
1612 void PyConsole_Editor::clearCompletion()
1614 // delete completion text if present
1616 // remove completion display
1618 // remove trailing line return:
1619 QTextCursor tc( textCursor() );
1620 tc.setPosition( document()->characterCount()-1 );
1621 setTextCursor( tc );
1622 textCursor().deletePreviousChar();
1623 // TODO: before wait for any <Tab> event to be completed
1629 \brief Format completion results - this is where we should create 3 columns etc ...
1630 \param matches list of possible completions
1631 \return result string
1633 QString PyConsole_Editor::formatCompletion( const QStringList& matches ) const
1635 static const int MAX_COMPLETIONS = 70;
1638 int sz = matches.size();
1640 if ( sz > MAX_COMPLETIONS )
1641 result.append( QString( "[%1]" ).arg( tr( "TOO_MANY_MATCHES" ) ) );
1643 for ( int i = 0; i < qMin( sz, MAX_COMPLETIONS); ++i )
1644 result.append( matches[i] );
1646 return result.join( "\n" );
1650 \brief Format the doc string in HTML format with the first line in bold blue
1651 \param doc initial doc string
1654 QString PyConsole_Editor::formatDocHTML( const QString& doc ) const
1656 static const char* templ = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" " \
1657 "\"http://www.w3.org/TR/REC-html40/strict.dtd\">\n" \
1658 "<html><head><meta name=\"qrichtext\" content=\"1\" /> " \
1659 "<style type=\"text/css\">\np, li { white-space: pre-wrap; }\n</style> " \
1660 "</head><body style=\" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;\">\n" \
1661 "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"> " \
1662 "<span style=\" font-weight:600; color:#0000ff;\">%1</span></p> " \
1663 "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">%2</p> " \
1666 QString fst, rest("");
1668 // Extract first line of doc
1669 int idx = doc.indexOf( "\n" );
1671 fst = doc.left( idx );
1672 rest = doc.mid( idx+1 );
1678 fst = fst.replace( "\n", " " );
1679 rest = rest.replace(" \n", " " );
1680 return QString( templ ).arg( fst ).arg( rest );
1684 \fn void PyConsole_Editor::updateDoc( const QString& doc);
1685 \brief Signal emitted by the editor widget when the doc string should be updated.
1686 \param doc a HTML block with the formatted doc string.
1687 \todo currently this signal is left uncaught.
1691 \brief Extract the common leading part of all strings in matches.
1695 QString PyConsole_Editor::extractCommon( const QStringList& matches ) const
1697 QString result = "";
1699 if ( matches.size() > 1 ) {
1702 while ( ok && l+1 < matches[0].size() ) {
1703 QString match = matches[0].left( l+1 );
1704 for ( int j = 1; j < matches.size() && ok; j++ )
1705 ok = matches[j].startsWith( match );
1709 result = matches[0].left( l );
1716 \brief Useful method to get banner from Python interpreter
1719 QString PyConsole_Editor::banner() const
1721 return myInterp->getBanner().c_str();