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