Salome HOME
updated copyright message
[modules/gui.git] / tools / PyConsole / src / PyConsole_Editor.cxx
1 // Copyright (C) 2007-2023  CEA/DEN, EDF R&D, OPEN CASCADE
2 //
3 // Copyright (C) 2003-2007  OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN,
4 // CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS
5 //
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.
10 //
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.
15 //
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
19 //
20 // See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
21 //
22 // File   : PyConsole_Editor.cxx
23 // Author : Vadim SANDLER, Open CASCADE S.A.S. (vadim.sandler@opencascade.com)
24
25 /*!
26   \class PyConsole_Editor
27   \brief Python command line interpreter front-end GUI widget.
28   
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.
32
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
83   - <Ctrl><V>            : paste
84   - <Ctrl><C>            : copy
85   - <Ctrl><X>            : cut
86   - <Ctrl><V>            : paste
87   - <Tab>                : performs auto-completion
88   - <Ctrl><Tab>          : undoes auto-completion
89 */
90
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"
96
97 #include <QApplication>
98 #include <QClipboard>
99 #include <QDropEvent>
100 #include <QEvent>
101 #include <QKeyEvent>
102 #include <QMimeData>
103 #include <QMouseEvent>
104 #include <QScrollBar>
105 #include <QTextBlock>
106 #include <QTextCursor>
107 #include <QTextStream>
108 #include <QChar>
109 #include <QFileDialog>
110 #include <QMessageBox>
111 #include <QtGlobal>
112
113 //VSR: uncomment below macro to support unicode text properly in SALOME
114 //     current commented out due to regressions
115 //#define PAL22528_UNICODE
116
117 namespace
118 {
119   QString fromUtf8( const char* txt )
120   {
121 #ifdef PAL22528_UNICODE
122     return QString::fromUtf8( txt );
123 #else
124     return QString( txt );
125 #endif
126   }
127 }
128
129 static QString READY_PROMPT = ">>> ";
130 static QString DOTS_PROMPT  = "... ";
131
132 void PyConsole_CallbackStdout( void* data, char* c )
133 {
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 ) );
138   }
139 }
140
141 void PyConsole_CallbackStderr( void* data, char* c )
142 {
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 ) );
147   }
148 }
149
150 /*!
151   \brief Default constructor. 
152   
153   Creates python editor window.
154   \param theParent parent widget
155 */
156 PyConsole_Editor::PyConsole_Editor( QWidget* parent )
157   : QTextEdit( parent )
158 {
159   PyConsole_Interp *interp(new PyConsole_Interp);
160   interp->initialize();
161   myInterp=interp;
162   init();
163 }
164
165 /*!
166   \brief Constructor. 
167   
168   Creates python editor window.
169   \param parent parent widget
170   \param interp python interpreter
171 */
172 PyConsole_Editor::PyConsole_Editor( QWidget*          parent,
173                                     PyConsole_Interp* interp )
174   : QTextEdit( parent )
175 {
176   myInterp.takeRef(interp);
177   init();
178 }
179
180
181 void PyConsole_Editor::init()
182 {
183   myCmdInHistory = -1;
184   myEventLoop = 0;
185   myShowBanner = true;
186   myIsSync = true;
187   myIsSuppressOutput = false;
188   myMultiLinePaste = false;
189   myAutoCompletion = false;
190   myTabMode = false;
191   myComplCursorPos = -1;
192
193   setFont( QFont( "Courier", 11 ) ); // default font
194   setUndoRedoEnabled( false );
195
196   myPrompt = READY_PROMPT;
197   setLineWrapMode( QTextEdit::WidgetWidth );
198   setWordWrapMode( QTextOption::WrapAnywhere );
199   setAcceptRichText( false );
200
201   // set callbacks to interpreter
202   myInterp->setvoutcb( PyConsole_CallbackStdout, this );
203   myInterp->setverrcb( PyConsole_CallbackStderr, this );
204   // print banner
205   if ( isShowBanner() )
206     addText( banner() );
207   // clear command buffer
208   myCommandBuffer.truncate(0);
209   // unset read-only state
210   setReadOnly( false );
211   // unset history browsing mode
212   myCmdInHistory = -1;
213   // add prompt
214   addText( myPrompt );
215   // unset busy cursor
216   viewport()->unsetCursor();
217 }
218
219 /*!
220   \brief Destructor.
221 */
222 PyConsole_Editor::~PyConsole_Editor()
223 {
224 }
225
226 /*!
227   \brief Get Python interpreter
228 */
229 PyConsole_Interp *PyConsole_Editor::getInterp() const
230 {
231   return myInterp.iAmATrollConstCast();
232 }
233
234 /*!
235   \brief Get synchronous mode flag value.
236   
237   \sa setIsSync()
238   \return \c true if python console works in synchronous mode
239 */
240 bool PyConsole_Editor::isSync() const
241 {
242   return myIsSync;
243 }
244
245 /*!
246   \brief Set synchronous mode flag value.
247
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.
252
253   \param on synhronous mode flag
254 */
255 void PyConsole_Editor::setIsSync( const bool on )
256 {
257   myIsSync = on;
258 }
259
260 /*!
261   \brief Get suppress output flag value.
262   
263   \sa setIsSuppressOutput()
264   \return \c true if python console output is suppressed.
265 */
266 bool PyConsole_Editor::isSuppressOutput() const
267 {
268   return myIsSuppressOutput;
269 }
270
271 /*!
272   \brief Set suppress output flag value.
273
274   In case if suppress output flag is \c true, the python 
275   console output suppressed.
276
277   \param on suppress output flag
278 */
279 void PyConsole_Editor::setIsSuppressOutput( const bool on )
280 {
281   myIsSuppressOutput = on;
282 }
283
284 /*!
285   \brief Get 'show banner' flag value.
286   
287   \sa setIsShowBanner()
288   \return \c true if python console shows banner
289 */
290 bool PyConsole_Editor::isShowBanner() const
291 {
292   return myShowBanner;
293 }
294
295 /*!
296   \brief Set 'show banner' flag value.
297
298   The banner is shown in the top of the python console window.
299
300   \sa isShowBanner()
301   \param on 'show banner' flag
302 */
303 void PyConsole_Editor::setIsShowBanner( const bool on )
304 {
305   if ( myShowBanner != on ) {
306     myShowBanner = on;
307     clear();
308   }
309 }
310
311 /*!
312   \brief Switch on/off commands auto-completion feature
313   \sa autoCompletion()
314 */
315 void PyConsole_Editor::setAutoCompletion( bool on )
316 {
317   myAutoCompletion = on;
318   document()->setUndoRedoEnabled( myAutoCompletion );
319 }
320   
321 /*!
322   \brief Returns \c true if auto-completion feature is switched on
323   or \c false otherwise
324   \sa setAutoCompletion()
325 */
326 bool PyConsole_Editor::autoCompletion() const
327 {
328   return myAutoCompletion;
329 }
330
331 /*!
332   \brief Check if trace logging is switched on.
333   
334   \sa startLog(), stopLog()
335   \return \c true if trace logging is switched on
336 */
337 bool PyConsole_Editor::isLogging() const
338 {
339   return !myLogFile.isEmpty();
340 }
341
342 /*!
343   \brief Get size hint for the Python console window
344   \return size hint value
345 */
346 QSize PyConsole_Editor::sizeHint() const
347 {
348   QFontMetrics fm( font() );
349   int nbLines = ( isShowBanner() ? banner().split("\n").count() : 0 ) + 1;
350   QSize s(100, fm.lineSpacing()*nbLines);
351   return s;
352 }
353
354 /*!
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
359 */
360 void PyConsole_Editor::addText( const QString& str, 
361                                 const bool     newBlock,
362                                 const bool     isError )
363 {
364   QTextCursor aCursor = textCursor();
365   QTextCharFormat cf;
366
367   moveCursor( QTextCursor::End );
368   if ( newBlock )
369     aCursor.insertBlock();
370   if ( isError )
371     cf.setForeground( QBrush( Qt::red ) );
372   aCursor.insertText( str, cf );
373   moveCursor( QTextCursor::End );
374   ensureCursorVisible();
375 }
376
377 /*!
378   \brief Convenient method for executing a Python command,
379   as if the user typed it manually.
380   \param command python command to be executed
381
382   !!! WARNING: doesn't work properly with multi-line commands. !!!
383 */
384 void PyConsole_Editor::exec( const QString& command )
385 {
386   if ( isReadOnly() ) {
387     // some interactive command is being executed in this editor...
388     // schedule the command to the queue
389     myQueue.push_back( command );
390     return;
391   }
392
393   // remove last line
394   moveCursor( QTextCursor::End );
395   moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
396   textCursor().removeSelectedText();
397
398   // set "ready" prompt
399   myPrompt = READY_PROMPT;
400
401   // clear command buffer
402   myCommandBuffer.truncate( 0 );
403
404   // unset history browsing mode
405   myCmdInHistory = -1;
406
407   // print command line by line
408   QString cmd = command;
409   if ( !cmd.endsWith( "\n" ) ) cmd += "\n";
410   QStringList lines = command.split( "\n" );
411   for ( int i = 0; i < lines.size(); i++ ) {
412     if ( !lines[i].trimmed().isEmpty() )
413       myHistory.push_back( lines[i] );
414     addText( ( i == 0 ? READY_PROMPT : DOTS_PROMPT ) + lines[i], i != 0 );
415     putLog( QString( "%1%2\n" ).arg( i == 0 ? READY_PROMPT : DOTS_PROMPT ).arg( lines[i] ) );
416   }
417
418   // IPAL20182
419   addText( "", true );
420
421   // set read-only mode
422   setReadOnly( true );
423
424   // set busy cursor
425   setCursor( Qt::BusyCursor );
426   
427   // post a request to execute Python command;
428   // editor will be informed via a custom event that execution has been completed
429   PyInterp_Dispatcher::Get()->Exec( createCmdRequest( cmd ) );
430 }
431
432 /*!
433   \brief Create request to the python dispatcher for the command execution.
434   \param command python command to be executed
435  */
436 PyInterp_Request* PyConsole_Editor::createCmdRequest( const QString& command )
437 {
438   return new PyConsole_ExecCommand( myInterp, command, this, isSync() );
439 }
440
441 /*!
442   \brief Create the Python request that will be posted to the interpreter to
443   get the completions.
444   \param input line entered by the user at the time <TAB> was pressed
445   \return completion command
446   \sa CompletionCommand
447 */
448 PyInterp_Request* PyConsole_Editor::createTabRequest( const QString& input )
449 {
450   // valid separators
451   static QStringList separators;
452   if ( separators.isEmpty() ) {
453     separators << " " << "(" << "[" << "+" << "-" << "*" << "/" << ";" << "^" << "=";
454   }
455
456   // parse input to extract on what part the dir() has to be executed
457
458   QString input2( input );
459
460   // split up to the last syntaxical separator
461   int lastSp = -1;
462   foreach ( QString separator, separators ) {
463     int j = input2.lastIndexOf( separator );
464     if ( j > lastSp )
465       lastSp = j;
466   }
467   if ( lastSp >= 0 )
468     input2 = input.mid( lastSp + 1 );
469
470   // detect a qualified name (with a point)
471   int lastPt = input2.lastIndexOf( "." );
472
473   if ( lastPt != -1 ) {
474     // split the 2 surrounding parts of the qualified name
475     myComplBeforePoint = input2.left( lastPt );
476     myComplAfterPoint = input2.mid( lastPt+1 );
477   }
478   else {
479     // no point found - do a global matching
480     // (the following will call dir() with an empty string)
481     myComplAfterPoint = input2;
482     myComplBeforePoint = "";
483   }
484
485   return new PyConsole_CompletionCommand( myInterp, myComplBeforePoint,
486                                           myComplAfterPoint, this, isSync() );
487 }
488
489 /*!
490   \brief Execute command in the python interpreter
491   and wait until it is finished.
492
493   \param command python command to be executed
494  */
495 void PyConsole_Editor::execAndWait( const QString& command )
496 {
497   // already running ?
498   if ( myEventLoop )
499     return;
500
501   // create new event loop
502   bool sync = isSync();
503   if ( !sync ) {
504     myEventLoop = new QEventLoop( this );
505   }
506
507   // execute command
508   exec( command );
509
510   if ( !sync ) {
511     // run event loop
512     myEventLoop->exec();
513     // delete event loop after command is processed
514     delete myEventLoop;
515     myEventLoop = 0;
516   }
517 }
518
519 /*!
520   \brief Process <Enter> key press event. 
521   
522   Execute the command entered by the user.
523 */
524 void PyConsole_Editor::handleReturn()
525 {
526   // Position cursor at the end
527   QTextCursor aCursor = textCursor();
528   aCursor.movePosition( QTextCursor::End );
529   setTextCursor( aCursor );
530
531   // get last line
532   QTextBlock par = document()->end().previous();
533   if ( !par.isValid() ) return;
534
535   // get command
536   QString cmd = par.text().remove( 0, promptSize() );
537
538   // extend the command buffer with the current command 
539   myCommandBuffer.append( cmd );
540
541   // add command to the history
542   if ( !cmd.trimmed().isEmpty() )
543     myHistory.push_back( cmd );
544   putLog( QString( "%1%2\n" ).arg( myPrompt ).arg( cmd ) );
545
546   // IPAL19397
547   addText( "", true ); 
548   
549   // set read-only mode
550   setReadOnly( true );
551
552   // set busy cursor
553   setCursor( Qt::BusyCursor );
554   
555   // post a request to execute Python command;
556   // editor will be informed via a custom event that execution has been completed
557   PyInterp_Dispatcher::Get()->Exec( createCmdRequest( myCommandBuffer ) );
558 }
559
560 /*!
561   \brief Process <Tab> key press event. 
562   
563   Perform auto-completion of the currently entered command, if this feature is enabled
564 */
565 void PyConsole_Editor::handleTab()
566 {
567   if ( !autoCompletion() )
568     return; // auto-completion feature is disabled
569
570   if ( myTabMode )
571     return; // already in tab mode
572
573   QTextCursor aCursor = textCursor();
574
575   // move cursor to the end of input
576   aCursor.movePosition( QTextCursor::End );
577   setTextCursor( aCursor );
578
579   // save cursor position if needed
580   if ( myComplCursorPos == -1 )
581     myComplCursorPos = textCursor().position();
582
583   // get last line
584   QTextBlock par = document()->end().previous();
585   if ( !par.isValid() ) return; // empty line
586
587   // switch to completion mode
588   myTabMode = true;
589
590   // get currently entered command
591   QString cmd = par.text().mid( promptSize() );
592
593   // post completion request
594   // editor will be informed that completion has been done via a custom event 
595   PyInterp_Dispatcher::Get()->Exec( createTabRequest( cmd ) );
596 }
597
598 /*!
599   \brief Process <Ctrl><Tab> key press event. 
600   
601   Undoe last auto-completion
602 */
603 void PyConsole_Editor::handleBackTab()
604 {
605   if ( !autoCompletion() )
606     return; // auto-completion feature is disabled
607
608   QTextCursor aCursor = textCursor();
609
610   if ( myComplCursorPos == -1 )
611     return; // invalid cursor position
612
613   // ensure cursor is at the end of command line
614   aCursor.setPosition( myComplCursorPos );
615   aCursor.movePosition( QTextCursor::EndOfLine );
616   //setCursor( aCursor );
617
618   // delete last completed text
619   int i = aCursor.position() - myComplCursorPos;
620   aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, i );
621   aCursor.removeSelectedText();
622   myComplCursorPos = -1;
623 }
624
625 /*!
626   \brief Process drop event.
627
628   Paste dragged text.
629   \param event drop event
630 */
631 void PyConsole_Editor::dropEvent( QDropEvent* event )
632 {
633   // get the initial drop position
634 #if QT_VERSION >= 0x060000
635   QPoint pos = event->position().toPoint();
636 #else
637   QPoint pos = event->pos();
638 #endif
639   QTextCursor aCursor = cursorForPosition( pos );
640
641   // if the position is not in the last line move it to the end of the command line
642   if ( aCursor.position() < document()->end().previous().position() + promptSize() ) {
643     moveCursor( QTextCursor::End );
644     pos = cursorRect().center();
645   }
646
647   // create new drop event and use it instead of the original
648   QDropEvent de( pos,
649                  event->possibleActions(),
650                  event->mimeData(),
651 #if QT_VERSION >= 0x060000
652                  event->buttons(),
653                  event->modifiers(),
654 #else
655                  event->mouseButtons(),
656                  event->keyboardModifiers(),
657 #endif
658                  event->type() );
659   QTextEdit::dropEvent( &de );
660
661   // accept the original event
662   event->acceptProposedAction();
663 }
664
665 /*!
666   \brief Process mouse press event
667
668   Clear the completion when any mouse button is pressed.
669
670   \param e mouse press event
671 */
672 void PyConsole_Editor::mousePressEvent( QMouseEvent* event )
673 {
674   if ( autoCompletion() ) {
675     clearCompletion();
676     myComplCursorPos = -1;
677   }
678   QTextEdit::mousePressEvent( event );
679 }
680
681 /*!
682   \brief Process mouse button release event.
683
684   Left mouse button: copy selection to the clipboard.
685   Middle mouse button: paste clipboard's contents.
686
687   \param event mouse event
688 */
689 void PyConsole_Editor::mouseReleaseEvent( QMouseEvent* event )
690 {
691   if ( event->button() == Qt::LeftButton ) {
692     QTextEdit::mouseReleaseEvent( event );
693   }
694   else if ( event->button() == Qt::MiddleButton ) {
695     QTextCursor aCursor = cursorForPosition( event->pos() );
696     // if the position is not in the last line move it to the end of the command line
697     if ( aCursor.position() < document()->end().previous().position() + promptSize() ) {
698       moveCursor( QTextCursor::End );
699     }
700     else {
701       setTextCursor( aCursor );
702     }
703     const QMimeData* md = QApplication::clipboard()->mimeData( QApplication::clipboard()->supportsSelection() ? 
704                                                                QClipboard::Selection : QClipboard::Clipboard );
705     if ( md )
706       insertFromMimeData( md );
707   }
708   else {
709     QTextEdit::mouseReleaseEvent( event );
710   }
711 }
712
713 /*!
714   \brief Check if the string is command.
715   
716   Return \c true if the string \a str is likely to be the command
717   (i.e. it is started from the '>>>' or '...').
718   \param str string to be checked
719 */
720 bool PyConsole_Editor::isCommand( const QString& str ) const
721 {
722   return str.startsWith( READY_PROMPT ) || str.startsWith( DOTS_PROMPT );
723 }
724
725 /*!
726   \brief Handle keyboard event.
727
728   Implement navigation, history browsing, copy/paste and other common
729   operations.
730
731   \param event keyboard event
732 */
733 void PyConsole_Editor::keyPressEvent( QKeyEvent* event )
734 {
735   // get cursor position
736   QTextCursor aCursor = textCursor();
737   int curLine = aCursor.blockNumber();
738   int curCol  = aCursor.columnNumber();
739
740   // get last edited line
741   int endLine = document()->blockCount()-1;
742
743   // get pressed key code
744   int aKey = event->key();
745
746   // check if <Ctrl> is pressed
747   bool ctrlPressed = event->modifiers() & Qt::ControlModifier;
748   // check if <Shift> is pressed
749   bool shftPressed = event->modifiers() & Qt::ShiftModifier;
750
751   if ( autoCompletion() ) {
752     // auto-completion support
753     if ( aKey == Qt::Key_Tab && !shftPressed ) {
754       // process <Tab> key
755       if ( !ctrlPressed ) {
756         handleTab();
757       }
758       else {
759         clearCompletion();
760         handleBackTab();
761       }
762       return;
763     }
764
765     // If <Ctrl> is not pressed (or if something else is pressed with <Ctrl>),
766     // or if <Ctrl> is not pressed alone, we have to clear completion
767     if ( !ctrlPressed || ( ctrlPressed && aKey != Qt::Key_Control ) ) {
768       clearCompletion();
769       myComplCursorPos = -1;
770     }
771     
772     // Discard <Ctrl> pressed alone:
773     if ( aKey == Qt::Key_Control )
774       return;
775   }
776
777   if ( aKey == Qt::Key_Escape || ( ctrlPressed && aKey == -1 ) ) {
778     // process <Ctrl>+<Break> key-binding and <Escape> key: clear current command
779     myCommandBuffer.truncate( 0 );
780     myPrompt = READY_PROMPT;
781     addText( myPrompt, true );
782     horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
783     return;
784   }
785   else if ( ctrlPressed && aKey == Qt::Key_C ) {
786     // process <Ctrl>+<C> key-binding : copy
787     copy();
788     return;
789   }
790   else if ( ctrlPressed && aKey == Qt::Key_X ) {
791     // process <Ctrl>+<X> key-binding : cut
792     cut();
793     return;
794   }
795   else if ( ctrlPressed && aKey == Qt::Key_V ) {
796     // process <Ctrl>+<V> key-binding : paste
797     paste();
798     return;
799   }
800
801   // check for printed key
802   // #### aKey = ( aKey < Qt::Key_Space || aKey > Qt::Key_ydiaeresis ) ? aKey : 0;
803   // Better:
804   aKey = !(QChar(aKey).isPrint()) ? aKey : 0;
805
806   switch ( aKey ) {
807   case 0 :
808     // any printed key: just print it
809     {
810       if ( curLine < endLine || curCol < promptSize() ) {
811         moveCursor( QTextCursor::End );
812       }
813       QTextEdit::keyPressEvent( event );
814       break;
815     }
816   case Qt::Key_Return:
817   case Qt::Key_Enter:
818     // <Enter> key: process the current command
819     {
820       handleReturn();
821       break;
822     }
823   case Qt::Key_Up:
824     // <Up> arrow key: process as follows:
825     // - without <Ctrl>, <Shift> modifiers: previous command in history
826     // - with <Ctrl> modifier key pressed:  move cursor one row up without selection
827     // - with <Shift> modifier key pressed: move cursor one row up with selection
828     // - with <Ctrl>+<Shift> modifier keys pressed: scroll one row up
829     {
830       if ( ctrlPressed && shftPressed ) {
831         int value   = verticalScrollBar()->value();
832         int spacing = fontMetrics().lineSpacing();
833         verticalScrollBar()->setValue( value > spacing ? value-spacing : 0 );
834       }
835       else if ( shftPressed || ctrlPressed ) {
836         if ( curLine > 0 )
837           moveCursor( QTextCursor::Up, 
838                       shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
839       }
840       else { 
841         if ( myCmdInHistory < 0 && myHistory.count() > 0 ) {
842           // set history browsing mode
843           myCmdInHistory = myHistory.count();
844           // remember current command
845           QTextBlock par = document()->end().previous();
846           myCurrentCommand = par.text().remove( 0, promptSize() );
847         }
848         if ( myCmdInHistory > 0 ) {
849           myCmdInHistory--;
850           // get previous command in the history
851           QString previousCommand = myHistory.at( myCmdInHistory );
852           // print previous command
853           moveCursor( QTextCursor::End );
854           moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
855           textCursor().removeSelectedText();
856           addText( myPrompt + previousCommand ); 
857           // move cursor to the end
858           moveCursor( QTextCursor::End );
859         }
860       }
861       break;
862     }
863   case Qt::Key_Down:
864     // <Down> arrow key: process as follows:
865     // - without <Ctrl>, <Shift> modifiers: next command in history
866     // - with <Ctrl> modifier key pressed:  move cursor one row down without selection
867     // - with <Shift> modifier key pressed: move cursor one row down with selection
868     // - with <Ctrl>+<Shift> modifier keys pressed: scroll one row down
869     {
870       if ( ctrlPressed && shftPressed ) {
871         int value   = verticalScrollBar()->value();
872         int maxval  = verticalScrollBar()->maximum();
873         int spacing = fontMetrics().lineSpacing();
874         verticalScrollBar()->setValue( value+spacing < maxval ? value+spacing : maxval );
875       }
876       else if ( shftPressed || ctrlPressed) {
877         if ( curLine < endLine )
878           moveCursor( QTextCursor::Down, 
879                       shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
880       }
881       else { 
882         if ( myCmdInHistory >= 0 ) {
883           // get next command in the history
884           myCmdInHistory++;
885           QString nextCommand;
886           if ( myCmdInHistory < myHistory.count() ) {
887             // next command in history
888             nextCommand = myHistory.at( myCmdInHistory );
889           }
890           else {
891             // end of history is reached
892             // last printed command
893             nextCommand = myCurrentCommand;
894             // unset history browsing mode
895             myCmdInHistory = -1;
896           }
897           // print next or current command
898           moveCursor( QTextCursor::End );
899           moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
900           textCursor().removeSelectedText();
901           addText( myPrompt + nextCommand );
902           // move cursor to the end
903           moveCursor( QTextCursor::End );
904         }
905       }
906       break;
907     }
908   case Qt::Key_Left:
909     // <Left> arrow key: process as follows:
910     // - without <Ctrl>, <Shift> modifiers: move one symbol left (taking into account prompt)
911     // - with <Ctrl> modifier key pressed:  move one word left (taking into account prompt)
912     // - with <Shift> modifier key pressed: move one symbol left with selection
913     // - with <Ctrl>+<Shift> modifier keys pressed: move one word left with selection
914     {
915       QString txt = textCursor().block().text();
916       if ( !shftPressed && isCommand( txt ) && curCol <= promptSize() ) {
917         moveCursor( QTextCursor::Up );
918         moveCursor( QTextCursor::EndOfBlock );
919       }
920       else {
921         QTextEdit::keyPressEvent( event );
922       }
923       break;
924     }
925   case Qt::Key_Right:
926     // <Right> arrow key: process as follows:
927     // - without <Ctrl>, <Shift> modifiers: move one symbol right (taking into account prompt)
928     // - with <Ctrl> modifier key pressed:  move one word right (taking into account prompt)
929     // - with <Shift> modifier key pressed: move one symbol right with selection
930     // - with <Ctrl>+<Shift> modifier keys pressed: move one word right with selection
931     {
932       QString txt = textCursor().block().text();
933       if ( !shftPressed ) {
934         if ( curCol < txt.length() ) {
935           if ( isCommand( txt ) && curCol < promptSize() ) {
936             aCursor.setPosition( aCursor.block().position() + promptSize() );
937             setTextCursor( aCursor );
938             break;
939           }
940         }
941         else {
942           if ( curLine < endLine && isCommand( textCursor().block().next().text() ) ) {
943             aCursor.setPosition( aCursor.position() + promptSize()+1 );
944             setTextCursor( aCursor );
945             horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
946             break;
947           }
948         }
949       }
950       QTextEdit::keyPressEvent( event );
951       break;
952     }
953   case Qt::Key_PageUp:
954     // <PageUp> key: process as follows:
955     // - without <Ctrl>, <Shift> modifiers: first command in history
956     // - with <Ctrl> modifier key pressed:  move cursor one page up without selection
957     // - with <Shift> modifier key pressed: move cursor one page up with selection
958     // - with <Ctrl>+<Shift> modifier keys pressed: scroll one page up
959     {
960       if ( ctrlPressed && shftPressed ) {
961         verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub);
962       }
963       else if ( shftPressed || ctrlPressed ) {
964         bool moved = false;
965         qreal lastY = cursorRect( aCursor ).top();
966         qreal distance = 0;
967         // move using movePosition to keep the cursor's x
968         do {
969           qreal y = cursorRect( aCursor ).top();
970           distance += qAbs( y - lastY );
971           lastY = y;
972           moved = aCursor.movePosition( QTextCursor::Up, 
973                                         shftPressed ? QTextCursor::KeepAnchor : 
974                                         QTextCursor::MoveAnchor );
975         } while ( moved && distance < viewport()->height() );
976         if ( moved ) {
977           aCursor.movePosition( QTextCursor::Down, 
978                                 shftPressed ? QTextCursor::KeepAnchor : 
979                                 QTextCursor::MoveAnchor );
980           verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub );
981         }
982         setTextCursor( aCursor );
983       }
984       else { 
985         if ( myCmdInHistory < 0 && myHistory.count() > 0 ) {
986           // set history browsing mode
987           myCmdInHistory = myHistory.count();
988           // remember current command
989           QTextBlock par = document()->end().previous();
990           myCurrentCommand = par.text().remove( 0, promptSize() );
991         }
992         if ( myCmdInHistory > 0 ) {
993           myCmdInHistory = 0;
994           // get very first command in the history
995           QString firstCommand = myHistory.at( myCmdInHistory );
996           // print first command
997           moveCursor( QTextCursor::End );
998           moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
999           textCursor().removeSelectedText();
1000           addText( myPrompt + firstCommand ); 
1001           // move cursor to the end
1002           moveCursor( QTextCursor::End );
1003         }
1004       }
1005       break;
1006     }
1007   case Qt::Key_PageDown:
1008     // <PageDown> key: process as follows:
1009     // - without <Ctrl>, <Shift> modifiers: last command in history
1010     // - with <Ctrl> modifier key pressed:  move cursor one page down without selection
1011     // - with <Shift> modifier key pressed: move cursor one page down with selection
1012     // - with <Ctrl>+<Shift> modifier keys pressed: scroll one page down
1013     {
1014       if ( ctrlPressed && shftPressed ) {
1015         verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd);
1016       }
1017       else if ( shftPressed || ctrlPressed ) {
1018         bool moved = false;
1019         qreal lastY = cursorRect( aCursor ).top();
1020         qreal distance = 0;
1021         // move using movePosition to keep the cursor's x
1022         do {
1023           qreal y = cursorRect( aCursor ).top();
1024           distance += qAbs( y - lastY );
1025           lastY = y;
1026           moved = aCursor.movePosition( QTextCursor::Down, 
1027                                         shftPressed ? QTextCursor::KeepAnchor : 
1028                                         QTextCursor::MoveAnchor );
1029         } while ( moved && distance < viewport()->height() );
1030         if ( moved ) {
1031           aCursor.movePosition( QTextCursor::Up, 
1032                                 shftPressed ? QTextCursor::KeepAnchor : 
1033                                 QTextCursor::MoveAnchor );
1034           verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub );
1035         }
1036         setTextCursor( aCursor );
1037       }
1038       else { 
1039         if ( myCmdInHistory >= 0 ) {
1040           // unset history browsing mode
1041           myCmdInHistory = -1;
1042           // print current command
1043           moveCursor( QTextCursor::End );
1044           moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
1045           textCursor().removeSelectedText();
1046           addText( myPrompt + myCurrentCommand ); 
1047           // move cursor to the end
1048           moveCursor( QTextCursor::End );
1049         }
1050       }
1051       break;
1052     }
1053   case Qt::Key_Home: 
1054     // <Home> key: process as follows:
1055     // - without <Ctrl>, <Shift> modifiers: move cursor to the beginning of the current line without selection
1056     // - with <Ctrl> modifier key pressed:  move cursor to the very first symbol without selection
1057     // - with <Shift> modifier key pressed: move cursor to the beginning of the current line with selection
1058     // - with <Ctrl>+<Shift> modifier keys pressed: move cursor to the very first symbol with selection
1059     {
1060       if ( ctrlPressed ) { 
1061         moveCursor( QTextCursor::Start, 
1062                     shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1063       }
1064       else {
1065         QString txt = textCursor().block().text();
1066         if ( isCommand( txt ) ) {
1067           if ( shftPressed ) {
1068             if ( curCol > promptSize() ) {
1069               aCursor.movePosition( QTextCursor::StartOfLine, QTextCursor::KeepAnchor );
1070               aCursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, promptSize() );
1071             }
1072           }
1073           else {
1074             aCursor.movePosition( QTextCursor::StartOfLine );
1075             aCursor.movePosition( QTextCursor::Right, QTextCursor::MoveAnchor, promptSize() );
1076           }
1077           setTextCursor( aCursor );
1078         }
1079         else {
1080           moveCursor( QTextCursor::StartOfBlock, 
1081                       shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1082         }
1083         horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1084       }
1085       break;
1086     }
1087   case Qt::Key_End:
1088     // <End> key: process as follows:
1089     // - without <Ctrl>, <Shift> modifiers: move cursor to the end of the current line without selection
1090     // - with <Ctrl> modifier key pressed:  move cursor to the very last symbol without selection
1091     // - with <Shift> modifier key pressed: move cursor to the end of the current line with selection
1092     // - with <Ctrl>+<Shift> modifier keys pressed: move cursor to the very last symbol with selection
1093     {
1094       moveCursor( ctrlPressed ? QTextCursor::End : QTextCursor::EndOfBlock, 
1095                   shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
1096       break;
1097     }  
1098   case Qt::Key_Backspace :
1099     // <Backspace> key: process as follows
1100     // - without any modifiers : delete symbol before the cursor / selection (taking into account prompt)
1101     // - with <Shift> modifier key pressed: delete previous word
1102     // - with <Ctrl> modifier key pressed: delete text from the cursor to the line beginning
1103     // works only for last (command) line
1104     {
1105       if ( aCursor.hasSelection() ) {
1106         cut();
1107       }
1108       else if ( aCursor.position() > document()->end().previous().position() + promptSize() ) {
1109         if ( shftPressed ) {
1110           moveCursor( QTextCursor::PreviousWord, QTextCursor::KeepAnchor );
1111           textCursor().removeSelectedText();
1112         }
1113         else if ( ctrlPressed ) {
1114           aCursor.setPosition( document()->end().previous().position() + promptSize(),
1115                                QTextCursor::KeepAnchor );
1116           setTextCursor( aCursor );
1117           textCursor().removeSelectedText();
1118         }
1119         else {
1120           QTextEdit::keyPressEvent( event );
1121         }
1122       }
1123       else {
1124         aCursor.setPosition( document()->end().previous().position() + promptSize() );
1125         setTextCursor( aCursor );
1126         horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1127       }
1128       break;
1129     }
1130   case Qt::Key_Delete :
1131     // <Delete> key: process as follows
1132     // - without any modifiers : delete symbol after the cursor / selection (taking into account prompt)
1133     // - with <Shift> modifier key pressed: delete next word
1134     // - with <Ctrl> modifier key pressed: delete text from the cursor to the end of line
1135     // works only for last (command) line
1136     {
1137       if ( aCursor.hasSelection() ) {
1138         cut();
1139       }
1140       else if ( aCursor.position() > document()->end().previous().position() + promptSize()-1 ) {
1141         if ( shftPressed ) {
1142           moveCursor( QTextCursor::NextWord, QTextCursor::KeepAnchor );
1143           textCursor().removeSelectedText();
1144         }
1145         else if ( ctrlPressed ) {
1146           moveCursor( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
1147           textCursor().removeSelectedText();
1148         }
1149         else {
1150           QTextEdit::keyPressEvent( event );
1151         }
1152       }
1153       else {
1154         aCursor.setPosition( document()->end().previous().position() + promptSize() );
1155         setTextCursor( aCursor );
1156         horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1157       }
1158       break;
1159     }
1160   case Qt::Key_Insert :
1161     // <Insert> key: process as follows
1162     // - with <Ctrl> modifier key pressed:  copy()
1163     // - with <Shift> modifier key pressed: paste() to the command line
1164     {
1165       if ( ctrlPressed ) {
1166         copy();
1167       }
1168       else if ( shftPressed ) {
1169         paste();
1170       }
1171       else
1172         QTextEdit::keyPressEvent( event );
1173       break;
1174     }
1175   default:
1176     break;
1177   }
1178 }
1179
1180 /*!
1181   \brief Handle notification event coming from Python dispatcher.
1182   \param event notification event
1183 */
1184 void PyConsole_Editor::customEvent( QEvent* event )
1185 {
1186   switch( (int) event->type() )
1187   {
1188   case PyConsole_PrintEvent::EVENT_ID:
1189   {
1190     PyConsole_PrintEvent* pe = (PyConsole_PrintEvent*)event;
1191     addText( pe->text(), false, pe->isError() );
1192     return;
1193   }
1194   case PyConsole_CompletionEvent::EVENT_ID:
1195   {
1196     PyConsole_CompletionEvent* ce = (PyConsole_CompletionEvent*)event;
1197     bool status = ce->status();
1198     QStringList matches = ce->matches();
1199     QString doc = ce->doc();
1200
1201     if ( status ) {
1202       // completion was successful
1203       QTextCursor aCursor = textCursor();
1204
1205       if ( matches.isEmpty() ) {
1206         // completion successful but there are no matches.
1207         myTabMode = false;
1208         myComplCursorPos = -1;
1209         return;
1210       }
1211       
1212       if ( matches.size() == 1 ) {
1213         // there's only one match - complete directly and update doc string window
1214         aCursor.insertText( matches[0].mid( myComplAfterPoint.size() ) );
1215         myTabMode = false;
1216         if ( doc.isEmpty() )
1217           emit updateDoc( formatDocHTML( QString( "(%1)\n" ).arg( tr( "NO_DOC_AVAILABLE" ) ) ) );
1218         else
1219           emit updateDoc( formatDocHTML( doc ) );
1220       }
1221       else {
1222         // there are several matches
1223         
1224         // detect if there is a common base to all available completion
1225         // in this case append this base to the text
1226         QString base = extractCommon( matches );
1227         aCursor.insertText( base.mid( myComplAfterPoint.size() ) );
1228         
1229         // if this happens to match exactly the first completion
1230         // also provide doc
1231         if ( base == matches[0] )
1232           emit updateDoc( formatDocHTML( doc ) );
1233         
1234         // print all matching completion in a "undo-able" block
1235         int cursorPos = aCursor.position();
1236         aCursor.insertBlock();
1237         aCursor.beginEditBlock();
1238         
1239         // insert all matches
1240         QTextCharFormat cf;
1241         cf.setForeground( QBrush( Qt::darkGreen ) );
1242         aCursor.setCharFormat( cf );
1243         aCursor.insertText( formatCompletion( matches ) );
1244         aCursor.endEditBlock();
1245         
1246         // position cursor where it was before inserting the completion list
1247         aCursor.setPosition( cursorPos );
1248         setTextCursor( aCursor );
1249       }
1250     }
1251     else {
1252       // completion failed
1253       myTabMode = false;
1254       myComplCursorPos = -1;
1255     }
1256     return;
1257   }
1258   case PyInterp_Event::ES_OK:
1259   case PyInterp_Event::ES_ERROR:
1260   {
1261     // clear command buffer
1262     myCommandBuffer.truncate( 0 );
1263     // add caret return line if necessary
1264     QTextBlock par = document()->end().previous();
1265     QString txt = par.text();
1266     txt.truncate( txt.length() - 1 );
1267     // IPAL19397 : addText moved to handleReturn() method
1268     //if ( !txt.isEmpty() )
1269     //  addText( "", true );
1270     // set "ready" prompt
1271     myPrompt = READY_PROMPT;
1272     addText( myPrompt );
1273     // unset busy cursor
1274     unsetCursor();
1275     // stop event loop (if running)
1276     if ( myEventLoop )
1277       myEventLoop->exit();
1278     // if we are in multi_paste_mode, process the next item
1279     multiLineProcessNextLine();
1280     break;
1281   }
1282   case PyInterp_Event::ES_INCOMPLETE:
1283   {
1284     // extend command buffer (multi-line command)
1285     myCommandBuffer.append( "\n" );
1286     // add caret return line if necessary
1287     QTextBlock par = document()->end().previous();
1288     QString txt = par.text();
1289     txt.truncate( txt.length() - 1 );
1290     // IPAL19397 : addText moved to handleReturn() method
1291     //if ( !txt.isEmpty() )
1292     //  addText( "", true );
1293     // set "dot" prompt
1294     myPrompt = DOTS_PROMPT;
1295     addText( myPrompt/*, true*/ ); // IPAL19397
1296     // unset busy cursor
1297     unsetCursor();
1298     // stop event loop (if running)
1299     if ( myEventLoop )
1300       myEventLoop->exit();
1301     // if we are in multi_paste_mode, process the next item
1302     multiLineProcessNextLine();
1303     break;
1304   }
1305   default:
1306     QTextEdit::customEvent( event );
1307   }
1308   
1309   // unset read-only state
1310   setReadOnly( false );
1311   // unset history browsing mode
1312   myCmdInHistory = -1;
1313
1314   if ( (int)event->type() == (int)PyInterp_Event::ES_OK && myQueue.count() > 0 )
1315   {
1316     // process the next scheduled command from the queue (if there is any)
1317     QString nextcmd = myQueue[0];
1318     myQueue.pop_front();
1319     exec( nextcmd );
1320   }
1321 }
1322
1323 /*!
1324   \brief "Copy" operation.
1325   
1326   Reimplemented from Qt.
1327   Warning! In Qt this method is not virtual.
1328 */
1329 void PyConsole_Editor::cut()
1330 {
1331   QTextCursor aCursor = textCursor();
1332   if ( aCursor.hasSelection() ) {
1333     QApplication::clipboard()->setText( aCursor.selectedText() );
1334     int startSelection = aCursor.selectionStart();
1335     if ( startSelection < document()->end().previous().position() + promptSize() )
1336       startSelection = document()->end().previous().position() + promptSize();
1337     int endSelection = aCursor.selectionEnd();
1338     if ( endSelection < document()->end().previous().position() + promptSize() )
1339       endSelection = document()->end().previous().position() + promptSize();
1340     aCursor.setPosition( startSelection );
1341     aCursor.setPosition( endSelection, QTextCursor::KeepAnchor );
1342     horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1343     setTextCursor( aCursor );
1344     textCursor().removeSelectedText();
1345   }
1346 }
1347
1348 /*!
1349   \brief "Paste" operation.
1350
1351   Reimplemented from Qt.
1352   Warning! In Qt this method is not virtual.
1353 */
1354 void PyConsole_Editor::paste()
1355 {
1356   QTextCursor aCursor = textCursor();
1357   if ( aCursor.hasSelection() ) {
1358     int startSelection = aCursor.selectionStart();
1359     if ( startSelection < document()->end().previous().position() + promptSize() )
1360       startSelection = document()->end().previous().position() + promptSize();
1361     int endSelection = aCursor.selectionEnd();
1362     if ( endSelection < document()->end().previous().position() + promptSize() )
1363       endSelection = document()->end().previous().position() + promptSize();
1364     aCursor.setPosition( startSelection );
1365     aCursor.setPosition( endSelection, QTextCursor::KeepAnchor );
1366     horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1367     setTextCursor( aCursor );
1368     textCursor().removeSelectedText();
1369   }
1370   if ( textCursor().position() < document()->end().previous().position() + promptSize() )
1371     moveCursor( QTextCursor::End );
1372   QTextEdit::paste();
1373 }
1374
1375 /*!
1376   \brief "Clear" operation.
1377
1378   Reimplemented from Qt.
1379   Warning! In Qt this method is not virtual.
1380 */
1381 void PyConsole_Editor::clear()
1382 {
1383   QTextEdit::clear();
1384   if ( isShowBanner() )
1385     addText( banner() );
1386   myPrompt = READY_PROMPT;
1387   addText( myPrompt );
1388 }
1389
1390 /*!
1391   \brief Dumps recorded Python commands to the file
1392   \param fileName path to the dump file
1393   \return \c true if dump operation succeeded or \c false otherwise
1394 */
1395 bool PyConsole_Editor::dump( const QString& fileName )
1396 {
1397   bool ok = false;
1398   if ( !fileName.isEmpty() ) {
1399     QFile file( fileName ); 
1400     if ( file.open( QFile::WriteOnly ) ) {
1401       QTextStream out( &file );
1402       for ( int i = 0; i < myHistory.count(); i++ ) {
1403 #if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
1404         out << myHistory[i] << Qt::endl;
1405 #else
1406         out << myHistory[i] << endl;
1407 #endif
1408       }
1409       file.close();
1410       ok = true;
1411     }
1412   }
1413   return ok;
1414 }
1415
1416 /*!
1417   \brief Dump menu action slot
1418 */
1419 void PyConsole_Editor::dump()
1420 {
1421   forever {
1422     // get file name
1423     QString fileName = getDumpFileName();
1424
1425     if ( fileName.isEmpty() )
1426       break; // cancelled
1427     
1428     if ( dump( fileName ) )
1429       break;
1430     else
1431       QMessageBox::warning( this,
1432                             tr( "WARNING" ),
1433                             tr( "ERR_FILE_NOT_WRITEABLE" ) );
1434   }
1435 }
1436
1437 /*!
1438   \brief Get file name for Dump commands operation.
1439   
1440   This function can be redefined in successor classes to show application
1441   specific dialog box.
1442
1443   \return path to the dump file
1444 */
1445 QString PyConsole_Editor::getDumpFileName()
1446 {
1447   return QFileDialog::getSaveFileName( this,
1448                                        tr( "GET_DUMP_COMMANDS_FILENAME" ),
1449                                        QString(),
1450                                        QString( "%1 (*.py)" ).arg( tr( "PYTHON_SCRIPTS" ) ) );
1451 }
1452
1453 /*!
1454   \brief Get file name for Log Python trace operation.
1455   
1456   This function can be redefined in successor classes to show application
1457   specific dialog box.
1458
1459   \return path to the log file
1460 */
1461 QString PyConsole_Editor::getLogFileName()
1462 {
1463   return QFileDialog::getSaveFileName( this,
1464                                        tr( "GET_PYTHON_TRACE_FILENAME" ),
1465                                        QString(),
1466                                        QString( "%1 (*.log *.txt)" ).arg( tr( "LOG_FILES" ) ) );
1467 }
1468
1469 /*!
1470   \brief Start python trace logging
1471   \param fileName the path to the log file
1472   \return \c true if operation succeeded or \c false otherwise
1473           (for example, if file is not writeable)
1474   \sa stopLog()
1475  */
1476 bool PyConsole_Editor::startLog( const QString& fileName )
1477 {
1478   // stop possibly already running logging
1479   if ( isLogging() )
1480     stopLog();
1481
1482   bool ok = false;
1483   if ( !fileName.isEmpty() ) {
1484     QFile file( fileName );
1485     if ( file.open( QFile::WriteOnly ) ) {
1486       file.close();
1487       myLogFile = fileName;
1488       ok = true;
1489     }
1490   }
1491   return ok;
1492 }
1493
1494 /*!
1495   \brief Start log action slot
1496 */
1497 void PyConsole_Editor::startLog()
1498 {
1499   forever {
1500     // get file name
1501     QString fileName = getLogFileName();
1502
1503     if ( fileName.isEmpty() )
1504       break; // cancelled
1505     
1506     if ( startLog( fileName ) )
1507       break;
1508     else
1509       QMessageBox::warning( this,
1510                             tr( "WARNING" ),
1511                             tr( "File is not writable" ) );
1512   }
1513 }
1514
1515 /*!
1516   \brief Stop log action slot
1517   
1518   Stops Python trace logging.
1519 */
1520 void PyConsole_Editor::stopLog()
1521 {
1522   myLogFile = QString();
1523 }
1524
1525 /*!
1526   \brief Put data to the log file
1527 */
1528 void PyConsole_Editor::putLog( const QString& s )
1529 {
1530   if ( !myLogFile.isEmpty() ) {
1531     QFile file( myLogFile );
1532     if ( !file.open( QFile::Append ) )
1533       return;
1534     
1535     QTextStream out( &file );
1536     out << s;
1537     
1538     file.close();
1539   }
1540 }
1541
1542 /*!
1543   \brief Handle properly multi-line pasting. Qt documentation recommends overriding this function.
1544   If the pasted text doesn't contain a line return, no special treatment is done.
1545   \param source
1546 */
1547 void PyConsole_Editor::insertFromMimeData(const QMimeData* source)
1548 {
1549   if ( myMultiLinePaste )
1550     return;
1551
1552   if ( source->hasText() ) {
1553     QString s = source->text();
1554     if ( s.contains( "\n" ) )
1555       multilinePaste( s );
1556     else
1557       QTextEdit::insertFromMimeData( source );
1558   }
1559   else {
1560     QTextEdit::insertFromMimeData( source );
1561   }
1562 }
1563
1564 /*!
1565   Start multi-line paste operation
1566   \internal
1567 */
1568 void PyConsole_Editor::multilinePaste( const QString& s )
1569 {
1570   // Turn on multi line pasting mode
1571   myMultiLinePaste = true;
1572
1573   // Split string data to lines
1574   QString s2 = s;
1575   s2.replace( "\r", "" ); // Windows string format converted to Unix style
1576 #if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
1577   QStringList lst = s2.split( QChar('\n'), Qt::KeepEmptyParts );
1578 #else
1579   QStringList lst = s2.split( QChar('\n'), QString::KeepEmptyParts );
1580 #endif
1581
1582   // Perform the proper paste operation for the first line to handle the case where
1583   // something was already there
1584   QMimeData source;
1585   source.setText( lst[0] );
1586   QTextEdit::insertFromMimeData( &source );
1587
1588   // Prepare what will have to be executed after the first line
1589   myMultiLineContent.clear();
1590   for ( int i = 1; i < lst.size(); ++i )
1591     myMultiLineContent.enqueue( lst[i] );
1592
1593   // Trigger the execution of the first (mixed) line
1594   handleReturn();
1595
1596   // See customEvent() and multiLineProcessNext() for the rest of the handling.
1597 }
1598
1599 /*!
1600   \brief Process the next line in the queue of multi-line paste operation; called
1601   from customEvent() function
1602   \internal
1603 */
1604 void PyConsole_Editor::multiLineProcessNextLine()
1605 {
1606   // not in multi-line paste mode
1607   if ( !myMultiLinePaste || myMultiLineContent.isEmpty() )
1608     return;
1609
1610   QString line = myMultiLineContent.dequeue();
1611   if ( myMultiLineContent.empty() )
1612   {
1613     // this isa last line in the queue, just paste it
1614     addText( line, false, false );
1615     myMultiLinePaste = false;
1616   }
1617   else
1618   {
1619     // paste the line and simulate a <RETURN> key stroke
1620     addText( line, false, false );
1621     handleReturn();
1622   }
1623 }
1624
1625 /*!
1626   \brief Clear results of completion
1627 */
1628 void PyConsole_Editor::clearCompletion()
1629 {
1630   // delete completion text if present
1631   if ( myTabMode ) {
1632     // remove completion display
1633     document()->undo();
1634     // remove trailing line return:
1635     QTextCursor tc( textCursor() );
1636     tc.setPosition( document()->characterCount()-1 );
1637     setTextCursor( tc );
1638     textCursor().deletePreviousChar();
1639     // TODO: before wait for any <Tab> event to be completed
1640   }
1641   myTabMode = false;
1642 }
1643
1644 /*!
1645  \brief Format completion results - this is where we should create 3 columns etc ...
1646  \param matches list of possible completions
1647  \return result string
1648 */
1649 QString PyConsole_Editor::formatCompletion( const QStringList& matches ) const
1650 {
1651   static const int MAX_COMPLETIONS = 70;
1652
1653   QStringList result;
1654   int sz = matches.size();
1655
1656   if ( sz > MAX_COMPLETIONS )
1657     result.append( QString( "[%1]" ).arg( tr( "TOO_MANY_MATCHES" ) ) );
1658
1659   for ( int i = 0; i < qMin( sz, MAX_COMPLETIONS); ++i )
1660     result.append( matches[i] );
1661
1662   return result.join( "\n" );
1663 }
1664
1665 /*!
1666   \brief Format the doc string in HTML format with the first line in bold blue
1667   \param doc initial doc string
1668   \return HTML string
1669 */
1670 QString PyConsole_Editor::formatDocHTML( const QString& doc ) const
1671 {
1672   static const char* templ = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" " \
1673     "\"http://www.w3.org/TR/REC-html40/strict.dtd\">\n" \
1674     "<html><head><meta name=\"qrichtext\" content=\"1\" /> " \
1675     "<style type=\"text/css\">\np, li { white-space: pre-wrap; }\n</style> " \
1676     "</head><body style=\" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;\">\n" \
1677     "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"> " \
1678     "<span style=\" font-weight:600; color:#0000ff;\">%1</span></p> " \
1679     "<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">%2</p> " \
1680     "</body></html>";
1681   
1682   QString fst, rest("");
1683
1684   // Extract first line of doc
1685   int idx = doc.indexOf( "\n" );
1686   if ( idx > 0 ) {
1687     fst = doc.left( idx );
1688     rest = doc.mid( idx+1 );
1689   }
1690   else {
1691     fst = doc;
1692   }
1693
1694   fst = fst.replace( "\n", " " );
1695   rest = rest.replace(" \n", " " );
1696   return QString( templ ).arg( fst ).arg( rest );
1697 }
1698
1699 /*!
1700   \fn void PyConsole_Editor::updateDoc( const QString& doc);
1701   \brief Signal emitted by the editor widget when the doc string should be updated.
1702   \param doc a HTML block with the formatted doc string.
1703   \todo currently this signal is left uncaught.
1704 */
1705
1706 /*!
1707  \brief Extract the common leading part of all strings in matches.
1708  \param matches
1709  \param result
1710 */
1711 QString PyConsole_Editor::extractCommon( const QStringList& matches ) const
1712 {
1713   QString result = "";
1714   
1715   if ( matches.size() > 1 ) {
1716     int l = 0;
1717     bool ok = true;
1718     while ( ok && l+1 < matches[0].size() ) {
1719       QString match = matches[0].left( l+1 );
1720       for ( int j = 1; j < matches.size() && ok; j++ )
1721         ok = matches[j].startsWith( match );
1722       if ( ok )
1723         l++;
1724     }
1725     result = matches[0].left( l );
1726   }
1727
1728   return result;
1729 }
1730
1731 /*!
1732   \brief Useful method to get banner from Python interpreter
1733   \return banner
1734 */
1735 QString PyConsole_Editor::banner() const
1736 {
1737   return myInterp->getBanner().c_str();
1738 }