Salome HOME
Merge branch 'V8_3_asterstudy' into V8_3_BR
[modules/gui.git] / tools / PyEditor / src / PyEditor_FindTool.cxx
1 // Copyright (C) 2015-2016  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_FindTool.cxx
20 // Author : Vadim SANDLER, Open CASCADE S.A.S. (vadim.sandler@opencascade.com)
21 //
22
23 #include "PyEditor_FindTool.h"
24 #include "PyEditor_Editor.h"
25
26 #include <QAction>
27 #include <QCompleter>
28 #include <QEvent>
29 #include <QGridLayout>
30 #include <QIcon>
31 #include <QLabel>
32 #include <QLineEdit>
33 #include <QMenu>
34 #include <QSignalMapper>
35 #include <QToolButton>
36
37 /*!
38   \class PyEditor_FindTool
39   \brief Find / Replace widget for PyEditor
40 */
41
42 /*!
43   \brief Constructor.
44   \param editor Python editor widget.
45   \param parent Parent widget.
46 */
47 PyEditor_FindTool::PyEditor_FindTool( PyEditor_Editor* editor, QWidget* parent )
48   : QWidget( parent ), myEditor( editor )
49 {
50   QLabel* findLabel = new QLabel( tr( "FIND_LABEL" ), this );
51   myFindEdit = new QLineEdit( this );
52   myFindEdit->setClearButtonEnabled( true );
53   myFindEdit->installEventFilter( this );
54   connect( myFindEdit, SIGNAL( textChanged( const QString& ) ), this, SLOT( find( const QString& ) ) );
55   connect( myFindEdit, SIGNAL( returnPressed() ), this, SLOT( findNext() ) );
56   myFindEdit->setCompleter( new QCompleter( myFindEdit ) );
57   myFindEdit->completer()->setModel( &myFindCompletion );
58
59   QLabel* replaceLabel = new QLabel( tr( "REPLACE_LABEL" ), this );
60   myReplaceEdit = new QLineEdit( this );
61   myReplaceEdit->setClearButtonEnabled( true );
62   myReplaceEdit->installEventFilter( this );
63   myReplaceEdit->setCompleter( new QCompleter( myReplaceEdit ) );
64   myReplaceEdit->completer()->setModel( &myReplaceCompletion );
65
66   myInfoLabel = new QLabel( this );
67   myInfoLabel->setAlignment( Qt::AlignVCenter | Qt::AlignRight );
68
69   QToolButton* prevBtn = new QToolButton( this );
70   prevBtn->setIcon( QIcon( ":images/py_find_previous.png" ) );
71   prevBtn->setAutoRaise( true );
72   connect( prevBtn, SIGNAL( clicked() ), this, SLOT( findPrevious() ) );
73
74   QToolButton* nextBtn = new QToolButton( this );
75   nextBtn->setIcon( QIcon( ":images/py_find_next.png" ) );
76   nextBtn->setAutoRaise( true );
77   connect( nextBtn, SIGNAL( clicked() ), this, SLOT( findNext() ) );
78
79   QToolButton* replaceBtn = new QToolButton();
80   replaceBtn->setText( tr( "REPLACE_BTN" ) );
81   replaceBtn->setAutoRaise( true );
82   connect( replaceBtn, SIGNAL( clicked() ), this, SLOT( replace() ) );
83
84   QToolButton* replaceAllBtn = new QToolButton();
85   replaceAllBtn->setText( tr( "REPLACE_ALL_BTN" ) );
86   replaceAllBtn->setAutoRaise( true );
87   connect( replaceAllBtn, SIGNAL( clicked() ), this, SLOT( replaceAll() ) );
88
89   QHBoxLayout* hl = new QHBoxLayout;
90   hl->setContentsMargins( 0, 0, 0, 0 );
91   hl->setSpacing( 0 );
92   hl->addWidget( prevBtn );
93   hl->addWidget( nextBtn );
94
95   QGridLayout* l = new QGridLayout( this );
96   l->setContentsMargins( 6, 2, 6, 2 );
97   l->setSpacing( 2 );
98   l->addWidget( findLabel, 0, 0 );
99   l->addWidget( myFindEdit, 0, 1 );
100   l->addLayout( hl, 0, 2 );
101   l->addWidget( myInfoLabel, 0, 3 );
102   l->addWidget( replaceLabel, 1, 0 );
103   l->addWidget( myReplaceEdit, 1, 1 );
104   l->addWidget( replaceBtn, 1, 2 );
105   l->addWidget( replaceAllBtn, 1, 3 );
106
107   QAction* menuAction = myFindEdit->addAction( QIcon(":images/py_search.png"), QLineEdit::LeadingPosition );
108   connect( menuAction, SIGNAL( triggered( bool ) ), this, SLOT( showMenu() ) );
109
110   addAction( new QAction( tr( "CASE_SENSITIVE_CHECK" ), this ) );
111   addAction( new QAction( tr( "WHOLE_WORDS_CHECK" ), this ) );
112   addAction( new QAction( tr( "REGEX_CHECK" ), this ) );
113   addAction( new QAction( tr( "Find" ), this ) );
114   addAction( new QAction( tr( "FindPrevious" ), this ) );
115   addAction( new QAction( tr( "FindNext" ), this ) );
116   addAction( new QAction( tr( "Replace" ), this ) );
117
118   foreach ( QAction* action, actions().mid( CaseSensitive, RegExp+1 ) )
119   {
120     action->setCheckable( true );
121     connect( action, SIGNAL( toggled( bool ) ), this, SLOT( update() ) );
122   }
123
124   QSignalMapper* mapper = new QSignalMapper( this );
125   connect( mapper, SIGNAL( mapped( int ) ), this, SLOT( activate( int ) ) );
126
127   for ( int i = Find; i < actions().count(); i++ )
128   {
129     QAction* action = actions()[i];
130     action->setShortcuts( shortcuts( i ) );
131     action->setShortcutContext( Qt::WidgetWithChildrenShortcut );
132     connect( action, SIGNAL( triggered( bool ) ), mapper, SLOT( map() ) );
133     mapper->setMapping( action, i );
134     myEditor->addAction( action );
135   }
136
137   myEditor->installEventFilter( this );
138
139   hide();
140 }
141
142 /*!
143   \brief Process events for this widget,
144   \param e Event being processed.
145   \return true if event's processing should be stopped; false otherwise.
146 */
147 bool PyEditor_FindTool::event( QEvent* e )
148 {
149   if ( e->type() == QEvent::EnabledChange )
150   {
151     updateShortcuts();
152   }
153   else if ( e->type() == QEvent::KeyPress )
154   {
155     QKeyEvent* ke = (QKeyEvent*)e;
156     switch ( ke->key() )
157     {
158     case Qt::Key_Escape:
159       hide();
160       return true;
161     default:
162       break;
163     }
164   }
165   else if ( e->type() == QEvent::Hide )
166   {
167     addCompletion( myFindEdit->text(), false );
168     addCompletion( myReplaceEdit->text(), true );
169     myEditor->setFocus();
170   }
171   return QWidget::event( e );
172 }
173
174 /*!
175   \brief Filter events from watched objects.
176   \param o Object being watched.
177   \param e Event being processed.
178   \return true if event should be filtered out; false otherwise.
179 */
180 bool PyEditor_FindTool::eventFilter( QObject* o, QEvent* e )
181 {
182   if ( o == myFindEdit )
183   {
184     if ( e->type() == QEvent::KeyPress )
185     {
186       QKeyEvent* keyEvent = (QKeyEvent*)e;
187       if ( keyEvent->key() == Qt::Key_Escape && !myFindEdit->text().isEmpty() )
188       {
189         addCompletion( myFindEdit->text(), false );
190         myFindEdit->clear();
191         return true;
192       }
193     }
194   }
195   else if ( o == myReplaceEdit )
196   {
197     if ( e->type() == QEvent::KeyPress )
198     {
199       QKeyEvent* keyEvent = (QKeyEvent*)e;
200       if ( keyEvent->key() == Qt::Key_Escape && !myReplaceEdit->text().isEmpty() )
201       {
202         myReplaceEdit->clear();
203         return true;
204       }
205     }
206   }
207   else if ( o == myEditor )
208   {
209     if ( e->type() == QEvent::EnabledChange )
210     {
211       setEnabled( myEditor->isEnabled() );
212     }
213     else if ( e->type() == QEvent::Hide )
214     {
215       hide();
216     }
217     else if ( e->type() == QEvent::KeyPress )
218     {
219       QKeyEvent* ke = (QKeyEvent*)e;
220       switch ( ke->key() )
221       {
222       case Qt::Key_Escape:
223         if ( isVisible() )
224           hide();
225         break;
226       default:
227         break;
228       }
229     }
230   }
231   return QWidget::eventFilter( o, e );
232 }
233
234 /*!
235   \brief Slot: activate 'Find' dialog.
236 */
237 void PyEditor_FindTool::activateFind()
238 {
239   activate( Find );
240 }
241
242 /*!
243   \brief Slot: activate 'Replace' dialog.
244 */
245 void PyEditor_FindTool::activateReplace()
246 {
247   activate( Replace );
248 }
249
250 /*!
251   \brief Slot: show context menu with search options.
252   \internal
253 */
254 void PyEditor_FindTool::showMenu()
255 {
256   QMenu::exec( actions().mid( CaseSensitive, RegExp+1 ), QCursor::pos() );
257 }
258  
259 /*!
260   \brief Slot: find text being typed in the 'Find' control.
261   \param text Text entered by the user.
262   \internal
263 */
264 void PyEditor_FindTool::find( const QString& text )
265 {
266   find( text, 0 );
267 }
268
269 /*!
270   \brief Slot: find text entered in the 'Find' control.
271   \internal
272   \overload
273 */
274 void PyEditor_FindTool::find()
275 {
276   find( myFindEdit->text(), 0 );
277 }
278
279 /*!
280   \brief Slot: find previous matched item; called when user presses 'Previous' button.
281   \internal
282 */
283 void PyEditor_FindTool::findPrevious()
284 {
285   find( myFindEdit->text(), -1 );
286 }
287
288 /*!
289   \brief Slot: find next matched item; called when user presses 'Next' button.
290   \internal
291 */
292 void PyEditor_FindTool::findNext()
293 {
294   find( myFindEdit->text(), 1 );
295 }
296
297 /*!
298   \brief Slot: replace currently selected match; called when user presses 'Replace' button.
299   \internal
300 */
301 void PyEditor_FindTool::replace()
302 {
303   QString text = myFindEdit->text();
304   QString replacement = myReplaceEdit->text();
305
306   QTextCursor editor = myEditor->textCursor();
307   if ( editor.hasSelection() && editor.selectedText() == text )
308   {
309     editor.beginEditBlock();
310     editor.removeSelectedText();
311     editor.insertText( replacement );
312     editor.endEditBlock();
313     find();
314   }
315 }
316
317 /*!
318   \brief Slot: replace all matches; called when user presses 'Replace All' button.
319   \internal
320 */
321 void PyEditor_FindTool::replaceAll()
322 {
323   QString text = myFindEdit->text();
324   QString replacement = myReplaceEdit->text();
325   QList<QTextCursor> results = matches( text );
326   if ( !results.isEmpty() )
327   {
328     QTextCursor editor( myEditor->document() );
329     editor.beginEditBlock();
330     foreach ( QTextCursor cursor, results )
331     {
332       editor.setPosition( cursor.anchor() );
333       editor.setPosition( cursor.position(), QTextCursor::KeepAnchor );
334       editor.removeSelectedText();
335       editor.insertText( replacement );
336     }
337     editor.endEditBlock();
338     find();
339   }
340 }
341
342 /*!
343   \brief Slot: restart search; called when search options are changed.
344   \internal
345 */
346 void PyEditor_FindTool::update()
347 {
348   find();
349 }
350
351 /*!
352   \brief Slot: activate action; called when user types corresponding shortcut.
353   \param action Action being activated.
354   \internal
355 */
356 void PyEditor_FindTool::activate( int action )
357 {
358   QTextCursor cursor = myEditor->textCursor();
359   cursor.movePosition( QTextCursor::StartOfWord );
360   cursor.movePosition( QTextCursor::EndOfWord, QTextCursor::KeepAnchor );
361   QString word = cursor.selectedText();
362
363   switch ( action )
364   {
365   case Find:
366   case Replace:
367     showReplaceControls( action == Replace );
368     show();
369     if ( !word.isEmpty() ) {
370       myFindEdit->setText( word );
371       myEditor->setTextCursor( cursor );
372     }
373     myFindEdit->setFocus();
374     myFindEdit->selectAll();
375     find( myFindEdit->text() );
376     break;
377   case FindPrevious:
378     findPrevious();
379     break;
380   case FindNext:
381     findNext();
382     break;
383   default:
384     break;
385   }
386 }
387
388 /*!
389   \brief Get shortcuts for given action.
390   \param action Editor's action.
391   \return List of shortcuts.
392   \internal
393 */
394 QList<QKeySequence> PyEditor_FindTool::shortcuts( int action ) const
395 {
396   QList<QKeySequence> bindings;
397   switch ( action )
398   {
399   case Find:
400     bindings << QKeySequence( QKeySequence::Find );
401     break;
402   case FindPrevious:
403     bindings << QKeySequence( QKeySequence::FindPrevious );
404     break;
405   case FindNext:
406     bindings << QKeySequence( QKeySequence::FindNext );
407     break;
408   case Replace:
409     bindings << QKeySequence( QKeySequence::Replace );
410     bindings << QKeySequence( "Ctrl+H" );
411     break;
412   default:
413     break;
414   }
415   return bindings;
416 }
417
418 /*!
419   \brief Update shortcuts when widget is enabled / disabled.
420   \internal
421 */
422 void PyEditor_FindTool::updateShortcuts()
423 {
424   foreach ( QAction* action, actions().mid( Find ) )
425   {
426     action->setEnabled( isEnabled() && myEditor->isEnabled() );
427   }
428 }
429
430 /*!
431   \brief Show / hide 'Replace' controls.
432   \param on Visibility flag.
433   \internal
434 */
435 void PyEditor_FindTool::showReplaceControls( bool on )
436 {
437   QGridLayout* l = qobject_cast<QGridLayout*>( layout() );
438   for ( int j = 0; j < l->columnCount(); j++ )
439   {
440     if ( l->itemAtPosition( 1, j )->widget() )
441       l->itemAtPosition( 1, j )->widget()->setVisible( on );
442   }
443 }
444
445 /*!
446   \brief Set palette for 'Find' tool depending on results of search.
447   \param found Search result: true in case of success; false otherwise.
448   \internal
449 */
450 void PyEditor_FindTool::setSearchResult( bool found )
451 {
452   QPalette pal = myFindEdit->palette();
453   QPalette ref = myReplaceEdit->palette();
454   pal.setColor( QPalette::Active, QPalette::Text,
455                 found ? ref.color( QPalette::Active, QPalette::Text ) : QColor( 255, 0, 0 ) );
456   myFindEdit->setPalette( pal );
457 }
458
459 /*!
460   \brief Get 'Use regular expression' search option.
461   \return true if option is switched on; false otherwise.
462   \internal
463 */
464 bool PyEditor_FindTool::isRegExp() const
465 {
466   return actions()[RegExp]->isChecked();
467 }
468
469 /*!
470   \brief Get 'Case sensitive search' search option.
471   \return true if option is switched on; false otherwise.
472   \internal
473 */
474 bool PyEditor_FindTool::isCaseSensitive() const
475 {
476   return actions()[CaseSensitive]->isChecked();
477 }
478
479 /*!
480   \brief Get 'Whole words only' search option.
481   \return true if option is switched on; false otherwise.
482   \internal
483 */
484 bool PyEditor_FindTool::isWholeWord() const
485 {
486   return actions()[WholeWord]->isChecked();
487 }
488
489 /*!
490   \brief Get search options.
491   \param back Search direction: backward if false; forward otherwise.
492   \return List of options
493   \internal
494 */
495 QTextDocument::FindFlags PyEditor_FindTool::searchFlags( bool back ) const
496 {
497   QTextDocument::FindFlags flags = 0;
498   if ( isCaseSensitive() )
499     flags |= QTextDocument::FindCaseSensitively;
500   if ( isWholeWord() )
501     flags |= QTextDocument::FindWholeWords;
502   if ( back )
503     flags |= QTextDocument::FindBackward;
504   return flags;
505 }
506
507 /*!
508   \brief Get all matches from Python editor.
509   \param text Text being searched.
510   \return List of all matches.
511   \internal
512 */
513 QList<QTextCursor> PyEditor_FindTool::matches( const QString& text ) const
514 {
515   QList<QTextCursor> results;
516
517   QTextDocument* document = myEditor->document();
518
519   QTextCursor cursor( document );
520   while ( !cursor.isNull() )
521   {
522     cursor = isRegExp() ? 
523       document->find( QRegExp( text, isCaseSensitive() ?
524                                Qt::CaseSensitive : Qt::CaseInsensitive ),
525                       cursor, searchFlags() ) : 
526       document->find( text, cursor, searchFlags() );
527     if ( !cursor.isNull() )
528       results.append( cursor );
529   }
530   return results;
531 }
532
533 /*!
534   \brief Find specified text.
535   \param text Text being searched.
536   \param delta Search direction.
537   \internal
538 */
539 void PyEditor_FindTool::find( const QString& text, int delta )
540 {
541   QTextCursor cursor = myEditor->textCursor();
542   int position = qMin( cursor.position(), cursor.anchor() ) + delta;
543   cursor.setPosition( position );
544   myEditor->setTextCursor( cursor );
545
546   QList<QTextCursor> results = matches( text );
547
548   int index = -1;
549   if ( !results.isEmpty() )
550   {
551     if ( delta >= 0 )
552     {
553       // search forward
554       if ( position > results.last().anchor() )
555         position = 0;
556       for ( int i = 0; i < results.count() && index == -1; i++ )
557       {
558         QTextCursor result = results[i];
559         if ( result.hasSelection() && position <= result.anchor() )
560         {
561           index = i;
562         }
563       }
564     }
565     else
566     {
567       // search backward
568       if ( position < results.first().position() )
569         position = results.last().position();
570
571       for ( int i = results.count()-1; i >= 0 && index == -1; i-- )
572       {
573         QTextCursor result = results[i];
574         if ( result.hasSelection() && position >= result.position() )
575         {
576           index = i;
577         }
578       }
579     }
580   }
581   if ( index != -1 )
582   {
583     myInfoLabel->setText( tr( "NB_MATCHED_LABEL" ).arg( index+1 ).arg( results.count() ) );
584     myEditor->setTextCursor( results[index] );
585   }
586   else
587   {
588     myInfoLabel->clear();
589     cursor.clearSelection();
590     myEditor->setTextCursor( cursor );
591   }
592
593   setSearchResult( text.isEmpty() || !results.isEmpty() );
594 }
595
596 /*!
597   \brief Add completion.
598   \param text Completeion being added.
599   \param replace true to add 'Replace' completion; false to add 'Find' completion.
600   \internal
601 */
602 void PyEditor_FindTool::addCompletion( const QString& text, bool replace )
603 {
604   QStringListModel& model = replace ? myReplaceCompletion : myFindCompletion;
605
606   QStringList completions = model.stringList();
607   if ( !text.isEmpty() and !completions.contains( text ) )
608   {
609     completions.prepend( text );
610     model.setStringList( completions );
611   }
612 }