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