SMILX  1.01
milxQtPythonConsole.cpp
Go to the documentation of this file.
1 /*
2 *
3 * Copyright (C) 2010 MeVis Medical Solutions AG 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 Medical Solutions AG, Universitaetsallee 29,
27 * 28359 Bremen, Germany or:
28 *
29 * http://www.mevis.de
30 *
31 */
32 
33 //----------------------------------------------------------------------------------
41 //----------------------------------------------------------------------------------
42 
43 #include "milxQtPythonConsole.h"
44 
45 #include <QMenu>
46 #include <QMouseEvent>
47 #include <QKeyEvent>
48 #include <QApplication>
49 #include <QTextDocumentFragment>
50 #include <QTextBlock>
51 #include <QTextCursor>
52 #include <QDebug>
53 #include <QCompleter>
54 #include <QStringListModel>
55 #include <QScrollBar>
56 #include <QSettings>
57 
58 //-----------------------------------------------------------------------------
59 
60 milxQtPythonConsole::milxQtPythonConsole(QWidget* parent, const PythonQtObjectPtr& context, Qt::WindowFlags windowFlags)
61  : QTextEdit(parent)
62 {
63 
64  setWindowFlags(windowFlags);
65 
66  _defaultTextCharacterFormat = currentCharFormat();
67  _context = context;
68  _historyPosition = 0;
69  _hadError = false;
70 
71  _completer = new QCompleter(this);
72  _completer->setWidget(this);
73  QObject::connect(_completer, SIGNAL(activated(const QString&)),
74  this, SLOT(insertCompletion(const QString&)));
75 
76  QTextEdit::clear();
77  consoleMessage("milxQt Python Console\nMain window is 'MainWindow' and displayed windows are 'rnd_<name>', 'img_<name>' and 'mdl_<name>'\nFile IO can be done using 'milxQtFile'.");
78  appendCommandPrompt();
79 
80  createActions();
81 
82  createConnections();
83 
84  readSettings();
85 }
86 
87 //-----------------------------------------------------------------------------
88 
89 void milxQtPythonConsole::stdOut(const QString& s)
90 {
91  _stdOut += s;
92  int idx;
93  while ((idx = _stdOut.indexOf('\n'))!=-1)
94  {
95  consoleMessage(_stdOut.left(idx));
96  //std::cout << _stdOut.left(idx).toLatin1().data() << std::endl;
97  _stdOut = _stdOut.mid(idx+1);
98  }
99 }
100 
101 void milxQtPythonConsole::stdErr(const QString& s)
102 {
103  _hadError = true;
104  _stdErr += s;
105  int idx;
106  while ((idx = _stdErr.indexOf('\n'))!=-1)
107  {
108  consoleMessage(_stdErr.left(idx));
109  //std::cerr << _stdErr.left(idx).toLatin1().data() << std::endl;
110  _stdErr = _stdErr.mid(idx+1);
111  }
112 }
113 
115 {
116  if (!_stdOut.isEmpty())
117  {
118  stdOut("\n");
119  }
120  if (!_stdErr.isEmpty())
121  {
122  stdErr("\n");
123  }
124 }
125 
126 //-----------------------------------------------------------------------------
127 
128 milxQtPythonConsole::~milxQtPythonConsole()
129 {
130  writeSettings();
131 }
132 
133 
134 
135 //-----------------------------------------------------------------------------
136 
138 {
139 
140  QTextEdit::clear();
141  appendCommandPrompt();
142 }
143 
144 //-----------------------------------------------------------------------------
145 
147 {
148  QTextCursor textCursor = this->textCursor();
149  textCursor.movePosition(QTextCursor::End);
150 
151  // Select the text from the command prompt until the end of the block
152  // and get the selected text.
153  textCursor.setPosition(commandPromptPosition());
154  textCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
155  QString code = textCursor.selectedText();
156 
157  // i don't know where this trailing space is coming from, blast it!
158  if (code.endsWith(" "))
159  {
160  code.truncate(code.length()-1);
161  }
162 
163  if (!code.isEmpty())
164  {
165  // Update the history
166  _history << code;
167  _historyPosition = _history.count();
168  _currentMultiLineCode += code + "\n";
169 
170  if (!storeOnly)
171  {
172  executeCode(_currentMultiLineCode);
173  _currentMultiLineCode = "";
174  }
175  }
176  // Insert a new command prompt
177  appendCommandPrompt(storeOnly);
178 
179 }
180 
181 void milxQtPythonConsole::executeCode(const QString& code)
182 {
183  // put visible cursor to the end of the line
184  QTextCursor cursor = QTextEdit::textCursor();
185  cursor.movePosition(QTextCursor::End);
186  setTextCursor(cursor);
187 
188  //~ int cursorPosition = this->textCursor().position();
189 
190  // evaluate the code
191  _stdOut = "";
192  _stdErr = "";
193  PythonQtObjectPtr p;
194  PyObject* dict = NULL;
195  if (PyModule_Check(_context))
196  {
197  dict = PyModule_GetDict(_context);
198  }
199  else if (PyDict_Check(_context))
200  {
201  dict = _context;
202  }
203  if (dict)
204  {
205  p.setNewRef(PyRun_String(code.toLatin1().data(), Py_single_input, dict, dict));
206  }
207 
208  if (!p)
209  {
210  PythonQt::self()->handleError();
211  }
212 
213  flushStdOut();
214 
215  //~ bool messageInserted = (this->textCursor().position() != cursorPosition);
216 
217  //~ // If a message was inserted, then put another empty line before the command prompt
218  //~ // to improve readability.
219  //~ if (messageInserted) {
220  //~ append(QString());
221  //~ }
222 }
223 
224 
225 //-----------------------------------------------------------------------------
226 
228 {
229  if (storeOnly)
230  {
231  _commandPrompt = "...> ";
232  }
233  else
234  {
235  _commandPrompt = ">>> ";
236  }
237  append(_commandPrompt);
238 
239  QTextCursor cursor = textCursor();
240  cursor.movePosition(QTextCursor::End);
241  setTextCursor(cursor);
242 }
243 
244 
245 
246 //-----------------------------------------------------------------------------
247 
248 void milxQtPythonConsole::setCurrentFont(const QColor& color, bool bold)
249 {
250 
251  QTextCharFormat charFormat(_defaultTextCharacterFormat);
252 
253  QFont font(charFormat.font());
254  font.setBold(bold);
255  charFormat.setFont(font);
256 
257  QBrush brush(charFormat.foreground());
258  brush.setColor(color);
259  charFormat.setForeground(brush);
260 
261  setCurrentCharFormat(charFormat);
262 }
263 
264 
265 
266 //-----------------------------------------------------------------------------
267 
269 {
270 
271  QTextCursor textCursor(this->textCursor());
272  textCursor.movePosition(QTextCursor::End);
273 
274  return textCursor.block().position() + _commandPrompt.length();
275 }
276 
277 
278 
279 //-----------------------------------------------------------------------------
280 
281 void milxQtPythonConsole::insertCompletion(const QString& completion)
282 {
283  QTextCursor tc = textCursor();
284  tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor);
285  if (tc.selectedText()==".")
286  {
287  tc.insertText(QString(".") + completion);
288  }
289  else
290  {
291  tc = textCursor();
292  tc.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
293  tc.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
294  tc.insertText(completion);
295  setTextCursor(tc);
296  }
297 }
298 
299 //-----------------------------------------------------------------------------
301 {
302  QTextCursor textCursor = this->textCursor();
303  int pos = textCursor.position();
304  textCursor.setPosition(commandPromptPosition());
305  textCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
306  int startPos = textCursor.selectionStart();
307 
308  int offset = pos-startPos;
309  QString text = textCursor.selectedText();
310 
311  QString textToComplete;
312  int cur = offset;
313  while (cur--)
314  {
315  QChar c = text.at(cur);
316  if (c.isLetterOrNumber() || c == '.' || c == '_')
317  {
318  textToComplete.prepend(c);
319  }
320  else
321  {
322  break;
323  }
324  }
325 
326 
327  QString lookup;
328  QString compareText = textToComplete;
329  int dot = compareText.lastIndexOf('.');
330  if (dot!=-1)
331  {
332  lookup = compareText.mid(0, dot);
333  compareText = compareText.mid(dot+1, offset);
334  }
335  if (!lookup.isEmpty() || !compareText.isEmpty())
336  {
337  compareText = compareText.toLower();
338  QStringList found;
339  QStringList l = PythonQt::self()->introspection(_context, lookup, PythonQt::Anything);
340  foreach (QString n, l)
341  {
342  if (n.toLower().startsWith(compareText))
343  {
344  found << n;
345  }
346  }
347 
348  if (!found.isEmpty())
349  {
350  _completer->setCompletionPrefix(compareText);
351  _completer->setCompletionMode(QCompleter::PopupCompletion);
352  _completer->setModel(new QStringListModel(found, _completer));
353  _completer->setCaseSensitivity(Qt::CaseInsensitive);
354  QTextCursor c = this->textCursor();
355  c.movePosition(QTextCursor::StartOfWord);
356  QRect cr = cursorRect(c);
357  cr.setWidth(_completer->popup()->sizeHintForColumn(0)
358  + _completer->popup()->verticalScrollBar()->sizeHint().width());
359  cr.translate(0,8);
360  _completer->complete(cr);
361  }
362  else
363  {
364  _completer->popup()->hide();
365  }
366  }
367  else
368  {
369  _completer->popup()->hide();
370  }
371 }
372 
374 {
375 
376  if (_completer && _completer->popup()->isVisible())
377  {
378  // The following keys are forwarded by the completer to the widget
379  switch (event->key())
380  {
381  case Qt::Key_Return:
382  if (!_completer->popup()->currentIndex().isValid())
383  {
384  insertCompletion(_completer->currentCompletion());
385  _completer->popup()->hide();
386  event->accept();
387  }
388  event->ignore();
389  return;
390  break;
391  case Qt::Key_Enter:
392  case Qt::Key_Escape:
393  case Qt::Key_Tab:
394  case Qt::Key_Backtab:
395 
396  event->ignore();
397  return; // let the completer do default behavior
398  default:
399  break;
400  }
401  }
402  bool eventHandled = false;
403  QTextCursor textCursor = this->textCursor();
404 
405  int key = event->key();
406  switch (key)
407  {
408 
409  case Qt::Key_Left:
410 
411  // Moving the cursor left is limited to the position
412  // of the command prompt.
413 
414  if (textCursor.position() <= commandPromptPosition())
415  {
416 
417  QApplication::beep();
418  eventHandled = true;
419  }
420  break;
421 
422  case Qt::Key_Up:
423 
424  // Display the previous command in the history
425  if (_historyPosition>0)
426  {
427  _historyPosition--;
428  changeHistory();
429  }
430 
431  eventHandled = true;
432  break;
433 
434  case Qt::Key_Down:
435 
436  // Display the next command in the history
437  if (_historyPosition+1<_history.count())
438  {
439  _historyPosition++;
440  changeHistory();
441  }
442 
443  eventHandled = true;
444  break;
445 
446  case Qt::Key_Return:
447 
448  executeLine(event->modifiers() & Qt::ShiftModifier);
449  eventHandled = true;
450  break;
451 
452  case Qt::Key_Backspace:
453 
454  if (textCursor.hasSelection())
455  {
456 
457  cut();
458  eventHandled = true;
459 
460  }
461  else
462  {
463 
464  // Intercept backspace key event to check if
465  // deleting a character is allowed. It is not
466  // allowed, if the user wants to delete the
467  // command prompt.
468 
469  if (textCursor.position() <= commandPromptPosition())
470  {
471 
472  QApplication::beep();
473  eventHandled = true;
474  }
475  }
476  break;
477 
478  case Qt::Key_Delete:
479 
480  cut();
481  eventHandled = true;
482  break;
483 
484  default:
485 
486  if (key >= Qt::Key_Space && key <= Qt::Key_division)
487  {
488 
489  if (textCursor.hasSelection() && !verifySelectionBeforeDeletion())
490  {
491 
492  // The selection must not be deleted.
493  eventHandled = true;
494 
495  }
496  else
497  {
498 
499  // The key is an input character, check if the cursor is
500  // behind the last command prompt, else inserting the
501  // character is not allowed.
502 
503  int commandPromptPosition = this->commandPromptPosition();
504  if (textCursor.position() < commandPromptPosition)
505  {
506 
507  textCursor.setPosition(commandPromptPosition);
508  setTextCursor(textCursor);
509  }
510  }
511  }
512  }
513 
514  if (eventHandled)
515  {
516 
517  _completer->popup()->hide();
518  event->accept();
519 
520  }
521  else
522  {
523 
524  QTextEdit::keyPressEvent(event);
525  QString text = event->text();
526  if (!text.isEmpty())
527  {
528  handleTabCompletion();
529  }
530  else
531  {
532  _completer->popup()->hide();
533  }
534  eventHandled = true;
535  }
536 }
537 
538 
539 
540 //-----------------------------------------------------------------------------
541 
543 {
544 
545  bool deletionAllowed = verifySelectionBeforeDeletion();
546  if (deletionAllowed)
547  {
548  QTextEdit::cut();
549  }
550 }
551 
552 
553 
554 //-----------------------------------------------------------------------------
555 
557 {
558 
559  bool deletionAllowed = true;
560 
561 
562  QTextCursor textCursor = this->textCursor();
563 
564  int commandPromptPosition = this->commandPromptPosition();
565  int selectionStart = textCursor.selectionStart();
566  int selectionEnd = textCursor.selectionEnd();
567 
568  if (textCursor.hasSelection())
569  {
570 
571  // Selected text may only be deleted after the last command prompt.
572  // If the selection is partly after the command prompt set the selection
573  // to the part and deletion is allowed. If the selection occurs before the
574  // last command prompt, then deletion is not allowed.
575 
576  if (selectionStart < commandPromptPosition ||
577  selectionEnd < commandPromptPosition)
578  {
579 
580  // Assure selectionEnd is bigger than selection start
581  if (selectionStart > selectionEnd)
582  {
583  int tmp = selectionEnd;
584  selectionEnd = selectionStart;
585  selectionStart = tmp;
586  }
587 
588  if (selectionEnd < commandPromptPosition)
589  {
590 
591  // Selection is completely before command prompt,
592  // so deletion is not allowed.
593  QApplication::beep();
594  deletionAllowed = false;
595 
596  }
597  else
598  {
599 
600  // The selectionEnd is after the command prompt, so set
601  // the selection start to the commandPromptPosition.
602  selectionStart = commandPromptPosition;
603  textCursor.setPosition(selectionStart);
604  textCursor.setPosition(selectionStart, QTextCursor::KeepAnchor);
605  setTextCursor(textCursor);
606  }
607  }
608 
609  }
610  else // if (hasSelectedText())
611  {
612 
613  // When there is no selected text, deletion is not allowed before the
614  // command prompt.
615  if (textCursor.position() < commandPromptPosition)
616  {
617 
618  QApplication::beep();
619  deletionAllowed = false;
620  }
621  }
622 
623  return deletionAllowed;
624 }
625 
626 
627 
628 //-----------------------------------------------------------------------------
629 
631 {
632 
633  // Select the text after the last command prompt ...
634  QTextCursor textCursor = this->textCursor();
635  textCursor.movePosition(QTextCursor::End);
636  textCursor.setPosition(commandPromptPosition(), QTextCursor::KeepAnchor);
637 
638  // ... and replace it with the history text.
639  textCursor.insertText(_history.value(_historyPosition));
640 
641  textCursor.movePosition(QTextCursor::End);
642  setTextCursor(textCursor);
643 }
644 
646 {
647  QSettings settings("Shekhar Chandra", "milxQt");
648 
649  settings.beginGroup("Python");
650  _history = settings.value("history").toStringList();
651  _historyPosition = _history.size();
652  settings.endGroup();
653 }
654 
655 
657 {
658  const int maxHistorySize = 100;
659 
660  QSettings settings("Shekhar Chandra", "milxQt");
661 
662  settings.beginGroup("Python");
664  QStringList savedHistory = settings.value("history").toStringList();
665  savedHistory.removeDuplicates();
666  _history.removeDuplicates();
667  savedHistory << _history;
668  for(int j = 0; j < savedHistory.size()-maxHistorySize; j ++)
669  savedHistory.removeFirst();
670  settings.setValue("history", savedHistory);
671  settings.endGroup();
672 }
673 
674 //-----------------------------------------------------------------------------
675 
676 void milxQtPythonConsole::consoleMessage(const QString & message)
677 {
678 
679  append(QString());
680  insertPlainText(message);
681 
682  // Reset all font modifications done by the html string
683  setCurrentCharFormat(_defaultTextCharacterFormat);
684 }
685 
687 {
688  copyAct = new QAction(this);
689  copyAct->setIcon(QIcon(":/resources/toolbar/copy.png"));
690  copyAct->setText(QApplication::translate("Console", "Copy", 0, QApplication::UnicodeUTF8));
691  copyAct->setShortcut(tr("Ctrl+c"));
692 
693  cutAct = new QAction(this);
694  cutAct->setIcon(QIcon(":/resources/toolbar/cut.png"));
695  cutAct->setText(QApplication::translate("Console", "Cut", 0, QApplication::UnicodeUTF8));
696  cutAct->setShortcut(tr("Ctrl+x"));
697 
698  pasteAct = new QAction(this);
699  pasteAct->setIcon(QIcon(":/resources/toolbar/paste.png"));
700  pasteAct->setText(QApplication::translate("Console", "Paste", 0, QApplication::UnicodeUTF8));
701  pasteAct->setShortcut(tr("Ctrl+v"));
702 
703  clearAct = new QAction(this);
704  clearAct->setText(QApplication::translate("Console", "Clear", 0, QApplication::UnicodeUTF8));
705  clearAct->setShortcut(tr("F5"));
706 }
707 
709 {
710  //Actions
711  connect(copyAct, SIGNAL(triggered()), this, SLOT(copy()));
712  connect(cutAct, SIGNAL(triggered()), this, SLOT(cut()));
713  connect(pasteAct, SIGNAL(triggered()), this, SLOT(paste()));
714  connect(clearAct, SIGNAL(triggered()), this, SLOT(clear()));
715  //Python
716  connect(PythonQt::self(), SIGNAL(pythonStdOut(const QString&)), this, SLOT(stdOut(const QString&)));
717  connect(PythonQt::self(), SIGNAL(pythonStdErr(const QString&)), this, SLOT(stdErr(const QString&)));
718 }
719 
720 void milxQtPythonConsole::contextMenuEvent(QContextMenuEvent *currentEvent)
721 {
722  QMenu* contextMenu = new QMenu(this);
723 
724  contextMenu->addAction(copyAct);
725  contextMenu->addAction(cutAct);
726  contextMenu->addAction(pasteAct);
727  contextMenu->addSeparator();
728  contextMenu->addAction(clearAct);
729 
730  contextMenu->exec(currentEvent->globalPos());
731 }
732 
void executeLine(bool storeOnly)
execute current line
void stdErr(const QString &s)
output redirection
void stdOut(const QString &s)
output redirection
void consoleMessage(const QString &message)
output from console
void appendCommandPrompt(bool storeOnly=false)
Appends a newline and command prompt at the end of the document.
void keyPressEvent(QKeyEvent *e)
derived key press event
void clear()
clear the console
void contextMenuEvent(QContextMenuEvent *currentEvent)
context menu
void writeSettings()
Write settings (mainly history)
void handleTabCompletion()
handle the pressing of tab
void setCurrentFont(const QColor &color=QColor(0, 0, 0), bool bold=false)
Sets the current font.
void createActions()
Create all the actions for the console.
void createConnections()
Create connections for the console.
void changeHistory()
change the history according to _historyPos
virtual void cut()
overridden to control which characters a user may delete
void flushStdOut()
flush output that was not yet printed
int commandPromptPosition()
Returns the position of the command prompt.
void readSettings()
Read settings (mainly history)