Salome HOME
0022652: [CEA 1194] Redirect the traces from embedded Python console in a log file
[modules/gui.git] / src / PyConsole / PyConsole_Editor.cxx
1 // Copyright (C) 2007-2014  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
23 //  SALOME SALOMEGUI : implementation of desktop and GUI kernel
24 // File   : PyConsole_Editor.cxx
25 // Author : Vadim SANDLER, Open CASCADE S.A.S. (vadim.sandler@opencascade.com)
26 //
27 /*!
28   \class PyConsole_Editor
29   \brief Python command line interpreter front-end GUI widget.
30   
31   This class provides simple GUI interface to the Python interpreter, including basic 
32   navigation operations, executing commands (both interactively and programmatically), 
33   copy-paste operations, history of the commands and so on.
34
35   Here below is the shortcut keyboard boundings used for navigation and other operations:
36   - <Enter>              : execute current command
37   - <Ctrl><Break>        : clear current command
38   - <Escape>             : clear current command
39   - <Up>                 : previous command in the history
40   - <Shift><Up>          : move cursor one row up with selection
41   - <Ctrl><Up>           : move cursor one row up without selection
42   - <Ctrl><Shift><Up>    : move cursor one row up with selection
43   - <Down>               : next command in the history
44   - <Shift><Down>        : move cursor one row down with selection
45   - <Ctrl><Down>         : move cursor one row down without selection
46   - <Ctrl><Shift><Down>  : move cursor one row down with selection
47   - <Left>               : move one symbol left without selection
48   - <Shift><Left>        : move one symbol left with selection
49   - <Ctrl><Left>         : move one word left without selection
50   - <Ctrl><Shift><Left>  : move one word left with selection
51   - <Right>              : move one symbol right without selection
52   - <Shift><Right>       : move one symbol right with selection
53   - <Ctrl><Right>        : move one word right without selection
54   - <Ctrl><Shift><Right> : move one word right with selection
55   - <PgUp>               : first command in the history
56   - <Shift><PgUp>        : move one page up with selection
57   - <Ctrl><PgUp>         : move one page up without selection
58   - <Ctrl><Shift><PgUp>  : scroll one page up
59   - <PgDn>               : last command in the history
60   - <Shift><PgDn>        : move one page down with selection
61   - <Ctrl><PgDn>         : move one page down without selection
62   - <Ctrl><Shift><PgDn>  : scroll one page down
63   - <Home>               : move to the beginning of the line without selection
64   - <Shift><Home>        : move to the beginning of the line with selection
65   - <Ctrl><Home>         : move to the very first symbol without selection
66   - <Ctrl><Shift><Home>  : move to the very first symbol with selection
67   - <End>                : move to the end of the line without selection
68   - <Shift><End>         : move to the end of the line with selection
69   - <Ctrl><End>          : move to the very last symbol without selection
70   - <Ctrl><Shift><End>   : move to the very last symbol with selection
71   - <Backspace>          : delete symbol before the cursor
72                            / remove selected text and put it to the clipboard (cut)
73   - <Shift><Backspace>   : delete previous word
74                            / remove selected text and put it to the clipboard (cut)
75   - <Ctrl><Backspace>    : delete text from the cursor to the beginning of the line 
76                            / remove selected text and put it to the clipboard (cut)
77   - <Delete>             : delete symbol after the cursor 
78                            / remove selected text and put it to the clipboard (cut)
79   - <Shift><Delete>      : delete next word
80                            / remove selected text and put it to the clipboard (cut)
81   - <Ctrl><Delete>       : delete text from the cursor to the end of the line
82                            / remove selected text and put it to the clipboard (cut)
83   - <Ctrl><Insert>       : copy
84   - <Shift><Insert>      : paste
85   - <Ctrl><V>            : paste
86   - <Ctrl><C>            : copy
87   - <Ctrl><X>            : cut
88   - <Ctrl><V>            : paste
89 */
90
91 #include "PyConsole_Interp.h"   // !!! WARNING !!! THIS INCLUDE MUST BE THE VERY FIRST !!!
92 #include "PyConsole_Editor.h"
93 #include "PyConsole_Event.h"
94 #include "PyInterp_Event.h"
95 #include "PyInterp_Dispatcher.h"
96 #include "PyConsole_Request.h"
97
98 #include <SUIT_Tools.h>
99 #include <SUIT_FileDlg.h>
100 #include <SUIT_MessageBox.h>
101 #include <SUIT_FileValidator.h>
102
103 #include <QApplication>
104 #include <QClipboard>
105 #include <QDropEvent>
106 #include <QEvent>
107 #include <QKeyEvent>
108 #include <QMouseEvent>
109 #include <QScrollBar>
110 #include <QTextBlock>
111 #include <QTextCursor>
112 #include <QTextDocument>
113 #include <QTextStream>
114 #include <QChar>
115
116 static QString READY_PROMPT = ">>> ";
117 static QString DOTS_PROMPT  = "... ";
118
119 class DumpCommandsFileValidator : public SUIT_FileValidator
120 {
121  public:
122   DumpCommandsFileValidator( QWidget* parent = 0 ) : SUIT_FileValidator ( parent ) {};
123   virtual ~DumpCommandsFileValidator() {};
124   virtual bool canSave( const QString& file, bool permissions );
125 };
126
127 bool DumpCommandsFileValidator::canSave(const QString& file, bool permissions)
128 {
129   QFileInfo fi( file );
130   if ( !QRegExp( "[A-Za-z_][A-Za-z0-9_]*" ).exactMatch( fi.completeBaseName() ) ) {
131     SUIT_MessageBox::critical( parent(),
132                                QObject::tr("WRN_WARNING"),
133                                QObject::tr("WRN_FILE_NAME_BAD") );
134     return false;
135   }
136   return SUIT_FileValidator::canSave( file, permissions);
137 }
138
139 void staticCallbackStdout( void* data, char* c )
140 {
141   if(!((PyConsole_Editor*)data)->isSuppressOutput())
142     QApplication::postEvent( (PyConsole_Editor*)data, new PrintEvent( QString::fromUtf8(c), false ) );
143 }
144
145 void staticCallbackStderr( void* data, char* c )
146 {
147   if(!((PyConsole_Editor*)data)->isSuppressOutput())
148     QApplication::postEvent( (PyConsole_Editor*)data, new PrintEvent( QString::fromUtf8(c), true ) );
149 }
150
151
152 /*!
153   \brief Constructor. 
154   
155   Creates python editor window.
156   \param theInterp python interper
157   \param theParent parent widget
158 */
159 PyConsole_Editor::PyConsole_Editor( PyConsole_Interp* theInterp, 
160                                     QWidget*          theParent )
161 : QTextEdit( theParent ),
162   myInterp( 0 ),
163   myCmdInHistory( -1 ),
164   myEventLoop( 0 ),
165   myShowBanner( true ),
166   myIsSync( false ),
167   myIsSuppressOutput( false )
168 {
169   QString fntSet( "" );
170   QFont aFont = SUIT_Tools::stringToFont( fntSet );
171   setFont( aFont );
172   setUndoRedoEnabled( false );
173
174   myPrompt = READY_PROMPT;
175   setLineWrapMode( QTextEdit::WidgetWidth );
176   setWordWrapMode( QTextOption::WrapAnywhere );
177   setAcceptRichText( false );
178
179   theInterp->setvoutcb( staticCallbackStdout, this );
180   theInterp->setverrcb( staticCallbackStderr, this );
181
182   // san - This is necessary for troubleless initialization
183   onPyInterpChanged( theInterp );
184 }
185
186 /*!
187   \brief Destructor.
188
189   Does nothing for the moment.
190 */
191 PyConsole_Editor::~PyConsole_Editor()
192 {
193   myInterp->destroy();
194   delete myInterp;
195   myInterp = 0;
196 }
197
198 /*!
199   \brief Get synchronous mode flag value.
200   
201   \sa setIsSync()
202   \return True if python console works in synchronous mode
203 */
204 bool PyConsole_Editor::isSync() const
205 {
206   return myIsSync;
207 }
208
209 /*!
210   \brief Set synchronous mode flag value.
211
212   In synhronous mode the Python commands are executed in the GUI thread
213   and the GUI is blocked until the command is finished. In the asynchronous
214   mode each Python command is executed in the separate thread that does not
215   block the main GUI loop.
216
217   \param on synhronous mode flag
218 */
219 void PyConsole_Editor::setIsSync( const bool on )
220 {
221   myIsSync = on;
222 }
223
224 /*!
225   \brief Get suppress output flag value.
226   
227   \sa setIsSuppressOutput()
228   \return \c true if python console output is suppressed.
229 */
230 bool PyConsole_Editor::isSuppressOutput() const
231 {
232   return myIsSuppressOutput;
233 }
234
235 /*!
236   \brief Set suppress output flag value.
237
238   In case if suppress output flag is true, the python 
239   console output suppressed.
240
241   \param on suppress output flag
242 */
243 void PyConsole_Editor::setIsSuppressOutput( const bool on )
244 {
245   myIsSuppressOutput = on;
246 }
247
248 /*!
249   \brief Get 'show banner' flag value.
250   
251   \sa setIsShowBanner()
252   \return \c true if python console shows banner
253 */
254 bool PyConsole_Editor::isShowBanner() const
255 {
256   return myShowBanner;
257 }
258
259 /*!
260   \brief Set 'show banner' flag value.
261
262   The banner is shown in the top of the python console window.
263
264   \sa isShowBanner()
265   \param on 'show banner' flag
266 */
267 void PyConsole_Editor::setIsShowBanner( const bool on )
268 {
269   if ( myShowBanner != on ) {
270     myShowBanner = on;
271     clear();
272   }
273 }
274
275 /*!
276   \brief Get size hint for the Python console window
277   \return size hint value
278 */
279 QSize PyConsole_Editor::sizeHint() const
280 {
281   QFontMetrics fm( font() );
282   int nbLines = ( isShowBanner() ? myBanner.split("\n").count() : 0 ) + 1;
283   QSize s(100, fm.lineSpacing()*nbLines);
284   return s;
285 }
286
287 /*!
288   \brief Put the string \a str to the python editor.
289   \param str string to be put in the command line of the editor
290   \param newBlock if True, then the string is printed on a new line
291   \param isError if true, the text is printed in dark red
292 */
293 void PyConsole_Editor::addText( const QString& str, 
294                                 const bool     newBlock,
295                                 const bool    isError)
296 {
297   QTextCursor theCursor(textCursor());
298   QTextCharFormat cf;
299
300   moveCursor( QTextCursor::End );
301   if ( newBlock )
302     theCursor.insertBlock();
303   if (isError)
304       cf.setForeground(QBrush(Qt::red));
305   else
306       cf.setForeground(QBrush(Qt::black));
307   theCursor.insertText( str, cf);
308   moveCursor( QTextCursor::End );
309   ensureCursorVisible();
310 }
311
312 /*!
313   \brief Convenient method for executing a Python command,
314   as if the user typed it manually.
315   \param command python command to be executed
316
317   !!! WARNING: doesn't work properly with multi-line commands. !!!
318 */
319 void PyConsole_Editor::exec( const QString& command )
320 {
321   if ( isReadOnly() ) {
322     // some interactive command is being executed in this editor...
323     // shedule the command to the queue
324     myQueue.push_back( command );
325     return;
326   }
327   // remove last line
328   moveCursor( QTextCursor::End );
329   moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
330   textCursor().removeSelectedText();
331   // set "ready" prompt
332   myPrompt = READY_PROMPT;
333   // clear command buffer
334   myCommandBuffer.truncate( 0 );
335   // unset history browsing mode
336   myCmdInHistory = -1;
337   // print command line by line
338   QString cmd = command;
339   if ( !cmd.endsWith( "\n" ) ) cmd += "\n";
340   QStringList lines = command.split( "\n" );
341   for ( int i = 0; i < lines.size(); i++ ) {
342     if ( !lines[i].trimmed().isEmpty() ) {
343       PyCommand aCommand;
344       aCommand.command = lines[i];
345       myHistory.append( aCommand );
346     }
347     addText( ( i == 0 ? READY_PROMPT : DOTS_PROMPT ) + lines[i], i != 0 );
348   }
349   // IPAL20182
350   addText( "", true );
351   // set read-only mode
352   setReadOnly( true );
353   // set busy cursor
354   setCursor( Qt::BusyCursor );
355   
356   // post a request to execute Python command;
357   // editor will be informed via a custom event that execution has been completed
358   PyInterp_Dispatcher::Get()->Exec( createRequest( cmd ) );
359 }
360
361 /*!
362   \brief Create request to the python dispatcher for the command execution.
363
364   \param command python command to be executed
365  */
366 PyInterp_Request* PyConsole_Editor::createRequest( const QString& command )
367 {
368   return new ExecCommand( myInterp, command, this, isSync() );
369 }
370
371 /*!
372   \brief Execute command in the python interpreter
373   and wait until it is finished.
374
375   \param command python command to be executed
376  */
377 void PyConsole_Editor::execAndWait( const QString& command )
378 {
379   // already running ?
380   if( myEventLoop )
381     return;
382
383   // create new event loop
384   myEventLoop = new QEventLoop( this );
385   // execute command
386   exec( command );
387   // run event loop
388   myEventLoop->exec();
389   // delete event loop after command is processed
390   delete myEventLoop;
391   myEventLoop = 0;
392 }
393
394 /*!
395   \brief Process "Enter" key press event. 
396   
397   Execute the command entered by the user.
398 */
399 void PyConsole_Editor::handleReturn()
400 {
401   // Position cursor at the end
402   QTextCursor curs(textCursor());
403   curs.movePosition(QTextCursor::End);
404   setTextCursor(curs);
405
406   // get last line
407   QTextBlock par = document()->end().previous();
408   if ( !par.isValid() ) return;
409
410   // get command
411   QString cmd = par.text().remove( 0, promptSize() );
412   // extend the command buffer with the current command 
413   myCommandBuffer.append( cmd );
414   // add command to the history
415   if ( !cmd.trimmed().isEmpty() ) {
416     PyCommand aCommand;
417     aCommand.command = cmd;
418     myHistory.append( aCommand );
419   }
420
421   // IPAL19397
422   addText( "", true ); 
423   
424   // set read-only mode
425   setReadOnly( true );
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( createRequest( myCommandBuffer ) );
432 }
433
434 /*!
435   \brief Process drop event.
436
437   Paste dragged text.
438   \param event drop event
439 */
440 void PyConsole_Editor::dropEvent( QDropEvent* event )
441 {
442   // get the initial drop position
443   QPoint pos = event->pos();
444   QTextCursor cur = cursorForPosition( event->pos() );
445   // if the position is not in the last line move it to the end of the command line
446   if ( cur.position() < document()->end().previous().position() + promptSize() ) {
447     moveCursor( QTextCursor::End );
448     pos = cursorRect().center();
449   }
450   // create new drop event and use it instead of the original
451   QDropEvent de( pos,
452                  event->possibleActions(),
453                  event->mimeData(),
454                  event->mouseButtons(),
455                  event->keyboardModifiers(),
456                  event->type() );
457   QTextEdit::dropEvent( &de );
458   // accept the original event
459   event->acceptProposedAction();
460 }
461
462 /*!
463   \brief Process mouse button release event.
464
465   Left mouse button: copy selection to the clipboard.
466   Middle mouse button: paste clipboard's contents.
467   \param event mouse event
468 */
469 void PyConsole_Editor::mouseReleaseEvent( QMouseEvent* event )
470 {
471   if ( event->button() == Qt::LeftButton ) {
472     QTextEdit::mouseReleaseEvent( event );
473     //copy();
474   }
475   else if ( event->button() == Qt::MidButton ) {
476     QTextCursor cur = cursorForPosition( event->pos() );
477     // if the position is not in the last line move it to the end of the command line
478     if ( cur.position() < document()->end().previous().position() + promptSize() ) {
479       moveCursor( QTextCursor::End );
480     }
481     else {
482       setTextCursor( cur );
483     }
484     const QMimeData* md = QApplication::clipboard()->mimeData( QApplication::clipboard()->supportsSelection() ? 
485                                                                QClipboard::Selection : QClipboard::Clipboard );
486     if ( md )
487       insertFromMimeData( md );
488   }
489   else {
490     QTextEdit::mouseReleaseEvent( event );
491   }
492 }
493
494 /*!
495   \brief Check if the string is command.
496   
497   Return True if the string \a str is likely to be the command
498   (i.e. it is started from the '>>>' or '...').
499   \param str string to be checked
500 */
501 bool PyConsole_Editor::isCommand( const QString& str ) const
502 {
503   return str.startsWith( READY_PROMPT ) || str.startsWith( DOTS_PROMPT );
504 }
505
506 /*!
507   \brief Handle keyboard event.
508
509   Implement navigation, history browsing, copy/paste and other common
510   operations.
511   \param event keyboard event
512 */
513 void PyConsole_Editor::keyPressEvent( QKeyEvent* event )
514 {
515   // get cursor position
516   QTextCursor cur = textCursor();
517   int curLine = cur.blockNumber();
518   int curCol  = cur.columnNumber();
519
520   // get last edited line
521   int endLine = document()->blockCount()-1;
522
523   // get pressed key code
524   int aKey = event->key();
525
526   // check if <Ctrl> is pressed
527   bool ctrlPressed = event->modifiers() & Qt::ControlModifier;
528   // check if <Shift> is pressed
529   bool shftPressed = event->modifiers() & Qt::ShiftModifier;
530
531   if ( aKey == Qt::Key_Escape || ( ctrlPressed && aKey == -1 ) ) {
532     // process <Ctrl>+<Break> key-binding and <Escape> key: clear current command
533     myCommandBuffer.truncate( 0 );
534     myPrompt = READY_PROMPT;
535     addText( myPrompt, true );
536     horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
537     return;
538   }
539   else if ( ctrlPressed && aKey == Qt::Key_C ) {
540     // process <Ctrl>+<C> key-binding : copy
541     copy();
542     return;
543   }
544   else if ( ctrlPressed && aKey == Qt::Key_X ) {
545     // process <Ctrl>+<X> key-binding : cut
546     cut();
547     return;
548   }
549   else if ( ctrlPressed && aKey == Qt::Key_V ) {
550     // process <Ctrl>+<V> key-binding : paste
551     paste();
552     return;
553   }
554
555   // check for printed key
556   // #### aKey = ( aKey < Qt::Key_Space || aKey > Qt::Key_ydiaeresis ) ? aKey : 0;
557   // Better:
558   aKey = !(QChar(aKey).isPrint()) ? aKey : 0;
559
560   switch ( aKey ) {
561   case 0 :
562     // any printed key: just print it
563     {
564       if ( curLine < endLine || curCol < promptSize() ) {
565         moveCursor( QTextCursor::End );
566       }
567       QTextEdit::keyPressEvent( event );
568       break;
569     }
570   case Qt::Key_Return:
571   case Qt::Key_Enter:
572     // <Enter> key: process the current command
573     {
574       handleReturn();
575       break;
576     }
577   case Qt::Key_Up:
578     // <Up> arrow key: process as follows:
579     // - without <Ctrl>, <Shift> modifiers: previous command in history
580     // - with <Ctrl> modifier key pressed:  move cursor one row up without selection
581     // - with <Shift> modifier key pressed: move cursor one row up with selection
582     // - with <Ctrl>+<Shift> modifier keys pressed: scroll one row up
583     {
584       if ( ctrlPressed && shftPressed ) {
585         int value   = verticalScrollBar()->value();
586         int spacing = fontMetrics().lineSpacing();
587         verticalScrollBar()->setValue( value > spacing ? value-spacing : 0 );
588       }
589       else if ( shftPressed || ctrlPressed ) {
590         if ( curLine > 0 )
591           moveCursor( QTextCursor::Up, 
592                       shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
593       }
594       else { 
595         if ( myCmdInHistory < 0 && myHistory.count() > 0 ) {
596           // set history browsing mode
597           myCmdInHistory = myHistory.count();
598           // remember current command
599           QTextBlock par = document()->end().previous();
600           myCurrentCommand = par.text().remove( 0, promptSize() );
601         }
602         if ( myCmdInHistory > 0 ) {
603           myCmdInHistory--;
604           // get previous command in the history
605           QString previousCommand = myHistory.at( myCmdInHistory ).command;
606           // print previous command
607           moveCursor( QTextCursor::End );
608           moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
609           textCursor().removeSelectedText();
610           addText( myPrompt + previousCommand ); 
611           // move cursor to the end
612           moveCursor( QTextCursor::End );
613         }
614       }
615       break;
616     }
617   case Qt::Key_Down:
618     // <Down> arrow key: process as follows:
619     // - without <Ctrl>, <Shift> modifiers: next command in history
620     // - with <Ctrl> modifier key pressed:  move cursor one row down without selection
621     // - with <Shift> modifier key pressed: move cursor one row down with selection
622     // - with <Ctrl>+<Shift> modifier keys pressed: scroll one row down
623     {
624       if ( ctrlPressed && shftPressed ) {
625         int value   = verticalScrollBar()->value();
626         int maxval  = verticalScrollBar()->maximum();
627         int spacing = fontMetrics().lineSpacing();
628         verticalScrollBar()->setValue( value+spacing < maxval ? value+spacing : maxval );
629       }
630       else if ( shftPressed || ctrlPressed) {
631         if ( curLine < endLine )
632           moveCursor( QTextCursor::Down, 
633                       shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
634       }
635       else { 
636         if ( myCmdInHistory >= 0 ) {
637           // get next command in the history
638           myCmdInHistory++;
639           QString nextCommand;
640           if ( myCmdInHistory < myHistory.count() ) {
641             // next command in history
642             nextCommand = myHistory.at( myCmdInHistory ).command;
643           }
644           else {
645             // end of history is reached
646             // last printed command
647             nextCommand = myCurrentCommand;
648             // unset history browsing mode
649             myCmdInHistory = -1;
650           }
651           // print next or current command
652           moveCursor( QTextCursor::End );
653           moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
654           textCursor().removeSelectedText();
655           addText( myPrompt + nextCommand );
656           // move cursor to the end
657           moveCursor( QTextCursor::End );
658         }
659       }
660       break;
661     }
662   case Qt::Key_Left:
663     // <Left> arrow key: process as follows:
664     // - without <Ctrl>, <Shift> modifiers: move one symbol left (taking into account prompt)
665     // - with <Ctrl> modifier key pressed:  move one word left (taking into account prompt)
666     // - with <Shift> modifier key pressed: move one symbol left with selection
667     // - with <Ctrl>+<Shift> modifier keys pressed: move one word left with selection
668     {
669       QString txt = textCursor().block().text();
670       if ( !shftPressed && isCommand( txt ) && curCol <= promptSize() ) {
671         moveCursor( QTextCursor::Up );
672         moveCursor( QTextCursor::EndOfBlock );
673       }
674       else {
675         QTextEdit::keyPressEvent( event );
676       }
677       break;
678     }
679   case Qt::Key_Right:
680     // <Right> arrow key: process as follows:
681     // - without <Ctrl>, <Shift> modifiers: move one symbol right (taking into account prompt)
682     // - with <Ctrl> modifier key pressed:  move one word right (taking into account prompt)
683     // - with <Shift> modifier key pressed: move one symbol right with selection
684     // - with <Ctrl>+<Shift> modifier keys pressed: move one word right with selection
685     {
686       QString txt = textCursor().block().text();
687       if ( !shftPressed ) {
688         if ( curCol < txt.length() ) {
689           if ( isCommand( txt ) && curCol < promptSize() ) {
690             cur.setPosition( cur.block().position() + promptSize() );
691             setTextCursor( cur );
692             break;
693           }
694         }
695         else {
696           if ( curLine < endLine && isCommand( textCursor().block().next().text() ) ) {
697             cur.setPosition( cur.position() + promptSize()+1 );
698             setTextCursor( cur );
699             horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
700             break;
701           }
702         }
703       }
704       QTextEdit::keyPressEvent( event );
705       break;
706     }
707   case Qt::Key_PageUp:
708     // <PageUp> key: process as follows:
709     // - without <Ctrl>, <Shift> modifiers: first command in history
710     // - with <Ctrl> modifier key pressed:  move cursor one page up without selection
711     // - with <Shift> modifier key pressed: move cursor one page up with selection
712     // - with <Ctrl>+<Shift> modifier keys pressed: scroll one page up
713     {
714       if ( ctrlPressed && shftPressed ) {
715         verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub);
716       }
717       else if ( shftPressed || ctrlPressed ) {
718         bool moved = false;
719         qreal lastY = cursorRect( cur ).top();
720         qreal distance = 0;
721         // move using movePosition to keep the cursor's x
722         do {
723           qreal y = cursorRect( cur ).top();
724           distance += qAbs( y - lastY );
725           lastY = y;
726           moved = cur.movePosition( QTextCursor::Up, 
727                                     shftPressed ? QTextCursor::KeepAnchor : 
728                                                   QTextCursor::MoveAnchor );
729         } while ( moved && distance < viewport()->height() );
730         if ( moved ) {
731           cur.movePosition( QTextCursor::Down, 
732                             shftPressed ? QTextCursor::KeepAnchor : 
733                                           QTextCursor::MoveAnchor );
734           verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub );
735         }
736         setTextCursor( cur );
737       }
738       else { 
739         if ( myCmdInHistory < 0 && myHistory.count() > 0 ) {
740           // set history browsing mode
741           myCmdInHistory = myHistory.count();
742           // remember current command
743           QTextBlock par = document()->end().previous();
744           myCurrentCommand = par.text().remove( 0, promptSize() );
745         }
746         if ( myCmdInHistory > 0 ) {
747           myCmdInHistory = 0;
748           // get very first command in the history
749           QString firstCommand = myHistory.at( myCmdInHistory ).command;
750           // print first command
751           moveCursor( QTextCursor::End );
752           moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
753           textCursor().removeSelectedText();
754           addText( myPrompt + firstCommand ); 
755           // move cursor to the end
756           moveCursor( QTextCursor::End );
757         }
758       }
759       break;
760     }
761   case Qt::Key_PageDown:
762     // <PageDown> key: process as follows:
763     // - without <Ctrl>, <Shift> modifiers: last command in history
764     // - with <Ctrl> modifier key pressed:  move cursor one page down without selection
765     // - with <Shift> modifier key pressed: move cursor one page down with selection
766     // - with <Ctrl>+<Shift> modifier keys pressed: scroll one page down
767     {
768       if ( ctrlPressed && shftPressed ) {
769         verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd);
770       }
771       else if ( shftPressed || ctrlPressed ) {
772         bool moved = false;
773         qreal lastY = cursorRect( cur ).top();
774         qreal distance = 0;
775         // move using movePosition to keep the cursor's x
776         do {
777           qreal y = cursorRect( cur ).top();
778           distance += qAbs( y - lastY );
779           lastY = y;
780           moved = cur.movePosition( QTextCursor::Down, 
781                                     shftPressed ? QTextCursor::KeepAnchor : 
782                                                   QTextCursor::MoveAnchor );
783         } while ( moved && distance < viewport()->height() );
784         if ( moved ) {
785           cur.movePosition( QTextCursor::Up, 
786                             shftPressed ? QTextCursor::KeepAnchor : 
787                                           QTextCursor::MoveAnchor );
788           verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub );
789         }
790         setTextCursor( cur );
791       }
792       else { 
793         if ( myCmdInHistory >= 0 ) {
794           // unset history browsing mode
795           myCmdInHistory = -1;
796           // print current command
797           moveCursor( QTextCursor::End );
798           moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
799           textCursor().removeSelectedText();
800           addText( myPrompt + myCurrentCommand ); 
801           // move cursor to the end
802           moveCursor( QTextCursor::End );
803         }
804       }
805       break;
806     }
807   case Qt::Key_Home: 
808     // <Home> key: process as follows:
809     // - without <Ctrl>, <Shift> modifiers: move cursor to the beginning of the current line without selection
810     // - with <Ctrl> modifier key pressed:  move cursor to the very first symbol without selection
811     // - with <Shift> modifier key pressed: move cursor to the beginning of the current line with selection
812     // - with <Ctrl>+<Shift> modifier keys pressed: move cursor to the very first symbol with selection
813     {
814       if ( ctrlPressed ) { 
815         moveCursor( QTextCursor::Start, 
816                     shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
817       }
818       else {
819         QString txt = textCursor().block().text();
820         if ( isCommand( txt ) ) {
821           if ( shftPressed ) {
822             if ( curCol > promptSize() ) {
823               cur.movePosition( QTextCursor::StartOfLine, QTextCursor::KeepAnchor );
824               cur.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, promptSize() );
825             }
826           }
827           else {
828             cur.movePosition( QTextCursor::StartOfLine );
829             cur.movePosition( QTextCursor::Right, QTextCursor::MoveAnchor, promptSize() );
830           }
831           setTextCursor( cur );
832         }
833         else {
834           moveCursor( QTextCursor::StartOfBlock, 
835                       shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
836         }
837         horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
838       }
839       break;
840     }
841   case Qt::Key_End:
842     // <End> key: process as follows:
843     // - without <Ctrl>, <Shift> modifiers: move cursor to the end of the current line without selection
844     // - with <Ctrl> modifier key pressed:  move cursor to the very last symbol without selection
845     // - with <Shift> modifier key pressed: move cursor to the end of the current line with selection
846     // - with <Ctrl>+<Shift> modifier keys pressed: move cursor to the very last symbol with selection
847     {
848       moveCursor( ctrlPressed ? QTextCursor::End : QTextCursor::EndOfBlock, 
849                   shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor );
850       break;
851     }  
852   case Qt::Key_Backspace :
853     // <Backspace> key: process as follows
854     // - without any modifiers : delete symbol before the cursor / selection (taking into account prompt)
855     // - with <Shift> modifier key pressed: delete previous word
856     // - with <Ctrl> modifier key pressed: delete text from the cursor to the line beginning
857     // works only for last (command) line
858     {
859       if ( cur.hasSelection() ) {
860         cut();
861       }
862       else if ( cur.position() > document()->end().previous().position() + promptSize() ) {
863         if ( shftPressed ) {
864           moveCursor( QTextCursor::PreviousWord, QTextCursor::KeepAnchor );
865           textCursor().removeSelectedText();
866         }
867         else if ( ctrlPressed ) {
868           cur.setPosition( document()->end().previous().position() + promptSize(),
869                            QTextCursor::KeepAnchor );
870           setTextCursor( cur );
871           textCursor().removeSelectedText();
872         }
873         else {
874           QTextEdit::keyPressEvent( event );
875         }
876       }
877       else {
878         cur.setPosition( document()->end().previous().position() + promptSize() );
879         setTextCursor( cur );
880         horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
881       }
882       break;
883     }
884   case Qt::Key_Delete :
885     // <Delete> key: process as follows
886     // - without any modifiers : delete symbol after the cursor / selection (taking into account prompt)
887     // - with <Shift> modifier key pressed: delete next word
888     // - with <Ctrl> modifier key pressed: delete text from the cursor to the end of line
889     // works only for last (command) line
890     {
891       if ( cur.hasSelection() ) {
892         cut();
893       }
894       else if ( cur.position() > document()->end().previous().position() + promptSize()-1 ) {
895         if ( shftPressed ) {
896           moveCursor( QTextCursor::NextWord, QTextCursor::KeepAnchor );
897           textCursor().removeSelectedText();
898         }
899         else if ( ctrlPressed ) {
900           moveCursor( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
901           textCursor().removeSelectedText();
902         }
903         else {
904           QTextEdit::keyPressEvent( event );
905         }
906       }
907       else {
908         cur.setPosition( document()->end().previous().position() + promptSize() );
909         setTextCursor( cur );
910         horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
911       }
912       break;
913     }
914   case Qt::Key_Insert :
915     // <Insert> key: process as follows
916     // - with <Ctrl> modifier key pressed:  copy()
917     // - with <Shift> modifier key pressed: paste() to the command line
918     {
919       if ( ctrlPressed ) {
920         copy();
921       }
922       else if ( shftPressed ) {
923         paste();
924       }
925       else
926         QTextEdit::keyPressEvent( event );
927       break;
928     }
929   }
930 }
931
932 /*!
933   \brief Handle notification event coming from Python dispatcher.
934   \param event notification event
935 */
936 void PyConsole_Editor::customEvent( QEvent* event )
937 {
938   switch( event->type() )
939   {
940   case PrintEvent::EVENT_ID:
941     {
942       PrintEvent* pe=(PrintEvent*)event;
943       addText( pe->text(), false, pe->isError());
944       myHistory.last().output = myHistory.last().output + pe->text();
945       return;
946     }
947   case PyInterp_Event::ES_OK:
948   case PyInterp_Event::ES_ERROR:
949   {
950     // clear command buffer
951     myCommandBuffer.truncate( 0 );
952     // add caret return line if necessary
953     QTextBlock par = document()->end().previous();
954     QString txt = par.text();
955     txt.truncate( txt.length() - 1 );
956     // IPAL19397 : addText moved to handleReturn() method
957     //if ( !txt.isEmpty() )
958     //  addText( "", true );
959     // set "ready" prompt
960     myPrompt = READY_PROMPT;
961     addText( myPrompt );
962     // unset busy cursor
963     unsetCursor();
964     // stop event loop (if running)
965     if ( myEventLoop )
966       myEventLoop->exit();
967     break;
968   }
969   case PyInterp_Event::ES_INCOMPLETE:
970   {
971     // extend command buffer (multi-line command)
972     myCommandBuffer.append( "\n" );
973     // add caret return line if necessary
974     QTextBlock par = document()->end().previous();
975     QString txt = par.text();
976     txt.truncate( txt.length() - 1 );
977     // IPAL19397 : addText moved to handleReturn() method
978     //if ( !txt.isEmpty() )
979     //  addText( "", true );
980     // set "dot" prompt
981     myPrompt = DOTS_PROMPT;
982     addText( myPrompt/*, true*/ ); // IPAL19397
983     // unset busy cursor
984     unsetCursor();
985     // stop event loop (if running)
986     if ( myEventLoop )
987       myEventLoop->exit();
988     break;
989   }
990   default:
991     QTextEdit::customEvent( event );
992   }
993   
994   // unset read-only state
995   setReadOnly( false );
996   // unset history browsing mode
997   myCmdInHistory = -1;
998
999   if ( (int)event->type() == (int)PyInterp_Event::ES_OK && myQueue.count() > 0 )
1000   {
1001     // process the next sheduled command from the queue (if there is any)
1002     QString nextcmd = myQueue[0];
1003     myQueue.pop_front();
1004     exec( nextcmd );
1005   }
1006 }
1007
1008 /*!
1009   \brief Handle Python interpreter change.
1010
1011   Perform initialization actions if the interpreter is changed.
1012   \param interp python interpreter is being set
1013 */
1014 void PyConsole_Editor::onPyInterpChanged( PyConsole_Interp* interp )
1015 {
1016   if ( myInterp != interp 
1017        // Force read-only state and wait cursor when myInterp is NULL
1018       || !myInterp ) {
1019     myInterp = interp;
1020     if ( myInterp ) {
1021       // print banner
1022       myBanner = myInterp->getbanner().c_str();
1023       if ( isShowBanner() )
1024         addText( myBanner );
1025       // clear command buffer
1026       myCommandBuffer.truncate(0);
1027       // unset read-only state
1028       setReadOnly( false );
1029       // unset history browsing mode
1030       myCmdInHistory = -1;
1031       // add prompt
1032       addText( myPrompt );
1033       // unset busy cursor
1034       viewport()->unsetCursor();
1035       // stop event loop (if running)
1036       if( myEventLoop)
1037         myEventLoop->exit();
1038     }
1039     else {
1040       // clear contents
1041       clear();
1042       // set read-only state
1043       setReadOnly( true );
1044       // set busy cursor
1045       setCursor( Qt::WaitCursor );
1046     }
1047   }
1048 }
1049
1050 /*!
1051   \brief "Copy" operation.
1052   
1053   Reimplemented from Qt.
1054   Warning! In Qt4 this method is not virtual.
1055  */
1056 void PyConsole_Editor::cut()
1057 {
1058   QTextCursor cur = textCursor();
1059   if ( cur.hasSelection() ) {
1060     QApplication::clipboard()->setText( cur.selectedText() );
1061     int startSelection = cur.selectionStart();
1062     if ( startSelection < document()->end().previous().position() + promptSize() )
1063       startSelection = document()->end().previous().position() + promptSize();
1064     int endSelection = cur.selectionEnd();
1065     if ( endSelection < document()->end().previous().position() + promptSize() )
1066       endSelection = document()->end().previous().position() + promptSize();
1067     cur.setPosition( startSelection );
1068     cur.setPosition( endSelection, QTextCursor::KeepAnchor );
1069     horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1070     setTextCursor( cur );
1071     textCursor().removeSelectedText();
1072   }
1073 }
1074
1075 /*!
1076   \brief "Paste" operation.
1077
1078   Reimplemented from Qt.
1079   Warning! In Qt4 this method is not virtual.
1080  */
1081 void PyConsole_Editor::paste()
1082 {
1083   QTextCursor cur = textCursor();
1084   if ( cur.hasSelection() ) {
1085     int startSelection = cur.selectionStart();
1086     if ( startSelection < document()->end().previous().position() + promptSize() )
1087       startSelection = document()->end().previous().position() + promptSize();
1088     int endSelection = cur.selectionEnd();
1089     if ( endSelection < document()->end().previous().position() + promptSize() )
1090       endSelection = document()->end().previous().position() + promptSize();
1091     cur.setPosition( startSelection );
1092     cur.setPosition( endSelection, QTextCursor::KeepAnchor );
1093     horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() );
1094     setTextCursor( cur );
1095     textCursor().removeSelectedText();
1096   }
1097   if ( textCursor().position() < document()->end().previous().position() + promptSize() )
1098     moveCursor( QTextCursor::End );
1099   QTextEdit::paste();
1100 }
1101
1102 /*!
1103   \brief "Clear" operation.
1104
1105   Reimplemented from Qt.
1106   Warning! In Qt4 this method is not virtual.
1107  */
1108 void PyConsole_Editor::clear()
1109 {
1110   QTextEdit::clear();
1111   if ( isShowBanner() )
1112     addText( myBanner );
1113   myPrompt = READY_PROMPT;
1114   addText( myPrompt );
1115 }
1116
1117 /*!
1118   \brief "Dump commands" operation.
1119  */
1120 void PyConsole_Editor::dump()
1121 {
1122   QStringList aFilters;
1123   aFilters.append( tr( "PYTHON_FILES_FILTER" ) );
1124   
1125   QString fileName = SUIT_FileDlg::getFileName( this, QString(),
1126                                      aFilters, tr( "TOT_DUMP_PYCOMMANDS" ),
1127                                      false, true, new DumpCommandsFileValidator( this ) );
1128   if ( fileName != "" ) {
1129     QFile file( fileName ); 
1130     if ( !file.open( QFile::WriteOnly ) )
1131       return;
1132
1133     QTextStream out (&file);
1134   
1135     for( int i=0; i<myHistory.count(); i++ ) {
1136          out<<myHistory.at(i).command<<endl;
1137     }
1138     file.close();
1139   }
1140 }
1141 /*!
1142   \brief "Save log" operation.
1143  */
1144 void PyConsole_Editor::saveLog()
1145 {
1146   QStringList aFilters;
1147   aFilters.append( tr( "PYTHON_FILES_FILTER" ) );
1148
1149   QString fileName = SUIT_FileDlg::getFileName( this, QString(),
1150                      aFilters, tr( "TOT_SAVE_PYLOG" ),
1151                  false, true, new DumpCommandsFileValidator( this ) );
1152   if ( fileName != "" ) {
1153     QFile file( fileName );
1154     if ( !file.open( QFile::WriteOnly ) )
1155       return;
1156
1157     QTextStream out (&file);
1158
1159     for( int i = 0; i < myHistory.count(); i++ ) {
1160          out << myHistory.at(i).command << endl;
1161          out << myHistory.at(i).output;
1162     }
1163     file.close();
1164   }
1165 }