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