1 // Copyright (C) 2015-2016 OPEN CASCADE
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.
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.
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
17 // See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
19 // File : PyEditor_FindTool.cxx
20 // Author : Vadim SANDLER, Open CASCADE S.A.S. (vadim.sandler@opencascade.com)
23 #include "PyEditor_FindTool.h"
24 #include "PyEditor_Editor.h"
29 #include <QGridLayout>
34 #include <QSignalMapper>
35 #include <QToolButton>
38 \class PyEditor_FindTool
39 \brief Find / Replace widget for PyEditor
44 \param editor Python editor widget.
45 \param parent Parent widget.
47 PyEditor_FindTool::PyEditor_FindTool( PyEditor_Editor* editor, QWidget* parent )
48 : QWidget( parent ), myEditor( editor )
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 );
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 );
66 myInfoLabel = new QLabel( this );
67 myInfoLabel->setAlignment( Qt::AlignVCenter | Qt::AlignRight );
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() ) );
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() ) );
79 QToolButton* replaceBtn = new QToolButton();
80 replaceBtn->setText( tr( "REPLACE_BTN" ) );
81 replaceBtn->setAutoRaise( true );
82 connect( replaceBtn, SIGNAL( clicked() ), this, SLOT( replace() ) );
84 QToolButton* replaceAllBtn = new QToolButton();
85 replaceAllBtn->setText( tr( "REPLACE_ALL_BTN" ) );
86 replaceAllBtn->setAutoRaise( true );
87 connect( replaceAllBtn, SIGNAL( clicked() ), this, SLOT( replaceAll() ) );
89 QHBoxLayout* hl = new QHBoxLayout;
90 hl->setContentsMargins( 0, 0, 0, 0 );
92 hl->addWidget( prevBtn );
93 hl->addWidget( nextBtn );
95 QGridLayout* l = new QGridLayout( this );
96 l->setContentsMargins( 6, 2, 6, 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 );
107 QAction* menuAction = myFindEdit->addAction( QIcon(":images/py_search.png"), QLineEdit::LeadingPosition );
108 connect( menuAction, SIGNAL( triggered( bool ) ), this, SLOT( showMenu() ) );
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 ) );
118 foreach ( QAction* action, actions().mid( CaseSensitive, RegExp+1 ) )
120 action->setCheckable( true );
121 connect( action, SIGNAL( toggled( bool ) ), this, SLOT( update() ) );
124 QSignalMapper* mapper = new QSignalMapper( this );
125 connect( mapper, SIGNAL( mapped( int ) ), this, SLOT( activate( int ) ) );
127 for ( int i = Find; i < actions().count(); i++ )
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 );
137 myEditor->installEventFilter( this );
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.
147 bool PyEditor_FindTool::event( QEvent* e )
149 if ( e->type() == QEvent::EnabledChange )
153 else if ( e->type() == QEvent::KeyPress )
155 QKeyEvent* ke = (QKeyEvent*)e;
165 else if ( e->type() == QEvent::Hide )
167 addCompletion( myFindEdit->text(), false );
168 addCompletion( myReplaceEdit->text(), true );
169 myEditor->setFocus();
171 return QWidget::event( e );
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.
180 bool PyEditor_FindTool::eventFilter( QObject* o, QEvent* e )
182 if ( o == myFindEdit )
184 if ( e->type() == QEvent::KeyPress )
186 QKeyEvent* keyEvent = (QKeyEvent*)e;
187 if ( keyEvent->key() == Qt::Key_Escape && !myFindEdit->text().isEmpty() )
189 addCompletion( myFindEdit->text(), false );
195 else if ( o == myReplaceEdit )
197 if ( e->type() == QEvent::KeyPress )
199 QKeyEvent* keyEvent = (QKeyEvent*)e;
200 if ( keyEvent->key() == Qt::Key_Escape && !myReplaceEdit->text().isEmpty() )
202 myReplaceEdit->clear();
207 else if ( o == myEditor )
209 if ( e->type() == QEvent::EnabledChange )
211 setEnabled( myEditor->isEnabled() );
213 else if ( e->type() == QEvent::Hide )
217 else if ( e->type() == QEvent::KeyPress )
219 QKeyEvent* ke = (QKeyEvent*)e;
231 return QWidget::eventFilter( o, e );
235 \brief Slot: activate 'Find' dialog.
237 void PyEditor_FindTool::activateFind()
243 \brief Slot: activate 'Replace' dialog.
245 void PyEditor_FindTool::activateReplace()
251 \brief Slot: show context menu with search options.
254 void PyEditor_FindTool::showMenu()
256 QMenu::exec( actions().mid( CaseSensitive, RegExp+1 ), QCursor::pos() );
260 \brief Slot: find text being typed in the 'Find' control.
261 \param text Text entered by the user.
264 void PyEditor_FindTool::find( const QString& text )
270 \brief Slot: find text entered in the 'Find' control.
274 void PyEditor_FindTool::find()
276 find( myFindEdit->text(), 0 );
280 \brief Slot: find previous matched item; called when user presses 'Previous' button.
283 void PyEditor_FindTool::findPrevious()
285 find( myFindEdit->text(), -1 );
289 \brief Slot: find next matched item; called when user presses 'Next' button.
292 void PyEditor_FindTool::findNext()
294 find( myFindEdit->text(), 1 );
298 \brief Slot: replace currently selected match; called when user presses 'Replace' button.
301 void PyEditor_FindTool::replace()
303 QString text = myFindEdit->text();
304 QString replacement = myReplaceEdit->text();
306 QTextCursor editor = myEditor->textCursor();
307 if ( editor.hasSelection() && editor.selectedText() == text )
309 editor.beginEditBlock();
310 editor.removeSelectedText();
311 editor.insertText( replacement );
312 editor.endEditBlock();
318 \brief Slot: replace all matches; called when user presses 'Replace All' button.
321 void PyEditor_FindTool::replaceAll()
323 QString text = myFindEdit->text();
324 QString replacement = myReplaceEdit->text();
325 QList<QTextCursor> results = matches( text );
326 if ( !results.isEmpty() )
328 QTextCursor editor( myEditor->document() );
329 editor.beginEditBlock();
330 foreach ( QTextCursor cursor, results )
332 editor.setPosition( cursor.anchor() );
333 editor.setPosition( cursor.position(), QTextCursor::KeepAnchor );
334 editor.removeSelectedText();
335 editor.insertText( replacement );
337 editor.endEditBlock();
343 \brief Slot: restart search; called when search options are changed.
346 void PyEditor_FindTool::update()
352 \brief Slot: activate action; called when user types corresponding shortcut.
353 \param action Action being activated.
356 void PyEditor_FindTool::activate( int action )
358 QTextCursor cursor = myEditor->textCursor();
359 cursor.movePosition( QTextCursor::StartOfWord );
360 cursor.movePosition( QTextCursor::EndOfWord, QTextCursor::KeepAnchor );
361 QString word = cursor.selectedText();
367 showReplaceControls( action == Replace );
369 if ( !word.isEmpty() ) {
370 myFindEdit->setText( word );
371 myEditor->setTextCursor( cursor );
373 myFindEdit->setFocus();
374 myFindEdit->selectAll();
375 find( myFindEdit->text() );
389 \brief Get shortcuts for given action.
390 \param action Editor's action.
391 \return List of shortcuts.
394 QList<QKeySequence> PyEditor_FindTool::shortcuts( int action ) const
396 QList<QKeySequence> bindings;
400 bindings << QKeySequence( QKeySequence::Find );
403 bindings << QKeySequence( QKeySequence::FindPrevious );
406 bindings << QKeySequence( QKeySequence::FindNext );
409 bindings << QKeySequence( QKeySequence::Replace );
410 bindings << QKeySequence( "Ctrl+H" );
419 \brief Update shortcuts when widget is enabled / disabled.
422 void PyEditor_FindTool::updateShortcuts()
424 foreach ( QAction* action, actions().mid( Find ) )
426 action->setEnabled( isEnabled() && myEditor->isEnabled() );
431 \brief Show / hide 'Replace' controls.
432 \param on Visibility flag.
435 void PyEditor_FindTool::showReplaceControls( bool on )
437 QGridLayout* l = qobject_cast<QGridLayout*>( layout() );
438 for ( int j = 0; j < l->columnCount(); j++ )
440 if ( l->itemAtPosition( 1, j )->widget() )
441 l->itemAtPosition( 1, j )->widget()->setVisible( on );
446 \brief Set palette for 'Find' tool depending on results of search.
447 \param found Search result: true in case of success; false otherwise.
450 void PyEditor_FindTool::setSearchResult( bool found )
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 );
460 \brief Get 'Use regular expression' search option.
461 \return true if option is switched on; false otherwise.
464 bool PyEditor_FindTool::isRegExp() const
466 return actions()[RegExp]->isChecked();
470 \brief Get 'Case sensitive search' search option.
471 \return true if option is switched on; false otherwise.
474 bool PyEditor_FindTool::isCaseSensitive() const
476 return actions()[CaseSensitive]->isChecked();
480 \brief Get 'Whole words only' search option.
481 \return true if option is switched on; false otherwise.
484 bool PyEditor_FindTool::isWholeWord() const
486 return actions()[WholeWord]->isChecked();
490 \brief Get search options.
491 \param back Search direction: backward if false; forward otherwise.
492 \return List of options
495 QTextDocument::FindFlags PyEditor_FindTool::searchFlags( bool back ) const
497 QTextDocument::FindFlags flags = 0;
498 if ( isCaseSensitive() )
499 flags |= QTextDocument::FindCaseSensitively;
501 flags |= QTextDocument::FindWholeWords;
503 flags |= QTextDocument::FindBackward;
508 \brief Get all matches from Python editor.
509 \param text Text being searched.
510 \return List of all matches.
513 QList<QTextCursor> PyEditor_FindTool::matches( const QString& text ) const
515 QList<QTextCursor> results;
517 QTextDocument* document = myEditor->document();
519 QTextCursor cursor( document );
520 while ( !cursor.isNull() )
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 );
534 \brief Find specified text.
535 \param text Text being searched.
536 \param delta Search direction.
539 void PyEditor_FindTool::find( const QString& text, int delta )
541 QTextCursor cursor = myEditor->textCursor();
542 int position = qMin( cursor.position(), cursor.anchor() ) + delta;
543 cursor.setPosition( position );
544 myEditor->setTextCursor( cursor );
546 QList<QTextCursor> results = matches( text );
549 if ( !results.isEmpty() )
554 if ( position > results.last().anchor() )
556 for ( int i = 0; i < results.count() && index == -1; i++ )
558 QTextCursor result = results[i];
559 if ( result.hasSelection() && position <= result.anchor() )
568 if ( position < results.first().position() )
569 position = results.last().position();
571 for ( int i = results.count()-1; i >= 0 && index == -1; i-- )
573 QTextCursor result = results[i];
574 if ( result.hasSelection() && position >= result.position() )
583 myInfoLabel->setText( tr( "NB_MATCHED_LABEL" ).arg( index+1 ).arg( results.count() ) );
584 myEditor->setTextCursor( results[index] );
588 myInfoLabel->clear();
589 cursor.clearSelection();
590 myEditor->setTextCursor( cursor );
593 setSearchResult( text.isEmpty() || !results.isEmpty() );
597 \brief Add completion.
598 \param text Completeion being added.
599 \param replace true to add 'Replace' completion; false to add 'Find' completion.
602 void PyEditor_FindTool::addCompletion( const QString& text, bool replace )
604 QStringListModel& model = replace ? myReplaceCompletion : myFindCompletion;
606 QStringList completions = model.stringList();
607 if ( !text.isEmpty() and !completions.contains( text ) )
609 completions.prepend( text );
610 model.setStringList( completions );