1 /*
2 *
3 *  Copyright (C) 2006 MeVis Research GmbH All Rights Reserved.
4 *
5 *  This library is free software; you can redistribute it and/or
6 *  modify it under the terms of the GNU Lesser General Public
7 *  License as published by the Free Software Foundation; either
8 *  version 2.1 of the License, or (at your option) any later version.
9 *
10 *  This library is distributed in the hope that it will be useful,
11 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 *  Lesser General Public License for more details.
14 *
15 *  Further, this software is distributed without any warranty that it is
16 *  free of the rightful claim of any third person regarding infringement
17 *  or the like.  Any license provided herein, whether implied or
18 *  otherwise, applies only to this software file.  Patent licenses, if
19 *  any, provided herein do not apply to combinations of this program with
20 *  other software, or any other product whatsoever.
21 *
22 *  You should have received a copy of the GNU Lesser General Public
23 *  License along with this library; if not, write to the Free Software
24 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25 *
26 *  Contact information: MeVis Research GmbH, Universitaetsallee 29,
27 *  28359 Bremen, Germany or:
28 *
29 *  http://www.mevis.de
30 *
31 */
32 
33 //----------------------------------------------------------------------------------
34 /*!
35 // \file    PythonQtScriptingConsole.cpp
36 // \author  Florian Link
37 // \author  Last changed by $Author: florian $
38 // \date    2006-10
39 */
40 //----------------------------------------------------------------------------------
41 
42 #include "PythonQtScriptingConsole.h"
43 
44 #include <QMenu>
45 #include <QMouseEvent>
46 #include <QKeyEvent>
47 #include <QApplication>
48 #include <QTextDocumentFragment>
49 #include <QTextBlock>
50 #include <QTextCursor>
51 #include <QDebug>
52 #include <QCompleter>
53 #include <QStringListModel>
54 #include <QScrollBar>
55 
56 //-----------------------------------------------------------------------------
57 
PythonQtScriptingConsole(QWidget * parent,const PythonQtObjectPtr & context,Qt::WindowFlags windowFlags)58 PythonQtScriptingConsole::PythonQtScriptingConsole(QWidget* parent, const PythonQtObjectPtr& context, Qt::WindowFlags windowFlags)
59 : QTextEdit(parent) {
60 
61   setWindowFlags(windowFlags);
62 
63   _defaultTextCharacterFormat = currentCharFormat();
64   _context                    = context;
65   _historyPosition            = 0;
66 
67   _completer = new QCompleter(this);
68   _completer->setWidget(this);
69   QObject::connect(_completer, SIGNAL(activated(const QString&)),
70     this, SLOT(insertCompletion(const QString&)));
71 
72   clear();
73 
74   connect(PythonQt::self(), SIGNAL(pythonStdOut(const QString&)), this, SLOT(stdOut(const QString&)));
75   connect(PythonQt::self(), SIGNAL(pythonStdErr(const QString&)), this, SLOT(stdErr(const QString&)));
76 }
77 
78 //-----------------------------------------------------------------------------
79 
stdOut(const QString & s)80 void PythonQtScriptingConsole::stdOut(const QString& s)
81 {
82   _stdOut += s;
83   int idx;
84   while ((idx = _stdOut.indexOf('\n'))!=-1) {
85     consoleMessage(_stdOut.left(idx));
86     std::cout << _stdOut.left(idx).toLatin1().data() << std::endl;
87     _stdOut = _stdOut.mid(idx+1);
88   }
89 }
90 
stdErr(const QString & s)91 void PythonQtScriptingConsole::stdErr(const QString& s)
92 {
93   _stdErr += s;
94   int idx;
95   while ((idx = _stdErr.indexOf('\n'))!=-1) {
96     consoleMessage(_stdErr.left(idx));
97     std::cout << _stdErr.left(idx).toLatin1().data() << std::endl;
98     _stdErr = _stdErr.mid(idx+1);
99   }
100 }
101 
flushStdOut()102 void PythonQtScriptingConsole::flushStdOut()
103 {
104   if (!_stdOut.isEmpty()) {
105     stdOut("\n");
106   }
107   if (!_stdErr.isEmpty()) {
108     stdErr("\n");
109   }
110 }
111 
112 //-----------------------------------------------------------------------------
113 
~PythonQtScriptingConsole()114 PythonQtScriptingConsole::~PythonQtScriptingConsole() {
115 }
116 
117 
118 
119 //-----------------------------------------------------------------------------
120 
clear()121 void PythonQtScriptingConsole::clear() {
122 
123   QTextEdit::clear();
124   appendCommandPrompt();
125 }
126 
127 //-----------------------------------------------------------------------------
128 
executeLine(bool storeOnly)129 void PythonQtScriptingConsole::executeLine(bool storeOnly)
130 {
131   QTextCursor textCursor = this->textCursor();
132   textCursor.movePosition(QTextCursor::End);
133 
134   // Select the text from the command prompt until the end of the block
135   // and get the selected text.
136   textCursor.setPosition(commandPromptPosition());
137   textCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
138   QString code = textCursor.selectedText();
139 
140   // i don't know where this trailing space is coming from, blast it!
141   if (code.endsWith(" ")) {
142     code.truncate(code.length()-1);
143   }
144 
145   if (!code.isEmpty()) {
146     // Update the history
147     _history << code;
148     _historyPosition = _history.count();
149     _currentMultiLineCode += code + "\n";
150 
151     if (!storeOnly) {
152       executeCode(_currentMultiLineCode);
153       _currentMultiLineCode = "";
154     }
155   }
156   // Insert a new command prompt
157   appendCommandPrompt(storeOnly);
158 
159 }
160 
executeCode(const QString & code)161 void PythonQtScriptingConsole::executeCode(const QString& code)
162 {
163   // put visible cursor to the end of the line
164   QTextCursor cursor = QTextEdit::textCursor();
165   cursor.movePosition(QTextCursor::End);
166   setTextCursor(cursor);
167 
168   int cursorPosition = this->textCursor().position();
169 
170   // evaluate the code
171   _stdOut = "";
172   _stdErr = "";
173   PythonQtObjectPtr p;
174   p.setNewRef(PyRun_String(code.toLatin1().data(), Py_single_input, PyModule_GetDict(_context), PyModule_GetDict(_context)));
175   if (!p) {
176     PythonQt::self()->handleError();
177   }
178 
179   flushStdOut();
180 
181   bool messageInserted = (this->textCursor().position() != cursorPosition);
182 
183   // If a message was inserted, then put another empty line before the command prompt
184   // to improve readability.
185   if (messageInserted) {
186     append(QString());
187   }
188 }
189 
190 
191 //-----------------------------------------------------------------------------
192 
appendCommandPrompt(bool storeOnly)193 void PythonQtScriptingConsole::appendCommandPrompt(bool storeOnly) {
194   if (storeOnly) {
195     _commandPrompt = "...> ";
196   } else {
197     _commandPrompt = "py> ";
198   }
199   append(_commandPrompt);
200 
201   QTextCursor cursor = textCursor();
202   cursor.movePosition(QTextCursor::End);
203   setTextCursor(cursor);
204 }
205 
206 
207 
208 //-----------------------------------------------------------------------------
209 
setCurrentFont(const QColor & color,bool bold)210 void PythonQtScriptingConsole::setCurrentFont(const QColor& color, bool bold) {
211 
212   QTextCharFormat charFormat(_defaultTextCharacterFormat);
213 
214   QFont font(charFormat.font());
215   font.setBold(bold);
216   charFormat.setFont(font);
217 
218   QBrush brush(charFormat.foreground());
219   brush.setColor(color);
220   charFormat.setForeground(brush);
221 
222   setCurrentCharFormat(charFormat);
223 }
224 
225 
226 
227 //-----------------------------------------------------------------------------
228 
commandPromptPosition()229 int PythonQtScriptingConsole::commandPromptPosition() {
230 
231   QTextCursor textCursor(this->textCursor());
232   textCursor.movePosition(QTextCursor::End);
233 
234   return textCursor.block().position() + _commandPrompt.length();
235 }
236 
237 
238 
239 //-----------------------------------------------------------------------------
240 
insertCompletion(const QString & completion)241 void PythonQtScriptingConsole::insertCompletion(const QString& completion)
242 {
243   QTextCursor tc = textCursor();
244   tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor);
245   if (tc.selectedText()==".") {
246     tc.insertText(QString(".") + completion);
247   } else {
248     tc = textCursor();
249     tc.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
250     tc.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
251     tc.insertText(completion);
252     setTextCursor(tc);
253   }
254 }
255 
256 //-----------------------------------------------------------------------------
handleTabCompletion()257 void PythonQtScriptingConsole::handleTabCompletion()
258 {
259   QTextCursor textCursor   = this->textCursor();
260   int pos = textCursor.position();
261   textCursor.setPosition(commandPromptPosition());
262   textCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
263   int startPos = textCursor.selectionStart();
264 
265   int offset = pos-startPos;
266   QString text = textCursor.selectedText();
267 
268   QString textToComplete;
269   int cur = offset;
270   while (cur--) {
271     QChar c = text.at(cur);
272     if (c.isLetterOrNumber() || c == '.' || c == '_') {
273       textToComplete.prepend(c);
274     } else {
275       break;
276     }
277   }
278 
279 
280   QString lookup;
281   QString compareText = textToComplete;
282   int dot = compareText.lastIndexOf('.');
283   if (dot!=-1) {
284     lookup = compareText.mid(0, dot);
285     compareText = compareText.mid(dot+1, offset);
286   }
287   if (!lookup.isEmpty() || !compareText.isEmpty()) {
288     compareText = compareText.toLower();
289     QStringList found;
290     QStringList l = PythonQt::self()->introspection(_context, lookup, PythonQt::Anything);
291     foreach (QString n, l) {
292       if (n.toLower().startsWith(compareText)) {
293         found << n;
294       }
295     }
296 
297     if (!found.isEmpty()) {
298       _completer->setCompletionPrefix(compareText);
299       _completer->setCompletionMode(QCompleter::PopupCompletion);
300       _completer->setModel(new QStringListModel(found, _completer));
301       _completer->setCaseSensitivity(Qt::CaseInsensitive);
302       QTextCursor c = this->textCursor();
303       c.movePosition(QTextCursor::StartOfWord);
304       QRect cr = cursorRect(c);
305       cr.setWidth(_completer->popup()->sizeHintForColumn(0)
306         + _completer->popup()->verticalScrollBar()->sizeHint().width());
307       cr.translate(0,8);
308       _completer->complete(cr);
309     } else {
310       _completer->popup()->hide();
311     }
312   } else {
313     _completer->popup()->hide();
314   }
315 }
316 
keyPressEvent(QKeyEvent * event)317 void PythonQtScriptingConsole::keyPressEvent(QKeyEvent* event) {
318 
319   if (_completer && _completer->popup()->isVisible()) {
320     // The following keys are forwarded by the completer to the widget
321     switch (event->key()) {
322     case Qt::Key_Return:
323       if (!_completer->popup()->currentIndex().isValid()) {
324         insertCompletion(_completer->currentCompletion());
325         _completer->popup()->hide();
326         event->accept();
327       }
328       event->ignore();
329       return;
330       break;
331     case Qt::Key_Enter:
332     case Qt::Key_Escape:
333     case Qt::Key_Tab:
334     case Qt::Key_Backtab:
335 
336       event->ignore();
337       return; // let the completer do default behavior
338     default:
339       break;
340     }
341   }
342   bool        eventHandled = false;
343   QTextCursor textCursor   = this->textCursor();
344 
345   int key = event->key();
346   switch (key) {
347 
348   case Qt::Key_Left:
349 
350     // Moving the cursor left is limited to the position
351     // of the command prompt.
352 
353     if (textCursor.position() <= commandPromptPosition()) {
354 
355       QApplication::beep();
356       eventHandled = true;
357     }
358     break;
359 
360   case Qt::Key_Up:
361 
362     // Display the previous command in the history
363     if (_historyPosition>0) {
364       _historyPosition--;
365       changeHistory();
366     }
367 
368     eventHandled = true;
369     break;
370 
371   case Qt::Key_Down:
372 
373     // Display the next command in the history
374     if (_historyPosition+1<_history.count()) {
375       _historyPosition++;
376       changeHistory();
377     }
378 
379     eventHandled = true;
380     break;
381 
382   case Qt::Key_Return:
383 
384     executeLine(event->modifiers() & Qt::ShiftModifier);
385     eventHandled = true;
386     break;
387 
388   case Qt::Key_Backspace:
389 
390     if (textCursor.hasSelection()) {
391 
392       cut();
393       eventHandled = true;
394 
395     } else {
396 
397       // Intercept backspace key event to check if
398       // deleting a character is allowed. It is not
399       // allowed, if the user wants to delete the
400       // command prompt.
401 
402       if (textCursor.position() <= commandPromptPosition()) {
403 
404         QApplication::beep();
405         eventHandled = true;
406       }
407     }
408     break;
409 
410   case Qt::Key_Delete:
411 
412     cut();
413     eventHandled = true;
414     break;
415 
416   default:
417 
418     if (key >= Qt::Key_Space && key <= Qt::Key_division) {
419 
420       if (textCursor.hasSelection() && !verifySelectionBeforeDeletion()) {
421 
422         // The selection must not be deleted.
423         eventHandled = true;
424 
425       } else {
426 
427         // The key is an input character, check if the cursor is
428         // behind the last command prompt, else inserting the
429         // character is not allowed.
430 
431         int commandPromptPosition = this->commandPromptPosition();
432         if (textCursor.position() < commandPromptPosition) {
433 
434           textCursor.setPosition(commandPromptPosition);
435           setTextCursor(textCursor);
436         }
437       }
438     }
439   }
440 
441   if (eventHandled) {
442 
443     _completer->popup()->hide();
444     event->accept();
445 
446   } else {
447 
448     QTextEdit::keyPressEvent(event);
449     QString text = event->text();
450     if (!text.isEmpty()) {
451       handleTabCompletion();
452     } else {
453       _completer->popup()->hide();
454     }
455     eventHandled = true;
456   }
457 }
458 
459 
460 
461 //-----------------------------------------------------------------------------
462 
cut()463 void PythonQtScriptingConsole::cut() {
464 
465   bool deletionAllowed = verifySelectionBeforeDeletion();
466   if (deletionAllowed) {
467     QTextEdit::cut();
468   }
469 }
470 
471 
472 
473 //-----------------------------------------------------------------------------
474 
verifySelectionBeforeDeletion()475 bool PythonQtScriptingConsole::verifySelectionBeforeDeletion() {
476 
477   bool deletionAllowed = true;
478 
479 
480   QTextCursor textCursor = this->textCursor();
481 
482   int commandPromptPosition = this->commandPromptPosition();
483   int selectionStart        = textCursor.selectionStart();
484   int selectionEnd          = textCursor.selectionEnd();
485 
486   if (textCursor.hasSelection()) {
487 
488     // Selected text may only be deleted after the last command prompt.
489     // If the selection is partly after the command prompt set the selection
490     // to the part and deletion is allowed. If the selection occurs before the
491     // last command prompt, then deletion is not allowed.
492 
493     if (selectionStart < commandPromptPosition ||
494       selectionEnd < commandPromptPosition) {
495 
496       // Assure selectionEnd is bigger than selection start
497       if (selectionStart > selectionEnd) {
498         int tmp         = selectionEnd;
499         selectionEnd    = selectionStart;
500         selectionStart  = tmp;
501       }
502 
503       if (selectionEnd < commandPromptPosition) {
504 
505         // Selection is completely before command prompt,
506         // so deletion is not allowed.
507         QApplication::beep();
508         deletionAllowed = false;
509 
510       } else {
511 
512         // The selectionEnd is after the command prompt, so set
513         // the selection start to the commandPromptPosition.
514         selectionStart = commandPromptPosition;
515         textCursor.setPosition(selectionStart);
516         textCursor.setPosition(selectionStart, QTextCursor::KeepAnchor);
517         setTextCursor(textCursor);
518       }
519     }
520 
521   } else { // if (hasSelectedText())
522 
523     // When there is no selected text, deletion is not allowed before the
524     // command prompt.
525     if (textCursor.position() < commandPromptPosition) {
526 
527       QApplication::beep();
528       deletionAllowed = false;
529     }
530   }
531 
532   return deletionAllowed;
533 }
534 
535 
536 
537 //-----------------------------------------------------------------------------
538 
changeHistory()539 void PythonQtScriptingConsole::changeHistory() {
540 
541   // Select the text after the last command prompt ...
542   QTextCursor textCursor = this->textCursor();
543   textCursor.movePosition(QTextCursor::End);
544   textCursor.setPosition(commandPromptPosition(), QTextCursor::KeepAnchor);
545 
546   // ... and replace it with the history text.
547   textCursor.insertText(_history.value(_historyPosition));
548 
549   textCursor.movePosition(QTextCursor::End);
550   setTextCursor(textCursor);
551 }
552 
553 
554 
555 //-----------------------------------------------------------------------------
556 
consoleMessage(const QString & message)557 void PythonQtScriptingConsole::consoleMessage(const QString & message) {
558 
559   append(QString());
560   insertPlainText(message);
561 
562   // Reset all font modifications done by the html string
563   setCurrentCharFormat(_defaultTextCharacterFormat);
564 }
565