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