Salome HOME
Revert "Synchronize adm files"
[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_EnhInterp * 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       static_cast<PyConsole_EnhInterp *>(myInterp)->clearCompletion();
121     }
122   _tab_mode = false;
123 }
124
125 /**
126  * Handle the sequence of events after having hit <TAB>
127  */
128 void PyConsole_EnhEditor::handleTab()
129 {
130   if (_tab_mode)
131     {
132       // Already tab mode - nothing to do !
133       return;
134     }
135
136   QTextCursor cursor(textCursor());
137
138   // Cursor at end of input
139   cursor.movePosition(QTextCursor::End);
140   setTextCursor(cursor);
141
142   // Save cursor position if needed
143   if (_cursor_pos == -1)
144     _cursor_pos = textCursor().position();
145
146   // get last line
147   QTextBlock par = document()->end().previous();
148   if ( !par.isValid() ) return;
149
150   // Switch to completion mode
151   _tab_mode = true;
152
153   QString cmd = par.text().mid(promptSize());
154
155   // Post completion request
156   // Editor will be informed via a custom event that completion has been run
157   PyInterp_Request* req = createTabRequest(cmd);
158   PyInterp_Dispatcher::Get()->Exec(req);
159 }
160
161 /**
162  * Handles what happens after hitting Ctrl-TAB
163  */
164 void PyConsole_EnhEditor::handleBackTab()
165 {
166   QTextCursor cursor(textCursor());
167
168   if (_cursor_pos == -1)
169     {
170       // Invalid cursor position - we can't do anything:
171       return;
172     }
173   // Ensure cursor is at the end of command line
174   cursor.setPosition(_cursor_pos);
175   cursor.movePosition(QTextCursor::EndOfLine);
176   //setCursor(cursor);
177
178   // Delete last completed text
179   int i = cursor.position() - _cursor_pos;
180   cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, i);
181   cursor.removeSelectedText();
182   _cursor_pos = -1;
183 }
184
185 /**
186  * Create the Python requested that will be posted to the interpreter to
187  * get the completions.
188  * @param input line typed by the user at the time TAB was hit
189  * @return a CompletionCommand
190  * @sa CompletionCommand
191  */
192 PyInterp_Request* PyConsole_EnhEditor::createTabRequest( const QString& input )
193 {
194   // Parse input to extract on what part the dir() has to be executed
195   QString input2(input);
196
197   // Split up to the last syntaxical separator
198   int lastSp = -1;
199   for (std::vector<QString>::const_iterator i = SEPARATORS.begin(); i != SEPARATORS.end(); i++)
200     {
201       int j = input2.lastIndexOf(*i);
202       if (j > lastSp)
203         lastSp = j;
204     }
205   if (lastSp >= 0)
206     input2 = input.mid(lastSp+1);
207
208   // Detect a qualified name (with a point)
209   int lastPt = input2.lastIndexOf(QString("."));
210
211   // Split the 2 surrounding parts of the qualified name
212   if (lastPt != -1)
213     {
214       _compl_before_point = input2.left(lastPt);
215       _compl_after_point = input2.mid(lastPt+1);
216     }
217   else
218     {
219       // No point found - do a global matching -
220       // (the following will call dir() with an empty string)
221       _compl_after_point = input2;
222       _compl_before_point = QString("");
223     }
224
225   return new CompletionCommand( static_cast<PyConsole_EnhInterp *>(myInterp), _compl_before_point,
226                                _compl_after_point, this, isSync() );
227 }
228
229 /**
230  * Format completion results - this is where we should create 3 columns etc ...
231  * @param matches list of possible completions
232  * @param result return value
233  */
234 void PyConsole_EnhEditor::formatCompletion(const std::vector<QString> & matches, QString & result) const
235 {
236   int sz = matches.size();
237
238   if (sz > MAX_COMPLETIONS)
239     {
240       sz = MAX_COMPLETIONS;
241       result.append("[Too many matches! Displaying first ones only ...]\n");
242     }
243
244   for (int i = 0; i < sz; ++i)
245     {
246       result.append(matches[i]);
247       result.append("\n");
248     }
249 }
250
251 /**
252  * Override. Catches the events generated by the enhanced interpreter after the execution
253  * of a completion request.
254  * @param event
255  */
256 void PyConsole_EnhEditor::customEvent( QEvent* event )
257 {
258   std::vector<QString> matches;
259   QString first_match, comple_text, doc, base;
260   QTextCursor cursor(textCursor());
261   QTextBlockFormat bf;
262   QTextCharFormat cf;
263   PyConsole_EnhInterp * interp = static_cast<PyConsole_EnhInterp *>(myInterp);
264   int cursorPos;
265
266   switch( event->type() )
267   {
268     case PyInterp_Event::ES_TAB_COMPLETE_OK:
269       // Extract corresponding matches from the interpreter
270       matches = interp->getLastMatches();
271
272       if (matches.size() == 0)
273         {
274           // Completion successful but nothing returned.
275           _tab_mode = false;
276           _cursor_pos = -1;
277           return;
278         }
279
280       // Only one match - complete directly and update doc string window
281       doc = interp->getDocStr();
282       if (matches.size() == 1)
283         {
284           first_match = matches[0].mid(_compl_after_point.size());
285           cursor.insertText(first_match);
286           _tab_mode = false;
287           if (doc == QString(""))
288             emit updateDoc(formatDocHTML("(no documentation available)\n"));
289           else
290             emit updateDoc(formatDocHTML(doc));
291         }
292       else
293         {
294           // Detect if there is a common base to all available completion
295           // In this case append this base to the text already
296           extractCommon(matches, base);
297           first_match = base.mid(_compl_after_point.size());
298           cursor.insertText(first_match);
299
300           // If this happens to match exaclty the first completion
301           // also provide doc
302           if (base == matches[0])
303             {
304               doc = formatDocHTML(doc);
305               emit updateDoc(doc);
306             }
307
308           // Print all matching completion in a "undo-able" block
309           cursorPos = cursor.position();
310           cursor.insertBlock();
311           cursor.beginEditBlock();
312
313           // Insert all matches
314           QTextCharFormat cf;
315           cf.setForeground(QBrush(Qt::darkGreen));
316           cursor.setCharFormat(cf);
317           formatCompletion(matches, comple_text);
318           cursor.insertText(comple_text);
319           cursor.endEditBlock();
320
321           // Position cursor where it was before inserting the completion list:
322           cursor.setPosition(cursorPos);
323           setTextCursor(cursor);
324         }
325       break;
326     case PyInterp_Event::ES_TAB_COMPLETE_ERR:
327       // Tab completion was unsuccessful, switch off mode:
328       _tab_mode = false;
329       _cursor_pos = -1;
330       break;
331     case PyInterp_Event::ES_OK:
332     case PyInterp_Event::ES_ERROR:
333     case PyInterp_Event::ES_INCOMPLETE:
334       // Before everything else, call super()
335       PyConsole_Editor::customEvent(event);
336       // If we are in multi_paste_mode, process the next item:
337       multiLineProcessNextLine();
338       break;
339     default:
340       PyConsole_Editor::customEvent( event );
341       break;
342   }
343 }
344
345 /**
346  * Extract the common leading part of all strings in matches.
347  * @param matches
348  * @param result
349  */
350 void PyConsole_EnhEditor::extractCommon(const std::vector<QString> & matches, QString & result) const
351 {
352   result = "";
353   int charIdx = 0;
354
355   if (matches.size() < 2)
356     return;
357
358   while (true)
359     {
360       if (charIdx >= matches[0].size())
361         return;
362       QChar ch = matches[0][charIdx];
363       for (int j = 1; j < matches.size(); j++)
364         if (charIdx >= matches[j].size() || matches[j][charIdx] != ch)
365           return;
366       result += ch;
367       charIdx++;
368     }
369 }
370
371 /**
372  * Format the doc string in HTML format with the first line in bold blue
373  * @param doc initial doc string
374  * @return HTML string
375  */
376 QString PyConsole_EnhEditor::formatDocHTML(const QString & doc) const
377 {
378   QString templ = QString("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" ") +
379       QString(" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n ") +
380       QString("<html><head><meta name=\"qrichtext\" content=\"1\" /> ") +
381       QString("<style type=\"text/css\">\np, li { white-space: pre-wrap; }\n</style> ") +
382       QString("</head><body style=\" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;\">\n") +
383       QString("<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">") +
384       QString("<span style=\" font-weight:600; color:#0000ff;\">%1</span></p>") +
385       QString("<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">%2</p>") +
386       QString("</body></html>");
387
388   QString fst, rest("");
389
390   // Extract first line of doc
391   int idx = doc.indexOf("\n");
392   if (idx > 0)
393     {
394       fst = doc.left(idx);
395       rest = doc.mid(idx+1);
396     }
397   else
398     {
399       fst = doc;
400     }
401
402   fst = fst.replace("\n", " ");
403   rest = rest.replace("\n", " ");
404   return templ.arg(fst).arg(rest);
405 }
406
407 /**
408  * Handle properly multi-line pasting. Qt4 doc recommends overriding this function.
409  * If the pasted text doesn't contain a line return, no special treatment is done.
410  * @param source
411  */
412 void PyConsole_EnhEditor::insertFromMimeData(const QMimeData * source)
413 {
414   if (_multi_line_paste)
415     return;
416
417   if (source->hasText())
418     {
419       QString s = source->text();
420       if (s.contains("\n"))
421         multilinePaste(s);
422       else
423         PyConsole_Editor::insertFromMimeData(source);
424     }
425   else
426     {
427       PyConsole_Editor::insertFromMimeData(source);
428     }
429 }
430
431
432 void PyConsole_EnhEditor::multilinePaste(const QString & s)
433 {
434   // Turn on multi line pasting mode
435   _multi_line_paste = true;
436
437   // Split the string:
438   QString s2 = s;
439   s2.replace("\r", ""); // Windows string format converted to Unix style
440
441   QStringList lst = s2.split(QChar('\n'), QString::KeepEmptyParts);
442
443   // Perform the proper paste operation for the first line to handle the case where
444   // sth was already there:
445   QMimeData source;
446   source.setText(lst[0]);
447   PyConsole_Editor::insertFromMimeData(&source);
448
449   // Prepare what will have to be executed after the first line:
450   _multi_line_content = std::queue<QString>();
451   for (int i = 1; i < lst.size(); ++i)
452     _multi_line_content.push(lst[i]);
453
454   // Trigger the execution of the first (mixed) line
455   handleReturn();
456
457   // See customEvent() and multiLineProcessNext() for the rest of the handling.
458 }
459
460 /**
461  * Process the next line in the queue of a multiple copy/paste:
462  */
463 void PyConsole_EnhEditor::multiLineProcessNextLine()
464 {
465   if (!_multi_line_paste)
466     return;
467
468   QString line(_multi_line_content.front());
469   _multi_line_content.pop();
470   if (!_multi_line_content.size())
471     {
472       // last line in the queue, just paste it
473       addText(line, false, false);
474       _multi_line_paste = false;
475     }
476   else
477     {
478       // paste the line and simulate a <RETURN> key stroke
479       addText(line, false, false);
480       handleReturn();
481     }
482 }