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