Salome HOME
0022802: EDF 9111 GUI: Regression: SIGSEV error when loading JOBMANGER after a python...
[modules/gui.git] / src / PyConsole / PyConsole_EnhEditor.cxx
1 // Copyright (C) 2007-2014  CEA/DEN, EDF R&D, OPEN CASCADE
2 //
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Lesser General Public
5 // License as published by the Free Software Foundation; either
6 // version 2.1 of the License, or (at your option) any later version.
7 //
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 // Lesser General Public License for more details.
12 //
13 // You should have received a copy of the GNU Lesser General Public
14 // License along with this library; if not, write to the Free Software
15 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 //
17 // See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
18 //
19 // Author : Adrien Bruneton (CEA/DEN)
20 // Created on: 4 avr. 2013
21
22 #include "PyConsole.h"
23 #include <Python.h>
24
25 #include <QKeyEvent>
26 #include <QTextBlock>
27 #include <QTextCursor>
28 #include <QTextCharFormat>
29 #include <QRegExp>
30
31 #include "PyConsole_EnhEditor.h"
32 #include "PyConsole_EnhInterp.h"
33 #include "PyConsole_Request.h"
34 #include "PyInterp_Dispatcher.h"
35
36 // Initialize list of valid separators
37 static const char * tmp_a[] = {" ", "(", "[","+", "-", "*", "/", ";", "^", "="};
38 const std::vector<QString> PyConsole_EnhEditor::SEPARATORS = \
39     std::vector<QString>(tmp_a, tmp_a + sizeof(tmp_a)/sizeof(tmp_a[0]));
40
41 /**
42  * Constructor.
43  * @param interp the interpreter linked to the editor
44  * @param parent parent widget
45  */
46 PyConsole_EnhEditor::PyConsole_EnhEditor(PyConsole_Interp* interp, QWidget* parent) :
47      PyConsole_Editor(interp, parent),
48      _tab_mode(false),
49      _cursor_pos(-1),
50      _multi_line_paste(false),
51      _multi_line_content()
52 {
53   document()->setUndoRedoEnabled(true);
54 }
55
56 /**
57  * Overrides. Catches the TAB and Ctrl+TAB combinations.
58  * @param event
59  */
60 void PyConsole_EnhEditor::keyPressEvent ( QKeyEvent* event)
61 {
62   // check if <Ctrl> is pressed
63   bool ctrlPressed = event->modifiers() & Qt::ControlModifier;
64   // check if <Shift> is pressed
65   bool shftPressed = event->modifiers() & Qt::ShiftModifier;
66
67   if (event->key() == Qt::Key_Tab && !shftPressed)
68     {
69       if (!ctrlPressed)
70         handleTab();
71       else
72         {
73           clearCompletion();
74           handleBackTab();
75         }
76       PyConsole_Editor::keyPressEvent(event);
77     }
78   else
79     {
80       // If ctrl is not pressed (and sth else is pressed with it),
81       // or if ctrl is not pressed alone
82       if (!ctrlPressed || (ctrlPressed && event->key() != Qt::Key_Control))
83         {
84           clearCompletion();
85           _cursor_pos = -1;
86         }
87       // Discard ctrl pressed alone:
88       if (event->key() != Qt::Key_Control)
89         PyConsole_Editor::keyPressEvent(event);
90     }
91 }
92
93 /**
94  * Whenever the mouse is clicked, clear the completion.
95  * @param e
96  */
97 void PyConsole_EnhEditor::mousePressEvent(QMouseEvent* e)
98 {
99   clearCompletion();
100   _cursor_pos = -1;
101   PyConsole_Editor::mousePressEvent(e);
102 }
103
104 /**
105  * Clear in the editor the block of text displayed after having hit <TAB>.
106  */
107 void PyConsole_EnhEditor::clearCompletion()
108 {
109   // Delete completion text if present
110   if (_tab_mode)
111     {
112       // Remove completion display
113       document()->undo();
114       // Remove trailing line return:
115       QTextCursor tc(textCursor());
116       tc.setPosition(document()->characterCount()-1);
117       setTextCursor(tc);
118       textCursor().deletePreviousChar();
119       // TODO: before wait for any TAB event to be completed
120       if ( myInterp ) 
121         myInterp->clearCompletion();
122     }
123   _tab_mode = false;
124 }
125
126 /**
127  * Handle the sequence of events after having hit <TAB>
128  */
129 void PyConsole_EnhEditor::handleTab()
130 {
131   if (_tab_mode)
132     {
133       // Already tab mode - nothing to do !
134       return;
135     }
136
137   QTextCursor cursor(textCursor());
138
139   // Cursor at end of input
140   cursor.movePosition(QTextCursor::End);
141   setTextCursor(cursor);
142
143   // Save cursor position if needed
144   if (_cursor_pos == -1)
145     _cursor_pos = textCursor().position();
146
147   // get last line
148   QTextBlock par = document()->end().previous();
149   if ( !par.isValid() ) return;
150
151   // Switch to completion mode
152   _tab_mode = true;
153
154   QString cmd = par.text().mid(promptSize());
155
156   // Post completion request
157   // Editor will be informed via a custom event that completion has been run
158   PyInterp_Request* req = createTabRequest(cmd);
159   PyInterp_Dispatcher::Get()->Exec(req);
160 }
161
162 /**
163  * Handles what happens after hitting Ctrl-TAB
164  */
165 void PyConsole_EnhEditor::handleBackTab()
166 {
167   QTextCursor cursor(textCursor());
168
169   if (_cursor_pos == -1)
170     {
171       // Invalid cursor position - we can't do anything:
172       return;
173     }
174   // Ensure cursor is at the end of command line
175   cursor.setPosition(_cursor_pos);
176   cursor.movePosition(QTextCursor::EndOfLine);
177   //setCursor(cursor);
178
179   // Delete last completed text
180   int i = cursor.position() - _cursor_pos;
181   cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, i);
182   cursor.removeSelectedText();
183   _cursor_pos = -1;
184 }
185
186 /**
187  * Create the Python requested that will be posted to the interpreter to
188  * get the completions.
189  * @param input line typed by the user at the time TAB was hit
190  * @return a CompletionCommand
191  * @sa CompletionCommand
192  */
193 PyInterp_Request* PyConsole_EnhEditor::createTabRequest( const QString& input )
194 {
195   // Parse input to extract on what part the dir() has to be executed
196   QString input2(input);
197
198   // Split up to the last syntaxical separator
199   int lastSp = -1;
200   for (std::vector<QString>::const_iterator i = SEPARATORS.begin(); i != SEPARATORS.end(); i++)
201     {
202       int j = input2.lastIndexOf(*i);
203       if (j > lastSp)
204         lastSp = j;
205     }
206   if (lastSp >= 0)
207     input2 = input.mid(lastSp+1);
208
209   // Detect a qualified name (with a point)
210   int lastPt = input2.lastIndexOf(QString("."));
211
212   // Split the 2 surrounding parts of the qualified name
213   if (lastPt != -1)
214     {
215       _compl_before_point = input2.left(lastPt);
216       _compl_after_point = input2.mid(lastPt+1);
217     }
218   else
219     {
220       // No point found - do a global matching -
221       // (the following will call dir() with an empty string)
222       _compl_after_point = input2;
223       _compl_before_point = QString("");
224     }
225
226   return new CompletionCommand( myInterp, _compl_before_point,
227                                 _compl_after_point, this, isSync() );
228 }
229
230 /**
231  * Format completion results - this is where we should create 3 columns etc ...
232  * @param matches list of possible completions
233  * @param result return value
234  */
235 void PyConsole_EnhEditor::formatCompletion(const QStringList& matches, QString& result) const
236 {
237   int sz = matches.size();
238
239   if (sz > MAX_COMPLETIONS)
240     {
241       sz = MAX_COMPLETIONS;
242       result.append("[Too many matches! Displaying first ones only ...]\n");
243     }
244
245   for (int i = 0; i < sz; ++i)
246     {
247       result.append(matches[i]);
248       result.append("\n");
249     }
250 }
251
252 /**
253  * Override. Catches the events generated by the enhanced interpreter after the execution
254  * of a completion request.
255  * @param event
256  */
257 void PyConsole_EnhEditor::customEvent( QEvent* event )
258 {
259   QStringList matches;
260   QString first_match, comple_text, doc, base;
261   QTextCursor cursor(textCursor());
262   QTextBlockFormat bf;
263   QTextCharFormat cf;
264   int cursorPos;
265
266   switch( event->type() )
267   {
268     case PyInterp_Event::ES_TAB_COMPLETE_OK:
269     {
270       // Extract corresponding matches from the interpreter
271       matches = getInterp()->getLastMatches();
272       doc = getInterp()->getDocStr();
273
274       if (matches.size() == 0)
275       {
276         // Completion successful but nothing returned.
277         _tab_mode = false;
278         _cursor_pos = -1;
279         return;
280       }
281       
282       // Only one match - complete directly and update doc string window
283       if (matches.size() == 1)
284       {
285         first_match = matches[0].mid(_compl_after_point.size());
286         cursor.insertText(first_match);
287         _tab_mode = false;
288         if (doc.isEmpty())
289           emit updateDoc(formatDocHTML("(no documentation available)\n"));
290         else
291           emit updateDoc(formatDocHTML(doc));
292       }
293       else
294       {
295         // Detect if there is a common base to all available completion
296         // In this case append this base to the text already
297         extractCommon(matches, base);
298         first_match = base.mid(_compl_after_point.size());
299         cursor.insertText(first_match);
300         
301         // If this happens to match exaclty the first completion
302         // also provide doc
303         if (base == matches[0])
304         {
305           doc = formatDocHTML(doc);
306           emit updateDoc(doc);
307         }
308         
309         // Print all matching completion in a "undo-able" block
310         cursorPos = cursor.position();
311         cursor.insertBlock();
312         cursor.beginEditBlock();
313         
314         // Insert all matches
315         QTextCharFormat cf;
316         cf.setForeground(QBrush(Qt::darkGreen));
317         cursor.setCharFormat(cf);
318         formatCompletion(matches, comple_text);
319         cursor.insertText(comple_text);
320         cursor.endEditBlock();
321         
322         // Position cursor where it was before inserting the completion list:
323         cursor.setPosition(cursorPos);
324         setTextCursor(cursor);
325       }
326       break;
327     }
328     case PyInterp_Event::ES_TAB_COMPLETE_ERR:
329     {
330       // Tab completion was unsuccessful, switch off mode:
331       _tab_mode = false;
332       _cursor_pos = -1;
333       break;
334     }
335     case PyInterp_Event::ES_OK:
336     case PyInterp_Event::ES_ERROR:
337     case PyInterp_Event::ES_INCOMPLETE:
338     {
339       // Before everything else, call super()
340       PyConsole_Editor::customEvent(event);
341       // If we are in multi_paste_mode, process the next item:
342       multiLineProcessNextLine();
343       break;
344     }
345     default:
346     {
347       PyConsole_Editor::customEvent( event );
348       break;
349     }
350   }
351 }
352
353 /**
354  * Extract the common leading part of all strings in matches.
355  * @param matches
356  * @param result
357  */
358 void PyConsole_EnhEditor::extractCommon(const QStringList& matches, QString& result) const
359 {
360   result = "";
361   int charIdx = 0;
362
363   if (matches.size() < 2)
364     return;
365
366   while (true)
367     {
368       if (charIdx >= matches[0].size())
369         return;
370       QChar ch = matches[0][charIdx];
371       for (int j = 1; j < matches.size(); j++)
372         if (charIdx >= matches[j].size() || matches[j][charIdx] != ch)
373           return;
374       result += ch;
375       charIdx++;
376     }
377 }
378
379 /**
380  * Format the doc string in HTML format with the first line in bold blue
381  * @param doc initial doc string
382  * @return HTML string
383  */
384 QString PyConsole_EnhEditor::formatDocHTML(const QString & doc) const
385 {
386   QString templ = QString("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" ") +
387       QString(" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n ") +
388       QString("<html><head><meta name=\"qrichtext\" content=\"1\" /> ") +
389       QString("<style type=\"text/css\">\np, li { white-space: pre-wrap; }\n</style> ") +
390       QString("</head><body style=\" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;\">\n") +
391       QString("<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">") +
392       QString("<span style=\" font-weight:600; color:#0000ff;\">%1</span></p>") +
393       QString("<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">%2</p>") +
394       QString("</body></html>");
395
396   QString fst, rest("");
397
398   // Extract first line of doc
399   int idx = doc.indexOf("\n");
400   if (idx > 0)
401     {
402       fst = doc.left(idx);
403       rest = doc.mid(idx+1);
404     }
405   else
406     {
407       fst = doc;
408     }
409
410   fst = fst.replace("\n", " ");
411   rest = rest.replace("\n", " ");
412   return templ.arg(fst).arg(rest);
413 }
414
415 /**
416  * Handle properly multi-line pasting. Qt4 doc recommends overriding this function.
417  * If the pasted text doesn't contain a line return, no special treatment is done.
418  * @param source
419  */
420 void PyConsole_EnhEditor::insertFromMimeData(const QMimeData * source)
421 {
422   if (_multi_line_paste)
423     return;
424
425   if (source->hasText())
426     {
427       QString s = source->text();
428       if (s.contains("\n"))
429         multilinePaste(s);
430       else
431         PyConsole_Editor::insertFromMimeData(source);
432     }
433   else
434     {
435       PyConsole_Editor::insertFromMimeData(source);
436     }
437 }
438
439
440 void PyConsole_EnhEditor::multilinePaste(const QString & s)
441 {
442   // Turn on multi line pasting mode
443   _multi_line_paste = true;
444
445   // Split the string:
446   QString s2 = s;
447   s2.replace("\r", ""); // Windows string format converted to Unix style
448
449   QStringList lst = s2.split(QChar('\n'), QString::KeepEmptyParts);
450
451   // Perform the proper paste operation for the first line to handle the case where
452   // sth was already there:
453   QMimeData source;
454   source.setText(lst[0]);
455   PyConsole_Editor::insertFromMimeData(&source);
456
457   // Prepare what will have to be executed after the first line:
458   _multi_line_content = std::queue<QString>();
459   for (int i = 1; i < lst.size(); ++i)
460     _multi_line_content.push(lst[i]);
461
462   // Trigger the execution of the first (mixed) line
463   handleReturn();
464
465   // See customEvent() and multiLineProcessNext() for the rest of the handling.
466 }
467
468 /**
469  * Process the next line in the queue of a multiple copy/paste:
470  */
471 void PyConsole_EnhEditor::multiLineProcessNextLine()
472 {
473   if (!_multi_line_paste)
474     return;
475
476   QString line(_multi_line_content.front());
477   _multi_line_content.pop();
478   if (!_multi_line_content.size())
479     {
480       // last line in the queue, just paste it
481       addText(line, false, false);
482       _multi_line_paste = false;
483     }
484   else
485     {
486       // paste the line and simulate a <RETURN> key stroke
487       addText(line, false, false);
488       handleReturn();
489     }
490 }