Salome HOME
787f9f4fa416116366723def1b451acf278b43e4
[modules/gui.git] / tools / PyEditor / src / PyEditor_Completer.cxx
1 // Copyright (C) 2015-2023  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 // File   : PyEditor_Completer.cxx
20 // Author : Sergey TELKOV, Open CASCADE S.A.S. (sergey.telkov@opencascade.com)
21 //
22
23 #include "PyEditor_Completer.h"
24
25 #include "PyEditor_Editor.h"
26 #include "PyEditor_Keywords.h"
27
28 #include <QSet>
29 #include <QTimer>
30 #include <QTextBlock>
31 #include <QTextCursor>
32 #include <QApplication>
33 #include <QAbstractItemView>
34 #include <QStandardItemModel>
35
36 /*!
37   \brief Constructor.
38 */
39 PyEditor_Completer::PyEditor_Completer( PyEditor_Editor* editor,
40                                         PyEditor_Keywords* std, PyEditor_Keywords* user )
41   : QCompleter( editor ),
42     myEditor( editor ),
43     myTimer( 0 ),
44     myStdKeywords( std ),
45     myUserKeywords( user )
46 {
47   setWidget( editor );
48   setCompletionMode(QCompleter::PopupCompletion);
49
50   connect(this, SIGNAL( activated( const QString& ) ),
51           this, SLOT( onActivated( const QString& ) ) );
52   connect(editor, SIGNAL( textChanged() ), this, SLOT( onTextChanged() ) );
53   connect(myStdKeywords, SIGNAL( keywordsChanged() ),
54           this, SLOT( onKeywordsChanged() ) );
55   connect(myUserKeywords, SIGNAL( keywordsChanged() ),
56           this, SLOT( onKeywordsChanged() ) );
57
58   updateKeywords();
59 }
60
61 /*!
62   \brief Destructor.
63 */
64 PyEditor_Completer::~PyEditor_Completer()
65 {
66 }
67
68 /*!
69   \brief Perform the completion if it possible.
70 */
71 void PyEditor_Completer::perform()
72 {
73   QString prefix = completionText();
74   setCompletionPrefix( prefix );
75
76   if ( !completionPrefix().isEmpty() && 
77        ( completionCount() > 1 || ( completionCount() == 1 && 
78                                     currentCompletion() != completionPrefix() ) ) )
79     complete(completionRect());
80   else
81     uncomplete();
82 }
83
84 /*!
85   \brief Hide the completer's popup menu.
86 */
87 void PyEditor_Completer::uncomplete()
88 {
89   if ( popup() )
90     popup()->hide();
91 }
92
93 /*!
94   \brief Handling 'Enter' key.
95 */
96 bool PyEditor_Completer::eventFilter(QObject* o, QEvent* e)
97 {
98   bool res = false;
99   if ( e->type() == QEvent::KeyPress && popup()->isVisible() ) {
100     QKeyEvent* ke = (QKeyEvent*)e;
101     if ( ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return ) {
102       res = true;
103       setCurrentRow(popup()->currentIndex().row());
104       onActivated(currentCompletion());
105     }
106   }
107
108   if ( !res )
109     res = QCompleter::eventFilter(o, e);
110
111   return res;
112 }
113
114 /*!
115   \brief Perform delayed completion.
116 */
117 void PyEditor_Completer::onTimeout()
118 {
119   perform();
120 }
121
122 /*!
123   \brief Invoked when text changed in editor.
124 */
125 void PyEditor_Completer::onTextChanged()
126 {
127   uncomplete();
128   if ( myEditor->completionPolicy() == PyEditor_Editor::Auto ||
129        myEditor->completionPolicy() == PyEditor_Editor::Always )
130     triggerComplete();
131 }
132
133 /*!
134   \brief Invoked when keywords changed in editor.
135 */
136 void PyEditor_Completer::onKeywordsChanged()
137 {
138   updateKeywords();
139 }
140
141 /*!
142   \brief Insert selected completion into editor.
143 */
144 void PyEditor_Completer::onActivated( const QString& text)
145 {
146   QPoint rng = completionRange();
147   QTextCursor cursor = myEditor->textCursor();
148   cursor.setPosition(cursor.position() - rng.y() + rng.x() - 1,
149                      QTextCursor::KeepAnchor);
150   cursor.insertText(text);
151   uncomplete();
152 }
153
154 /*!
155   \brief Get the rectangle for completion popup.
156   \return completion popup rect
157 */
158 QRect PyEditor_Completer::completionRect() const
159 {
160   QRect res = myEditor->cursorRect(myEditor->textCursor());
161   res.setWidth(popup()->sizeHint().width());
162   res.translate(myEditor->document()->documentMargin(),
163                 myEditor->document()->documentMargin());
164   return res;
165 }
166
167 /*!
168   \brief Get the current completion prefix from editor.
169   \return completion prefix string
170 */
171 QString PyEditor_Completer::completionText() const
172 {
173   QString prefix;
174   if ( myEditor ) {
175     QString txt = myEditor->textCursor().block().text();
176     if ( !txt.isEmpty() ) {
177       QPoint range = completionRange();
178       prefix = txt.mid( range.x(), range.y() - range.x() + 1 );
179     }
180   }
181   return prefix;
182 }
183
184 /*!
185   \brief Get position of completion prefix in editor.
186   \return begin and end of completion prefix
187 */
188 QPoint PyEditor_Completer::completionRange() const
189 {
190   QPoint range;
191
192   if ( myEditor ) {
193     QTextCursor cursor = myEditor->textCursor();
194     QString txt = cursor.block().text();
195     int beg = 0;
196     int end = cursor.positionInBlock() - 1;
197
198     QRegExp rx("[A-Za-z]{1}\\w*$");
199     int pos = rx.indexIn(txt.mid(beg, end - beg + 1));
200
201     if ( pos >= 0 )
202       beg = pos;
203
204     range = QPoint(beg, end);
205   }
206
207   return range;
208 }
209
210 /*!
211   \brief Schedule the delayed completion.
212 */
213 void PyEditor_Completer::triggerComplete()
214 {
215   if ( !myTimer ) {
216     myTimer = new QTimer( this );
217     myTimer->setSingleShot( true );
218     myTimer->setInterval( 200 );
219
220     connect( myTimer, SIGNAL( timeout() ), this, SLOT( onTimeout() ) );
221   }
222
223   if ( myTimer->isActive() )
224     myTimer->stop();
225   myTimer->start();
226 }
227
228 /*!
229   \brief Updates the keywords list in completer.
230 */
231 void PyEditor_Completer::updateKeywords()
232 {
233   QStandardItemModel* model = new QStandardItemModel( this );
234   KeywordMap kwMap = keywords();
235   for ( KeywordMap::const_iterator it = kwMap.begin(); it != kwMap.end(); ++it ) {
236     QStandardItem* item = new QStandardItem( it.key() );
237     if ( it.value().isValid() )
238       item->setForeground( it.value() );
239     model->appendRow( item );
240   }
241   setModel( model );
242 }
243
244 /*!
245   \brief Gets the keywords list.
246   \return keyword string list
247 */
248 PyEditor_Completer::KeywordMap PyEditor_Completer::keywords() const
249 {
250   KeywordMap res;
251   QList<PyEditor_Keywords*> kwDicts;
252   kwDicts << myStdKeywords << myUserKeywords;
253
254   for ( QList<PyEditor_Keywords*>::iterator itr = kwDicts.begin(); itr != kwDicts.end(); ++itr ) {
255     PyEditor_Keywords* dict = *itr;
256     QStringList kwList = dict->keywords();
257     for ( QStringList::const_iterator it = kwList.begin(); it != kwList.end(); ++it ) {
258       if ( !res.contains( *it ) ) {
259         res.insert( *it, dict->color( *it ) );
260       }
261     }
262   }
263   return res;
264 }