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