1 // Copyright (C) 2015-2024 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( QIcon( ":/images/py_find.png" ), tr( "Find" ), this ) );
114 addAction( new QAction( tr( "FindPrevious" ), this ) );
115 addAction( new QAction( tr( "FindNext" ), this ) );
116 addAction( new QAction( QIcon( ":/images/py_replace.png" ), 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 );
138 connect( myEditor, SIGNAL( customizeMenu( QMenu* ) ), this, SLOT( customizeMenu( QMenu* ) ) );
144 \brief Process events for this widget,
145 \param e Event being processed.
146 \return true if event's processing should be stopped; false otherwise.
148 bool PyEditor_FindTool::event( QEvent* e )
150 if ( e->type() == QEvent::EnabledChange )
154 else if ( e->type() == QEvent::KeyPress )
156 QKeyEvent* ke = (QKeyEvent*)e;
166 else if ( e->type() == QEvent::Hide )
168 addCompletion( myFindEdit->text(), false );
169 addCompletion( myReplaceEdit->text(), true );
170 myEditor->setFocus();
172 return QWidget::event( e );
176 \brief Filter events from watched objects.
177 \param o Object being watched.
178 \param e Event being processed.
179 \return true if event should be filtered out; false otherwise.
181 bool PyEditor_FindTool::eventFilter( QObject* o, QEvent* e )
183 if ( o == myFindEdit )
185 if ( e->type() == QEvent::KeyPress )
187 QKeyEvent* keyEvent = (QKeyEvent*)e;
188 if ( keyEvent->key() == Qt::Key_Escape && !myFindEdit->text().isEmpty() )
190 addCompletion( myFindEdit->text(), false );
196 else if ( o == myReplaceEdit )
198 if ( e->type() == QEvent::KeyPress )
200 QKeyEvent* keyEvent = (QKeyEvent*)e;
201 if ( keyEvent->key() == Qt::Key_Escape && !myReplaceEdit->text().isEmpty() )
203 myReplaceEdit->clear();
208 else if ( o == myEditor )
210 if ( e->type() == QEvent::EnabledChange )
212 setEnabled( myEditor->isEnabled() );
214 else if ( e->type() == QEvent::Hide )
218 else if ( e->type() == QEvent::KeyPress )
220 QKeyEvent* ke = (QKeyEvent*)e;
235 return QWidget::eventFilter( o, e );
239 \brief Slot: activate 'Find' dialog.
241 void PyEditor_FindTool::activateFind()
247 \brief Customize menu for editor.
249 void PyEditor_FindTool::customizeMenu( QMenu* menu )
251 menu->addSeparator();
252 menu->addAction( actions()[Find] );
253 menu->addAction( actions()[Replace] );
257 \brief Slot: activate 'Replace' dialog.
259 void PyEditor_FindTool::activateReplace()
265 \brief Slot: show context menu with search options.
268 void PyEditor_FindTool::showMenu()
270 QMenu::exec( actions().mid( CaseSensitive, RegExp+1 ), QCursor::pos() );
274 \brief Slot: find text being typed in the 'Find' control.
275 \param text Text entered by the user.
278 void PyEditor_FindTool::find( const QString& text )
284 \brief Slot: find text entered in the 'Find' control.
288 void PyEditor_FindTool::find()
290 find( myFindEdit->text(), 0 );
294 \brief Slot: find previous matched item; called when user presses 'Previous' button.
297 void PyEditor_FindTool::findPrevious()
299 find( myFindEdit->text(), -1 );
303 \brief Slot: find next matched item; called when user presses 'Next' button.
306 void PyEditor_FindTool::findNext()
308 find( myFindEdit->text(), 1 );
312 \brief Slot: replace currently selected match; called when user presses 'Replace' button.
315 void PyEditor_FindTool::replace()
317 QString text = myFindEdit->text();
318 QString replacement = myReplaceEdit->text();
320 QTextCursor editor = myEditor->textCursor();
321 if ( editor.hasSelection() && editor.selectedText() == text )
323 editor.beginEditBlock();
324 editor.removeSelectedText();
325 editor.insertText( replacement );
326 editor.endEditBlock();
332 \brief Slot: replace all matches; called when user presses 'Replace All' button.
335 void PyEditor_FindTool::replaceAll()
337 QString text = myFindEdit->text();
338 QString replacement = myReplaceEdit->text();
339 QList<QTextCursor> results = matches( text );
340 if ( !results.isEmpty() )
342 QTextCursor editor( myEditor->document() );
343 editor.beginEditBlock();
344 foreach ( QTextCursor cursor, results )
346 editor.setPosition( cursor.anchor() );
347 editor.setPosition( cursor.position(), QTextCursor::KeepAnchor );
348 editor.removeSelectedText();
349 editor.insertText( replacement );
351 editor.endEditBlock();
357 \brief Slot: restart search; called when search options are changed.
360 void PyEditor_FindTool::update()
366 \brief Slot: activate action; called when user types corresponding shortcut.
367 \param action Action being activated.
370 void PyEditor_FindTool::activate( int action )
372 QTextCursor cursor = myEditor->textCursor();
373 cursor.movePosition( QTextCursor::StartOfWord );
374 cursor.movePosition( QTextCursor::EndOfWord, QTextCursor::KeepAnchor );
375 QString word = cursor.selectedText();
381 showReplaceControls( action == Replace );
383 if ( !word.isEmpty() ) {
384 myFindEdit->setText( word );
385 myEditor->setTextCursor( cursor );
387 myFindEdit->setFocus();
388 myFindEdit->selectAll();
389 find( myFindEdit->text() );
403 \brief Get shortcuts for given action.
404 \param action Editor's action.
405 \return List of shortcuts.
408 QList<QKeySequence> PyEditor_FindTool::shortcuts( int action ) const
410 QList<QKeySequence> bindings;
414 bindings << QKeySequence( QKeySequence::Find );
417 bindings << QKeySequence( QKeySequence::FindPrevious );
420 bindings << QKeySequence( QKeySequence::FindNext );
423 bindings << QKeySequence( "Ctrl+H" );
424 bindings << QKeySequence( QKeySequence::Replace );
433 \brief Update shortcuts when widget is enabled / disabled.
436 void PyEditor_FindTool::updateShortcuts()
438 foreach ( QAction* action, actions().mid( Find ) )
440 action->setEnabled( isEnabled() && myEditor->isEnabled() );
445 \brief Show / hide 'Replace' controls.
446 \param on Visibility flag.
449 void PyEditor_FindTool::showReplaceControls( bool on )
451 QGridLayout* l = qobject_cast<QGridLayout*>( layout() );
452 for ( int j = 0; j < l->columnCount(); j++ )
454 if ( l->itemAtPosition( 1, j )->widget() )
455 l->itemAtPosition( 1, j )->widget()->setVisible( on );
460 \brief Set palette for 'Find' tool depending on results of search.
461 \param found Search result: true in case of success; false otherwise.
464 void PyEditor_FindTool::setSearchResult( bool found )
466 QPalette pal = myFindEdit->palette();
467 QPalette ref = myReplaceEdit->palette();
468 pal.setColor( QPalette::Active, QPalette::Text,
469 found ? ref.color( QPalette::Active, QPalette::Text ) : QColor( 255, 0, 0 ) );
470 myFindEdit->setPalette( pal );
474 \brief Get 'Use regular expression' search option.
475 \return true if option is switched on; false otherwise.
478 bool PyEditor_FindTool::isRegExp() const
480 return actions()[RegExp]->isChecked();
484 \brief Get 'Case sensitive search' search option.
485 \return true if option is switched on; false otherwise.
488 bool PyEditor_FindTool::isCaseSensitive() const
490 return actions()[CaseSensitive]->isChecked();
494 \brief Get 'Whole words only' search option.
495 \return true if option is switched on; false otherwise.
498 bool PyEditor_FindTool::isWholeWord() const
500 return actions()[WholeWord]->isChecked();
504 \brief Get search options.
505 \param back Search direction: backward if false; forward otherwise.
506 \return List of options
509 QTextDocument::FindFlags PyEditor_FindTool::searchFlags( bool back ) const
511 QTextDocument::FindFlags flags = 0;
512 if ( isCaseSensitive() )
513 flags |= QTextDocument::FindCaseSensitively;
515 flags |= QTextDocument::FindWholeWords;
517 flags |= QTextDocument::FindBackward;
522 \brief Get all matches from Python editor.
523 \param text Text being searched.
524 \return List of all matches.
527 QList<QTextCursor> PyEditor_FindTool::matches( const QString& text ) const
529 QList<QTextCursor> results;
531 QTextDocument* document = myEditor->document();
533 QTextCursor cursor( document );
534 while ( !cursor.isNull() )
536 cursor = isRegExp() ?
537 document->find( QRegExp( text, isCaseSensitive() ?
538 Qt::CaseSensitive : Qt::CaseInsensitive ),
539 cursor, searchFlags() ) :
540 document->find( text, cursor, searchFlags() );
541 if ( !cursor.isNull() )
542 results.append( cursor );
548 \brief Find specified text.
549 \param text Text being searched.
550 \param delta Search direction.
553 void PyEditor_FindTool::find( const QString& text, int delta )
555 QTextCursor cursor = myEditor->textCursor();
556 int position = qMin( cursor.position(), cursor.anchor() ) + delta;
557 cursor.setPosition( position );
558 myEditor->setTextCursor( cursor );
560 QList<QTextCursor> results = matches( text );
563 if ( !results.isEmpty() )
568 if ( position > results.last().anchor() )
570 for ( int i = 0; i < results.count() && index == -1; i++ )
572 QTextCursor result = results[i];
573 if ( result.hasSelection() && position <= result.anchor() )
582 if ( position < results.first().position() )
583 position = results.last().position();
585 for ( int i = results.count()-1; i >= 0 && index == -1; i-- )
587 QTextCursor result = results[i];
588 if ( result.hasSelection() && position >= result.position() )
597 myInfoLabel->setText( tr( "NB_MATCHED_LABEL" ).arg( index+1 ).arg( results.count() ) );
598 myEditor->setTextCursor( results[index] );
602 myInfoLabel->clear();
603 cursor.clearSelection();
604 myEditor->setTextCursor( cursor );
607 setSearchResult( text.isEmpty() || !results.isEmpty() );
611 \brief Add completion.
612 \param text Completeion being added.
613 \param replace true to add 'Replace' completion; false to add 'Find' completion.
616 void PyEditor_FindTool::addCompletion( const QString& text, bool replace )
618 QStringListModel& model = replace ? myReplaceCompletion : myFindCompletion;
620 QStringList completions = model.stringList();
621 if ( !text.isEmpty() && !completions.contains( text ) )
623 completions.prepend( text );
624 model.setStringList( completions );