From b975b2713ca3a0679420c1bb0041ee9bc69c5048 Mon Sep 17 00:00:00 2001
From: vsr
Date: Mon, 22 May 2017 14:16:04 +0300
Subject: [PATCH] PyEditor: implement Find/Replace feature
---
src/PyViewer/CMakeLists.txt | 2 +
src/PyViewer/PyViewer_ViewWindow.cxx | 71 +-
src/PyViewer/PyViewer_ViewWindow.h | 5 +-
src/PyViewer/resources/PyViewer_images.ts | 8 +
src/PyViewer/resources/PyViewer_msg_en.ts | 24 +
src/PyViewer/resources/PyViewer_msg_fr.ts | 24 +
src/PyViewer/resources/PyViewer_msg_ja.ts | 24 +
tools/PyEditor/src/CMakeLists.txt | 4 +
tools/PyEditor/src/PyEditor.cxx | 29 +-
tools/PyEditor/src/PyEditor_FindTool.cxx | 612 ++++++++++++++++++
tools/PyEditor/src/PyEditor_FindTool.h | 89 +++
tools/PyEditor/src/PyEditor_Widget.cxx | 251 +++++++
tools/PyEditor/src/PyEditor_Widget.h | 85 +++
tools/PyEditor/src/PyEditor_Window.cxx | 89 ++-
tools/PyEditor/src/PyEditor_Window.h | 12 +-
tools/PyEditor/src/python/PyEditorPy.sip | 72 +++
tools/PyEditor/src/resources/PyEditor.qrc | 5 +
tools/PyEditor/src/resources/about.txt | 15 +-
.../PyEditor/src/resources/images/py_find.png | Bin 0 -> 19794 bytes
.../src/resources/images/py_find_next.png | Bin 0 -> 472 bytes
.../src/resources/images/py_find_previous.png | Bin 0 -> 474 bytes
.../src/resources/images/py_replace.png | Bin 0 -> 60177 bytes
.../src/resources/images/py_search.png | Bin 0 -> 1264 bytes
.../resources/translations/PyEditor_msg_en.ts | 80 ++-
.../resources/translations/PyEditor_msg_fr.ts | 80 ++-
.../resources/translations/PyEditor_msg_ja.ts | 80 ++-
26 files changed, 1588 insertions(+), 73 deletions(-)
create mode 100644 tools/PyEditor/src/PyEditor_FindTool.cxx
create mode 100644 tools/PyEditor/src/PyEditor_FindTool.h
create mode 100644 tools/PyEditor/src/PyEditor_Widget.cxx
create mode 100644 tools/PyEditor/src/PyEditor_Widget.h
create mode 100644 tools/PyEditor/src/resources/images/py_find.png
create mode 100644 tools/PyEditor/src/resources/images/py_find_next.png
create mode 100644 tools/PyEditor/src/resources/images/py_find_previous.png
create mode 100644 tools/PyEditor/src/resources/images/py_replace.png
create mode 100644 tools/PyEditor/src/resources/images/py_search.png
diff --git a/src/PyViewer/CMakeLists.txt b/src/PyViewer/CMakeLists.txt
index bc4fbe352..85bcc87ee 100644
--- a/src/PyViewer/CMakeLists.txt
+++ b/src/PyViewer/CMakeLists.txt
@@ -69,12 +69,14 @@ SET(_other_RESOURCES
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_copy.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_cut.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_delete.png
+ ${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_find.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_help.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_new.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_open.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_paste.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_preferences.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_redo.png
+ ${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_replace.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_save.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_save_as.png
${PROJECT_SOURCE_DIR}/tools/PyEditor/src/resources/images/py_select_all.png
diff --git a/src/PyViewer/PyViewer_ViewWindow.cxx b/src/PyViewer/PyViewer_ViewWindow.cxx
index f7c37be7e..a9e6c79c2 100644
--- a/src/PyViewer/PyViewer_ViewWindow.cxx
+++ b/src/PyViewer/PyViewer_ViewWindow.cxx
@@ -22,7 +22,7 @@
#include "PyViewer_ViewWindow.h"
-#include "PyEditor_Editor.h"
+#include "PyEditor_Widget.h"
#include "PyEditor_SettingsDlg.h"
#include "SUIT_Session.h"
@@ -32,9 +32,11 @@
#include "QtxActionToolMgr.h"
#include
+#include
#include
#include
#include
+#include
/*!
\class PyViewer_ViewWindow
@@ -48,9 +50,9 @@
PyViewer_ViewWindow::PyViewer_ViewWindow( SUIT_Desktop* desktop ) :
SUIT_ViewWindow( desktop )
{
- // Create editor and set it as a central widget.
- myTextEditor = new PyEditor_Editor( this );
- setCentralWidget( myTextEditor );
+ // Create central widget.
+ myEditor = new PyEditor_Widget( this );
+ setCentralWidget( myEditor );
// Create actions.
SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
@@ -82,7 +84,7 @@ PyViewer_ViewWindow::PyViewer_ViewWindow( SUIT_Desktop* desktop ) :
action->setShortcut( QKeySequence::Save );
connect( action, SIGNAL( triggered( bool ) ), this, SLOT( onSave() ) );
action->setEnabled( false );
- connect( myTextEditor->document(), SIGNAL( modificationChanged( bool ) ),
+ connect( myEditor, SIGNAL( modificationChanged( bool ) ),
action, SLOT( setEnabled( bool ) ) );
toolMgr()->registerAction( action, SaveId );
@@ -101,9 +103,9 @@ PyViewer_ViewWindow::PyViewer_ViewWindow( SUIT_Desktop* desktop ) :
tr( "ACT_UNDO" ), 0, this );
action->setStatusTip( tr( "DSC_UNDO" ) );
action->setShortcut( QKeySequence::Undo );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( undo() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( undo() ) );
action->setEnabled( false );
- connect( myTextEditor->document(), SIGNAL( undoAvailable( bool ) ),
+ connect( myEditor, SIGNAL( undoAvailable( bool ) ),
action, SLOT( setEnabled( bool ) ) );
toolMgr()->registerAction( action, UndoId );
@@ -113,9 +115,9 @@ PyViewer_ViewWindow::PyViewer_ViewWindow( SUIT_Desktop* desktop ) :
tr( "ACT_REDO" ), 0, this );
action->setStatusTip( tr( "DSC_REDO" ) );
action->setShortcut( QKeySequence::Redo );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( redo() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( redo() ) );
action->setEnabled( false );
- connect( myTextEditor->document(), SIGNAL( redoAvailable( bool ) ),
+ connect( myEditor, SIGNAL( redoAvailable( bool ) ),
action, SLOT( setEnabled( bool ) ) );
toolMgr()->registerAction( action, RedoId );
@@ -125,9 +127,9 @@ PyViewer_ViewWindow::PyViewer_ViewWindow( SUIT_Desktop* desktop ) :
tr( "ACT_CUT" ), 0, this );
action->setStatusTip( tr( "DSC_CUT" ) );
action->setShortcut( QKeySequence::Cut );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( cut() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( cut() ) );
action->setEnabled( false );
- connect( myTextEditor, SIGNAL( copyAvailable( bool ) ),
+ connect( myEditor, SIGNAL( copyAvailable( bool ) ),
action, SLOT( setEnabled( bool ) ) );
toolMgr()->registerAction( action, CutId );
@@ -137,9 +139,9 @@ PyViewer_ViewWindow::PyViewer_ViewWindow( SUIT_Desktop* desktop ) :
tr( "ACT_COPY" ), 0, this );
action->setStatusTip( tr( "DSC_COPY" ) );
action->setShortcut( QKeySequence::Copy );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( copy() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( copy() ) );
action->setEnabled( false );
- connect( myTextEditor, SIGNAL( copyAvailable( bool ) ),
+ connect( myEditor, SIGNAL( copyAvailable( bool ) ),
action, SLOT( setEnabled( bool ) ) );
toolMgr()->registerAction( action, CopyId );
@@ -149,7 +151,7 @@ PyViewer_ViewWindow::PyViewer_ViewWindow( SUIT_Desktop* desktop ) :
tr( "ACT_PASTE" ), 0, this );
action->setStatusTip( tr( "DSC_PASTE" ) );
action->setShortcut( QKeySequence::Paste );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( paste() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( paste() ) );
toolMgr()->registerAction( action, PasteId );
// . Delete
@@ -158,9 +160,9 @@ PyViewer_ViewWindow::PyViewer_ViewWindow( SUIT_Desktop* desktop ) :
tr( "ACT_DELETE" ), 0, this );
action->setStatusTip( tr( "DSC_DELETE" ) );
action->setShortcut( QKeySequence::Delete );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( deleteSelected() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( deleteSelected() ) );
action->setEnabled( false );
- connect( myTextEditor, SIGNAL( copyAvailable( bool ) ),
+ connect( myEditor, SIGNAL( copyAvailable( bool ) ),
action, SLOT( setEnabled( bool ) ) );
toolMgr()->registerAction( action, DeleteId );
@@ -170,9 +172,29 @@ PyViewer_ViewWindow::PyViewer_ViewWindow( SUIT_Desktop* desktop ) :
tr( "ACT_SELECT_ALL" ), 0, this );
action->setStatusTip( tr( "DSC_SELECT_ALL" ) );
action->setShortcut( QKeySequence::SelectAll );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( selectAll() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( selectAll() ) );
toolMgr()->registerAction( action, SelectAllId );
+ // . Find
+ action = new QtxAction( tr( "TTP_FIND" ),
+ resMgr->loadPixmap( "PyViewer", tr( "ICON_FIND" ) ),
+ tr( "ACT_FIND" ), 0, this );
+ action->setStatusTip( tr( "DSC_FIND" ) );
+ action->setShortcut( QKeySequence::Find );
+ action->setShortcutContext( Qt::WidgetShortcut );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( find() ) );
+ toolMgr()->registerAction( action, FindId );
+
+ // . Replace
+ action = new QtxAction( tr( "TTP_REPLACE" ),
+ resMgr->loadPixmap( "PyViewer", tr( "ICON_REPLACE" ) ),
+ tr( "ACT_REPLACE" ), 0, this );
+ action->setStatusTip( tr( "DSC_REPLACE" ) );
+ action->setShortcut( QKeySequence::Replace );
+ action->setShortcutContext( Qt::WidgetShortcut );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( replace() ) );
+ toolMgr()->registerAction( action, ReplaceId );
+
// . Preferences
action = new QtxAction( tr( "TTP_PREFERENCES" ),
resMgr->loadPixmap( "PyViewer", tr( "ICON_PREFERENCES" ) ),
@@ -205,6 +227,9 @@ PyViewer_ViewWindow::PyViewer_ViewWindow( SUIT_Desktop* desktop ) :
toolMgr()->append( DeleteId, idTB );
toolMgr()->append( SelectAllId, idTB );
toolMgr()->append( toolMgr()->separator(), idTB );
+ toolMgr()->append( FindId, idTB );
+ toolMgr()->append( ReplaceId, idTB );
+ toolMgr()->append( toolMgr()->separator(), idTB );
toolMgr()->append( PreferencesId, idTB );
toolMgr()->append( toolMgr()->separator(), idTB );
toolMgr()->append( HelpId, idTB );
@@ -239,7 +264,7 @@ void PyViewer_ViewWindow::onNew()
{
if ( whetherSave() )
{
- myTextEditor->clear();
+ myEditor->clear();
setCurrentFile( QString() );
}
}
@@ -299,7 +324,7 @@ bool PyViewer_ViewWindow::onSaveAs()
*/
void PyViewer_ViewWindow::onPreferences()
{
- PyEditor_SettingsDlg dlg( myTextEditor, true, this );
+ PyEditor_SettingsDlg dlg( myEditor->editor(), true, this );
connect( &dlg, SIGNAL( help() ), this, SLOT( onHelp() ) );
dlg.exec();
}
@@ -311,7 +336,7 @@ void PyViewer_ViewWindow::onPreferences()
void PyViewer_ViewWindow::setCurrentFile( const QString& filePath )
{
myURL = filePath;
- myTextEditor->document()->setModified( false );
+ myEditor->setModified( false );
}
/*!
@@ -321,7 +346,7 @@ void PyViewer_ViewWindow::setCurrentFile( const QString& filePath )
*/
bool PyViewer_ViewWindow::whetherSave()
{
- if ( myTextEditor->document()->isModified() )
+ if ( myEditor->isModified() )
{
QMessageBox::StandardButton answer = QMessageBox::warning( this,
tr( "NAME_PYEDITOR" ),
@@ -358,7 +383,7 @@ void PyViewer_ViewWindow::loadFile( const QString& filePath )
QTextStream anInput( &aFile );
QApplication::setOverrideCursor( Qt::WaitCursor );
- myTextEditor->setPlainText( anInput.readAll() );
+ myEditor->setText( anInput.readAll() );
QApplication::restoreOverrideCursor();
setCurrentFile( filePath );
@@ -382,7 +407,7 @@ bool PyViewer_ViewWindow::saveFile( const QString& filePath )
QTextStream anOutput( &aFile );
QApplication::setOverrideCursor( Qt::WaitCursor );
- anOutput << myTextEditor->toPlainText();
+ anOutput << myEditor->text();
QApplication::restoreOverrideCursor();
setCurrentFile( filePath );
diff --git a/src/PyViewer/PyViewer_ViewWindow.h b/src/PyViewer/PyViewer_ViewWindow.h
index b96ea8b2c..229721d68 100644
--- a/src/PyViewer/PyViewer_ViewWindow.h
+++ b/src/PyViewer/PyViewer_ViewWindow.h
@@ -27,7 +27,7 @@
#include
-class PyEditor_Editor;
+class PyEditor_Widget;
class PYVIEWER_EXPORT PyViewer_ViewWindow : public SUIT_ViewWindow
{
@@ -36,6 +36,7 @@ class PYVIEWER_EXPORT PyViewer_ViewWindow : public SUIT_ViewWindow
public:
enum { NewId, OpenId, SaveId, SaveAsId,
UndoId, RedoId, CutId, CopyId, PasteId, DeleteId, SelectAllId,
+ FindId, ReplaceId,
PreferencesId, HelpId };
PyViewer_ViewWindow( SUIT_Desktop* = 0 );
@@ -61,7 +62,7 @@ private:
QString defaultName() const;
private:
- PyEditor_Editor* myTextEditor;
+ PyEditor_Widget* myEditor;
QString myURL;
};
diff --git a/src/PyViewer/resources/PyViewer_images.ts b/src/PyViewer/resources/PyViewer_images.ts
index 639f1b584..6ac464e6c 100644
--- a/src/PyViewer/resources/PyViewer_images.ts
+++ b/src/PyViewer/resources/PyViewer_images.ts
@@ -47,6 +47,14 @@
ICON_SELECT_ALL
py_select_all.png
+
+ ICON_FIND
+ py_find.png
+
+
+ ICON_REPLACE
+ py_replace.png
+
ICON_PREFERENCES
py_preferences.png
diff --git a/src/PyViewer/resources/PyViewer_msg_en.ts b/src/PyViewer/resources/PyViewer_msg_en.ts
index 0f4355ee3..20e989a1a 100644
--- a/src/PyViewer/resources/PyViewer_msg_en.ts
+++ b/src/PyViewer/resources/PyViewer_msg_en.ts
@@ -146,6 +146,30 @@
DSC_SELECT_ALL
Select all the contents
+
+ ACT_FIND
+ Find
+
+
+ TTP_FIND
+ Find
+
+
+ DSC_FIND
+ Find text
+
+
+ ACT_REPLACE
+ Replace
+
+
+ TTP_REPLACE
+ Find & Replace
+
+
+ DSC_REPLACE
+ Find and replace text
+
ACT_PREFERENCES
Pre&ferences
diff --git a/src/PyViewer/resources/PyViewer_msg_fr.ts b/src/PyViewer/resources/PyViewer_msg_fr.ts
index c0fc973cd..7a52883c0 100644
--- a/src/PyViewer/resources/PyViewer_msg_fr.ts
+++ b/src/PyViewer/resources/PyViewer_msg_fr.ts
@@ -146,6 +146,30 @@
DSC_SELECT_ALL
Sélectionne tout le contenu
+
+ ACT_FIND
+ Find
+
+
+ TTP_FIND
+ Find
+
+
+ DSC_FIND
+ Find text
+
+
+ ACT_REPLACE
+ Replace
+
+
+ TTP_REPLACE
+ Find & Replace
+
+
+ DSC_REPLACE
+ Find and replace text
+
ACT_PREFERENCES
Préférences
diff --git a/src/PyViewer/resources/PyViewer_msg_ja.ts b/src/PyViewer/resources/PyViewer_msg_ja.ts
index 6b8691da3..35c4e054e 100644
--- a/src/PyViewer/resources/PyViewer_msg_ja.ts
+++ b/src/PyViewer/resources/PyViewer_msg_ja.ts
@@ -146,6 +146,30 @@
DSC_SELECT_ALL
å
¨é¸æ
+
+ ACT_FIND
+ Find
+
+
+ TTP_FIND
+ Find
+
+
+ DSC_FIND
+ Find text
+
+
+ ACT_REPLACE
+ Replace
+
+
+ TTP_REPLACE
+ Find & Replace
+
+
+ DSC_REPLACE
+ Find and replace text
+
ACT_PREFERENCES
ç°å¢è¨å® (&f)
diff --git a/tools/PyEditor/src/CMakeLists.txt b/tools/PyEditor/src/CMakeLists.txt
index 2715e6ebd..53f50bbea 100644
--- a/tools/PyEditor/src/CMakeLists.txt
+++ b/tools/PyEditor/src/CMakeLists.txt
@@ -43,11 +43,13 @@ SET(_link_LIBRARIES ${PLATFORM_LIBS} ${QT_LIBRARIES})
# header files / to be processed by moc
SET(_moc_HEADERS
PyEditor_Editor.h
+ PyEditor_FindTool.h
PyEditor_LineNumberArea.h
PyEditor_Keywords.h
PyEditor_Completer.h
PyEditor_PyHighlighter.h
PyEditor_SettingsDlg.h
+ PyEditor_Widget.h
PyEditor_Window.h
)
@@ -84,6 +86,7 @@ QT_ADD_RESOURCES(_rcc_SOURCES ${_rcc_RESOURCES})
# sources / static
SET(_other_SOURCES
PyEditor_Editor.cxx
+ PyEditor_FindTool.cxx
PyEditor_LineNumberArea.cxx
PyEditor_Keywords.cxx
PyEditor_Completer.cxx
@@ -91,6 +94,7 @@ SET(_other_SOURCES
PyEditor_Settings.cxx
PyEditor_SettingsDlg.cxx
PyEditor_StdSettings.cxx
+ PyEditor_Widget.cxx
PyEditor_Window.cxx
)
diff --git a/tools/PyEditor/src/PyEditor.cxx b/tools/PyEditor/src/PyEditor.cxx
index 03a29b8e9..5a213338d 100644
--- a/tools/PyEditor/src/PyEditor.cxx
+++ b/tools/PyEditor/src/PyEditor.cxx
@@ -24,6 +24,7 @@
#include "PyEditor_StdSettings.h"
#include
+#include
#include
#include
#include
@@ -67,11 +68,11 @@ int main( int argc, char *argv[] )
app.setOrganizationDomain( "www.salome-platform.org" );
app.setApplicationName( "pyeditor" );
+ QLocale locale;
+
PyEditor_StdSettings* settings = new PyEditor_StdSettings();
PyEditor_Settings::setSettings( settings );
- QString language = settings->language();
-
// Load Qt translations.
QString qtDirTrSet = QLibraryInfo::location( QLibraryInfo::TranslationsPath );
QString qtDirTrEnv = qtTrDir();
@@ -85,7 +86,11 @@ int main( int argc, char *argv[] )
foreach( QString qtTrFile, qtTrFiles ) {
foreach ( QString qtTrDir, qtTrDirs ) {
QTranslator* translator = new QTranslator;
- if ( translator->load( QString("%1_%2").arg( qtTrFile ).arg( language ), qtTrDir ) ) {
+ if ( translator->load( locale, QString("%1").arg( qtTrFile ), "_", qtTrDir ) ) {
+ app.installTranslator( translator );
+ break;
+ }
+ else if ( translator->load( QString("%1_en").arg( qtTrFile ), qtTrDir ) ) {
app.installTranslator( translator );
break;
}
@@ -97,13 +102,27 @@ int main( int argc, char *argv[] )
// Load application's translations.
QTranslator translator;
- if ( translator.load( QString( "PyEditor_msg_%1" ).arg( language ), resourceDir() ) )
+ if ( translator.load( locale, QString( "PyEditor_msg" ), "_", resourceDir() ) )
app.installTranslator( &translator );
-
+ else if ( translator.load( QString( "PyEditor_msg_en" ), resourceDir() ) )
+ app.installTranslator( &translator );
+
+ QCommandLineParser parser;
+ parser.setApplicationDescription( QApplication::translate( "PyEditor", "PROGRAM_DESCRIPTION" ) );
+ parser.addHelpOption();
+ parser.addPositionalArgument( QApplication::translate( "PyEditor", "FILE_PARAM_NAME" ),
+ QApplication::translate( "PyEditor", "FILE_PARAM_DESCRIPTION" ) );
+
+ parser.process( app );
+ const QStringList args = parser.positionalArguments();
+
PyEditor_Window window;
window.setWindowIcon( QIcon( ":/images/py_editor.png" ) );
window.resize( 650, 700 );
window.show();
+
+ if ( args.count() > 0 )
+ window.loadFile( args[0], false );
return app.exec();
}
diff --git a/tools/PyEditor/src/PyEditor_FindTool.cxx b/tools/PyEditor/src/PyEditor_FindTool.cxx
new file mode 100644
index 000000000..de6be82e5
--- /dev/null
+++ b/tools/PyEditor/src/PyEditor_FindTool.cxx
@@ -0,0 +1,612 @@
+// Copyright (C) 2015-2016 OPEN CASCADE
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+//
+// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+//
+// File : PyEditor_FindTool.cxx
+// Author : Vadim SANDLER, Open CASCADE S.A.S. (vadim.sandler@opencascade.com)
+//
+
+#include "PyEditor_FindTool.h"
+#include "PyEditor_Editor.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/*!
+ \class PyEditor_FindTool
+ \brief Find / Replace widget for PyEditor
+*/
+
+/*!
+ \brief Constructor.
+ \param editor Python editor widget.
+ \param parent Parent widget.
+*/
+PyEditor_FindTool::PyEditor_FindTool( PyEditor_Editor* editor, QWidget* parent )
+ : QWidget( parent ), myEditor( editor )
+{
+ QLabel* findLabel = new QLabel( tr( "FIND_LABEL" ), this );
+ myFindEdit = new QLineEdit( this );
+ myFindEdit->setClearButtonEnabled( true );
+ myFindEdit->installEventFilter( this );
+ connect( myFindEdit, SIGNAL( textChanged( const QString& ) ), this, SLOT( find( const QString& ) ) );
+ connect( myFindEdit, SIGNAL( returnPressed() ), this, SLOT( findNext() ) );
+ myFindEdit->setCompleter( new QCompleter( myFindEdit ) );
+ myFindEdit->completer()->setModel( &myFindCompletion );
+
+ QLabel* replaceLabel = new QLabel( tr( "REPLACE_LABEL" ), this );
+ myReplaceEdit = new QLineEdit( this );
+ myReplaceEdit->setClearButtonEnabled( true );
+ myReplaceEdit->installEventFilter( this );
+ myReplaceEdit->setCompleter( new QCompleter( myReplaceEdit ) );
+ myReplaceEdit->completer()->setModel( &myReplaceCompletion );
+
+ myInfoLabel = new QLabel( this );
+ myInfoLabel->setAlignment( Qt::AlignVCenter | Qt::AlignRight );
+
+ QToolButton* prevBtn = new QToolButton( this );
+ prevBtn->setIcon( QIcon( ":images/py_find_previous.png" ) );
+ prevBtn->setAutoRaise( true );
+ connect( prevBtn, SIGNAL( clicked() ), this, SLOT( findPrevious() ) );
+
+ QToolButton* nextBtn = new QToolButton( this );
+ nextBtn->setIcon( QIcon( ":images/py_find_next.png" ) );
+ nextBtn->setAutoRaise( true );
+ connect( nextBtn, SIGNAL( clicked() ), this, SLOT( findNext() ) );
+
+ QToolButton* replaceBtn = new QToolButton();
+ replaceBtn->setText( tr( "REPLACE_BTN" ) );
+ replaceBtn->setAutoRaise( true );
+ connect( replaceBtn, SIGNAL( clicked() ), this, SLOT( replace() ) );
+
+ QToolButton* replaceAllBtn = new QToolButton();
+ replaceAllBtn->setText( tr( "REPLACE_ALL_BTN" ) );
+ replaceAllBtn->setAutoRaise( true );
+ connect( replaceAllBtn, SIGNAL( clicked() ), this, SLOT( replaceAll() ) );
+
+ QHBoxLayout* hl = new QHBoxLayout;
+ hl->setContentsMargins( 0, 0, 0, 0 );
+ hl->setSpacing( 0 );
+ hl->addWidget( prevBtn );
+ hl->addWidget( nextBtn );
+
+ QGridLayout* l = new QGridLayout( this );
+ l->setContentsMargins( 6, 2, 6, 2 );
+ l->setSpacing( 2 );
+ l->addWidget( findLabel, 0, 0 );
+ l->addWidget( myFindEdit, 0, 1 );
+ l->addLayout( hl, 0, 2 );
+ l->addWidget( myInfoLabel, 0, 3 );
+ l->addWidget( replaceLabel, 1, 0 );
+ l->addWidget( myReplaceEdit, 1, 1 );
+ l->addWidget( replaceBtn, 1, 2 );
+ l->addWidget( replaceAllBtn, 1, 3 );
+
+ QAction* menuAction = myFindEdit->addAction( QIcon(":images/py_search.png"), QLineEdit::LeadingPosition );
+ connect( menuAction, SIGNAL( triggered( bool ) ), this, SLOT( showMenu() ) );
+
+ addAction( new QAction( tr( "CASE_SENSITIVE_CHECK" ), this ) );
+ addAction( new QAction( tr( "WHOLE_WORDS_CHECK" ), this ) );
+ addAction( new QAction( tr( "REGEX_CHECK" ), this ) );
+ addAction( new QAction( tr( "Find" ), this ) );
+ addAction( new QAction( tr( "FindPrevious" ), this ) );
+ addAction( new QAction( tr( "FindNext" ), this ) );
+ addAction( new QAction( tr( "Replace" ), this ) );
+
+ foreach ( QAction* action, actions().mid( CaseSensitive, RegExp+1 ) )
+ {
+ action->setCheckable( true );
+ connect( action, SIGNAL( toggled( bool ) ), this, SLOT( update() ) );
+ }
+
+ QSignalMapper* mapper = new QSignalMapper( this );
+ connect( mapper, SIGNAL( mapped( int ) ), this, SLOT( activate( int ) ) );
+
+ for ( int i = Find; i < actions().count(); i++ )
+ {
+ QAction* action = actions()[i];
+ action->setShortcuts( shortcuts( i ) );
+ action->setShortcutContext( Qt::WidgetWithChildrenShortcut );
+ connect( action, SIGNAL( triggered( bool ) ), mapper, SLOT( map() ) );
+ mapper->setMapping( action, i );
+ myEditor->addAction( action );
+ }
+
+ myEditor->installEventFilter( this );
+
+ hide();
+}
+
+/*!
+ \brief Process events for this widget,
+ \param e Event being processed.
+ \return true if event's processing should be stopped; false otherwise.
+*/
+bool PyEditor_FindTool::event( QEvent* e )
+{
+ if ( e->type() == QEvent::EnabledChange )
+ {
+ updateShortcuts();
+ }
+ else if ( e->type() == QEvent::KeyPress )
+ {
+ QKeyEvent* ke = (QKeyEvent*)e;
+ switch ( ke->key() )
+ {
+ case Qt::Key_Escape:
+ hide();
+ break;
+ default:
+ break;
+ }
+ }
+ else if ( e->type() == QEvent::Hide )
+ {
+ addCompletion( myFindEdit->text(), false );
+ addCompletion( myReplaceEdit->text(), true );
+ myEditor->setFocus();
+ }
+ return QWidget::event( e );
+}
+
+/*!
+ \brief Filter events from watched objects.
+ \param o Object being watched.
+ \param e Event being processed.
+ \return true if event should be filtered out; false otherwise.
+*/
+bool PyEditor_FindTool::eventFilter( QObject* o, QEvent* e )
+{
+ if ( o == myFindEdit )
+ {
+ if ( e->type() == QEvent::KeyPress )
+ {
+ QKeyEvent* keyEvent = (QKeyEvent*)e;
+ if ( keyEvent->key() == Qt::Key_Escape && !myFindEdit->text().isEmpty() )
+ {
+ addCompletion( myFindEdit->text(), false );
+ myFindEdit->clear();
+ return true;
+ }
+ }
+ }
+ else if ( o == myReplaceEdit )
+ {
+ if ( e->type() == QEvent::KeyPress )
+ {
+ QKeyEvent* keyEvent = (QKeyEvent*)e;
+ if ( keyEvent->key() == Qt::Key_Escape && !myReplaceEdit->text().isEmpty() )
+ {
+ myReplaceEdit->clear();
+ return true;
+ }
+ }
+ }
+ else if ( o == myEditor )
+ {
+ if ( e->type() == QEvent::EnabledChange )
+ {
+ setEnabled( myEditor->isEnabled() );
+ }
+ else if ( e->type() == QEvent::Hide )
+ {
+ hide();
+ }
+ else if ( e->type() == QEvent::KeyPress )
+ {
+ QKeyEvent* ke = (QKeyEvent*)e;
+ switch ( ke->key() )
+ {
+ case Qt::Key_Escape:
+ if ( isVisible() )
+ hide();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ return QWidget::eventFilter( o, e );
+}
+
+/*!
+ \brief Slot: activate 'Find' dialog.
+*/
+void PyEditor_FindTool::activateFind()
+{
+ activate( Find );
+}
+
+/*!
+ \brief Slot: activate 'Replace' dialog.
+*/
+void PyEditor_FindTool::activateReplace()
+{
+ activate( Replace );
+}
+
+/*!
+ \brief Slot: show context menu with search options.
+ \internal
+*/
+void PyEditor_FindTool::showMenu()
+{
+ QMenu::exec( actions().mid( CaseSensitive, RegExp+1 ), QCursor::pos() );
+}
+
+/*!
+ \brief Slot: find text being typed in the 'Find' control.
+ \param text Text entered by the user.
+ \internal
+*/
+void PyEditor_FindTool::find( const QString& text )
+{
+ find( text, 0 );
+}
+
+/*!
+ \brief Slot: find text entered in the 'Find' control.
+ \internal
+ \overload
+*/
+void PyEditor_FindTool::find()
+{
+ find( myFindEdit->text(), 0 );
+}
+
+/*!
+ \brief Slot: find previous matched item; called when user presses 'Previous' button.
+ \internal
+*/
+void PyEditor_FindTool::findPrevious()
+{
+ find( myFindEdit->text(), -1 );
+}
+
+/*!
+ \brief Slot: find next matched item; called when user presses 'Next' button.
+ \internal
+*/
+void PyEditor_FindTool::findNext()
+{
+ find( myFindEdit->text(), 1 );
+}
+
+/*!
+ \brief Slot: replace currently selected match; called when user presses 'Replace' button.
+ \internal
+*/
+void PyEditor_FindTool::replace()
+{
+ QString text = myFindEdit->text();
+ QString replacement = myReplaceEdit->text();
+
+ QTextCursor editor = myEditor->textCursor();
+ if ( editor.hasSelection() && editor.selectedText() == text )
+ {
+ editor.beginEditBlock();
+ editor.removeSelectedText();
+ editor.insertText( replacement );
+ editor.endEditBlock();
+ find();
+ }
+}
+
+/*!
+ \brief Slot: replace all matches; called when user presses 'Replace All' button.
+ \internal
+*/
+void PyEditor_FindTool::replaceAll()
+{
+ QString text = myFindEdit->text();
+ QString replacement = myReplaceEdit->text();
+ QList results = matches( text );
+ if ( !results.isEmpty() )
+ {
+ QTextCursor editor( myEditor->document() );
+ editor.beginEditBlock();
+ foreach ( QTextCursor cursor, results )
+ {
+ editor.setPosition( cursor.anchor() );
+ editor.setPosition( cursor.position(), QTextCursor::KeepAnchor );
+ editor.removeSelectedText();
+ editor.insertText( replacement );
+ }
+ editor.endEditBlock();
+ find();
+ }
+}
+
+/*!
+ \brief Slot: restart search; called when search options are changed.
+ \internal
+*/
+void PyEditor_FindTool::update()
+{
+ find();
+}
+
+/*!
+ \brief Slot: activate action; called when user types corresponding shortcut.
+ \param action Action being activated.
+ \internal
+*/
+void PyEditor_FindTool::activate( int action )
+{
+ QTextCursor cursor = myEditor->textCursor();
+ cursor.movePosition( QTextCursor::StartOfWord );
+ cursor.movePosition( QTextCursor::EndOfWord, QTextCursor::KeepAnchor );
+ QString word = cursor.selectedText();
+
+ switch ( action )
+ {
+ case Find:
+ case Replace:
+ showReplaceControls( action == Replace );
+ show();
+ if ( !word.isEmpty() ) {
+ myFindEdit->setText( word );
+ myEditor->setTextCursor( cursor );
+ }
+ myFindEdit->setFocus();
+ myFindEdit->selectAll();
+ find( myFindEdit->text() );
+ break;
+ case FindPrevious:
+ findPrevious();
+ break;
+ case FindNext:
+ findNext();
+ break;
+ default:
+ break;
+ }
+}
+
+/*!
+ \brief Get shortcuts for given action.
+ \param action Editor's action.
+ \return List of shortcuts.
+ \internal
+*/
+QList PyEditor_FindTool::shortcuts( int action ) const
+{
+ QList bindings;
+ switch ( action )
+ {
+ case Find:
+ bindings << QKeySequence( QKeySequence::Find );
+ break;
+ case FindPrevious:
+ bindings << QKeySequence( QKeySequence::FindPrevious );
+ break;
+ case FindNext:
+ bindings << QKeySequence( QKeySequence::FindNext );
+ break;
+ case Replace:
+ bindings << QKeySequence( QKeySequence::Replace );
+ bindings << QKeySequence( "Ctrl+H" );
+ break;
+ default:
+ break;
+ }
+ return bindings;
+}
+
+/*!
+ \brief Update shortcuts when widget is enabled / disabled.
+ \internal
+*/
+void PyEditor_FindTool::updateShortcuts()
+{
+ foreach ( QAction* action, actions().mid( Find ) )
+ {
+ action->setEnabled( isEnabled() && myEditor->isEnabled() );
+ }
+}
+
+/*!
+ \brief Show / hide 'Replace' controls.
+ \param on Visibility flag.
+ \internal
+*/
+void PyEditor_FindTool::showReplaceControls( bool on )
+{
+ QGridLayout* l = qobject_cast( layout() );
+ for ( int j = 0; j < l->columnCount(); j++ )
+ {
+ if ( l->itemAtPosition( 1, j )->widget() )
+ l->itemAtPosition( 1, j )->widget()->setVisible( on );
+ }
+}
+
+/*!
+ \brief Set palette for 'Find' tool depending on results of search.
+ \param found Search result: true in case of success; false otherwise.
+ \internal
+*/
+void PyEditor_FindTool::setSearchResult( bool found )
+{
+ QPalette pal = myFindEdit->palette();
+ QPalette ref = myReplaceEdit->palette();
+ pal.setColor( QPalette::Active, QPalette::Text,
+ found ? ref.color( QPalette::Active, QPalette::Text ) : QColor( 255, 0, 0 ) );
+ myFindEdit->setPalette( pal );
+}
+
+/*!
+ \brief Get 'Use regular expression' search option.
+ \return true if option is switched on; false otherwise.
+ \internal
+*/
+bool PyEditor_FindTool::isRegExp() const
+{
+ return actions()[RegExp]->isChecked();
+}
+
+/*!
+ \brief Get 'Case sensitive search' search option.
+ \return true if option is switched on; false otherwise.
+ \internal
+*/
+bool PyEditor_FindTool::isCaseSensitive() const
+{
+ return actions()[CaseSensitive]->isChecked();
+}
+
+/*!
+ \brief Get 'Whole words only' search option.
+ \return true if option is switched on; false otherwise.
+ \internal
+*/
+bool PyEditor_FindTool::isWholeWord() const
+{
+ return actions()[WholeWord]->isChecked();
+}
+
+/*!
+ \brief Get search options.
+ \param back Search direction: backward if false; forward otherwise.
+ \return List of options
+ \internal
+*/
+QTextDocument::FindFlags PyEditor_FindTool::searchFlags( bool back ) const
+{
+ QTextDocument::FindFlags flags = 0;
+ if ( isCaseSensitive() )
+ flags |= QTextDocument::FindCaseSensitively;
+ if ( isWholeWord() )
+ flags |= QTextDocument::FindWholeWords;
+ if ( back )
+ flags |= QTextDocument::FindBackward;
+ return flags;
+}
+
+/*!
+ \brief Get all matches from Python editor.
+ \param text Text being searched.
+ \return List of all matches.
+ \internal
+*/
+QList PyEditor_FindTool::matches( const QString& text ) const
+{
+ QList results;
+
+ QTextDocument* document = myEditor->document();
+
+ QTextCursor cursor( document );
+ while ( !cursor.isNull() )
+ {
+ cursor = isRegExp() ?
+ document->find( QRegExp( text, isCaseSensitive() ?
+ Qt::CaseSensitive : Qt::CaseInsensitive ),
+ cursor, searchFlags() ) :
+ document->find( text, cursor, searchFlags() );
+ if ( !cursor.isNull() )
+ results.append( cursor );
+ }
+ return results;
+}
+
+/*!
+ \brief Find specified text.
+ \param text Text being searched.
+ \param delta Search direction.
+ \internal
+*/
+void PyEditor_FindTool::find( const QString& text, int delta )
+{
+ QTextCursor cursor = myEditor->textCursor();
+ int position = qMin( cursor.position(), cursor.anchor() ) + delta;
+ cursor.setPosition( position );
+ myEditor->setTextCursor( cursor );
+
+ QList results = matches( text );
+
+ int index = -1;
+ if ( !results.isEmpty() )
+ {
+ if ( delta >= 0 )
+ {
+ // search forward
+ if ( position > results.last().anchor() )
+ position = 0;
+ for ( int i = 0; i < results.count() && index == -1; i++ )
+ {
+ QTextCursor result = results[i];
+ if ( result.hasSelection() && position <= result.anchor() )
+ {
+ index = i;
+ }
+ }
+ }
+ else
+ {
+ // search backward
+ if ( position < results.first().position() )
+ position = results.last().position();
+
+ for ( int i = results.count()-1; i >= 0 && index == -1; i-- )
+ {
+ QTextCursor result = results[i];
+ if ( result.hasSelection() && position >= result.position() )
+ {
+ index = i;
+ }
+ }
+ }
+ }
+ if ( index != -1 )
+ {
+ myInfoLabel->setText( tr( "NB_MATCHED_LABEL" ).arg( index+1 ).arg( results.count() ) );
+ myEditor->setTextCursor( results[index] );
+ }
+ else
+ {
+ myInfoLabel->clear();
+ cursor.clearSelection();
+ myEditor->setTextCursor( cursor );
+ }
+
+ setSearchResult( text.isEmpty() || !results.isEmpty() );
+}
+
+/*!
+ \brief Add completion.
+ \param text Completeion being added.
+ \param replace true to add 'Replace' completion; false to add 'Find' completion.
+ \internal
+*/
+void PyEditor_FindTool::addCompletion( const QString& text, bool replace )
+{
+ QStringListModel& model = replace ? myReplaceCompletion : myFindCompletion;
+
+ QStringList completions = model.stringList();
+ if ( !text.isEmpty() and !completions.contains( text ) )
+ {
+ completions.prepend( text );
+ model.setStringList( completions );
+ }
+}
diff --git a/tools/PyEditor/src/PyEditor_FindTool.h b/tools/PyEditor/src/PyEditor_FindTool.h
new file mode 100644
index 000000000..e6501700e
--- /dev/null
+++ b/tools/PyEditor/src/PyEditor_FindTool.h
@@ -0,0 +1,89 @@
+// Copyright (C) 2015-2016 OPEN CASCADE
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+//
+// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+//
+// File : PyEditor_FindTool.h
+// Author : Vadim SANDLER, Open CASCADE S.A.S. (vadim.sandler@opencascade.com)
+//
+
+#ifndef PYEDITOR_FINDTOOL_H
+#define PYEDITOR_FINDTOOL_H
+
+#include "PyEditor.h"
+
+#include
+#include
+#include
+
+class PyEditor_Editor;
+class QAction;
+class QLabel;
+class QLineEdit;
+
+class PYEDITOR_EXPORT PyEditor_FindTool : public QWidget
+{
+Q_OBJECT
+
+ enum { CaseSensitive, WholeWord, RegExp, Find, FindPrevious, FindNext, Replace };
+
+public:
+ PyEditor_FindTool( PyEditor_Editor*, QWidget* = 0 );
+
+ bool event( QEvent* );
+ bool eventFilter( QObject*, QEvent* );
+
+public slots:
+ void activateFind();
+ void activateReplace();
+
+private slots:
+ void showMenu();
+ void find( const QString& );
+ void find();
+ void findPrevious();
+ void findNext();
+ void replace();
+ void replaceAll();
+ void update();
+ void activate( int );
+
+private:
+ QList shortcuts( int ) const;
+ void updateShortcuts();
+
+ void showReplaceControls( bool );
+ void setSearchResult( bool );
+
+ bool isRegExp() const;
+ bool isCaseSensitive() const;
+ bool isWholeWord() const;
+ QTextDocument::FindFlags searchFlags( bool = false ) const;
+
+ QList matches( const QString& ) const;
+ void find( const QString&, int );
+
+ void addCompletion( const QString&, bool );
+
+private:
+ PyEditor_Editor* myEditor;
+ QLineEdit* myFindEdit;
+ QLineEdit* myReplaceEdit;
+ QLabel* myInfoLabel;
+ QStringListModel myFindCompletion, myReplaceCompletion;
+};
+
+#endif // PYEDITOR_FINDTOOL_H
diff --git a/tools/PyEditor/src/PyEditor_Widget.cxx b/tools/PyEditor/src/PyEditor_Widget.cxx
new file mode 100644
index 000000000..badf97329
--- /dev/null
+++ b/tools/PyEditor/src/PyEditor_Widget.cxx
@@ -0,0 +1,251 @@
+// Copyright (C) 2015-2016 OPEN CASCADE
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+//
+// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+//
+// File : PyEditor_Widget.cxx
+// Author : Vadim SANDLER, Open CASCADE S.A.S. (vadim.sandler@opencascade.com)
+//
+
+#include "PyEditor_Editor.h"
+#include "PyEditor_FindTool.h"
+#include "PyEditor_Widget.h"
+
+#include
+
+/*!
+ \class PyEditor_Widget
+ \brief Wraps Python editor with the find/replace functionality to a single widget.
+*/
+
+/*!
+ \brief Constructor.
+ \param parent Parent widget.
+*/
+PyEditor_Widget::PyEditor_Widget( QWidget* parent )
+{
+ // Create editor.
+ myEditor = new PyEditor_Editor( this );
+
+ // Create find tool.
+ myFindTool = new PyEditor_FindTool( myEditor, this );
+
+ // Set-up layout
+ QVBoxLayout* layout = new QVBoxLayout( this );
+ layout->setContentsMargins( 0, 0, 0, 0 );
+ layout->setSpacing( 3 );
+ layout->addWidget( myEditor );
+ layout->addWidget( myFindTool );
+
+ connect( myEditor, SIGNAL( modificationChanged( bool ) ),
+ this, SIGNAL( modificationChanged( bool ) ) );
+ connect( myEditor, SIGNAL( undoAvailable( bool ) ),
+ this, SIGNAL( undoAvailable( bool ) ) );
+ connect( myEditor, SIGNAL( redoAvailable( bool ) ),
+ this, SIGNAL( redoAvailable( bool ) ) );
+ connect( myEditor, SIGNAL( copyAvailable( bool ) ),
+ this, SIGNAL( copyAvailable( bool ) ) );
+
+ connect( myEditor, SIGNAL( selectionChanged() ),
+ this, SIGNAL( selectionChanged() ) );
+ connect( myEditor, SIGNAL( textChanged() ),
+ this, SIGNAL( textChanged() ) );
+ connect( myEditor, SIGNAL( cursorPositionChanged() ),
+ this, SIGNAL( cursorPositionChanged() ) );
+
+ setFocusProxy( myEditor );
+}
+
+/*!
+ \brief Get editor.
+ \return Pointer to editor.
+*/
+PyEditor_Editor* PyEditor_Widget::editor()
+{
+ return myEditor;
+}
+
+/*!
+ \brief Get find tool.
+ \return Pointer to find tool.
+*/
+PyEditor_FindTool* PyEditor_Widget::findTool()
+{
+ return myFindTool;
+}
+
+/*!
+ \brief Get all custom keywords from editor.
+ \return List of keywords.
+*/
+QStringList PyEditor_Widget::keywords() const
+{
+ return myEditor->keywords();
+}
+
+/*!
+ \brief Set custom keywords to editor.
+ \param keywords List of keywords.
+ \param type Type of keywords (group id).
+ \param color Color of keywords.
+*/
+void PyEditor_Widget::appendKeywords( const QStringList& keywords, int type, const QColor& color )
+{
+ myEditor->appendKeywords( keywords, type, color );
+}
+
+/*!
+ \brief Remove given custom keywords from editor.
+ \param keywords List of keywords to remove.
+*/
+void PyEditor_Widget::removeKeywords( const QStringList& keywords )
+{
+ myEditor->removeKeywords( keywords );
+}
+
+/*!
+ \brief Get current editor's completion policy.
+ \return Completion policy (see PyEditor_Editor::CompletionPolicy).
+*/
+int PyEditor_Widget::completionPolicy() const
+{
+ return (int) myEditor->completionPolicy();
+}
+
+/*!
+ \brief Set editor's completion policy.
+ \param policy Completion policy (see PyEditor_Editor::CompletionPolicy).
+*/
+void PyEditor_Widget::setCompletionPolicy( int policy )
+{
+ myEditor->setCompletionPolicy( (PyEditor_Editor::CompletionPolicy) policy );
+}
+
+/*!
+ \brief Activate Find dialog.
+*/
+void PyEditor_Widget::find()
+{
+ myFindTool->activateFind();
+}
+
+/*!
+ \brief Activate Replace dialog.
+*/
+void PyEditor_Widget::replace()
+{
+ myFindTool->activateReplace();
+}
+
+/*!
+ \brief Undo last editor's operation.
+*/
+void PyEditor_Widget::undo()
+{
+ myEditor->undo();
+}
+
+/*!
+ \brief Redo last undone editor's operation.
+*/
+void PyEditor_Widget::redo()
+{
+ myEditor->redo();
+}
+
+/*!
+ \brief Cut text selected in editor and put it into clipboard.
+*/
+void PyEditor_Widget::cut()
+{
+ myEditor->cut();
+}
+
+/*!
+ \brief Copy text selected in editor into clipboard.
+*/
+void PyEditor_Widget::copy()
+{
+ myEditor->copy();
+}
+
+/*!
+ \brief Paste text from clipboard into editor.
+*/
+void PyEditor_Widget::paste()
+{
+ myEditor->paste();
+}
+
+/*!
+ \brief Delete text selected in editor.
+*/
+void PyEditor_Widget::deleteSelected()
+{
+ myEditor->deleteSelected();
+}
+
+/*!
+ \brief Select all text in editor.
+*/
+void PyEditor_Widget::selectAll()
+{
+ myEditor->selectAll();
+}
+
+/*!
+ \brief Clear content of editor.
+*/
+void PyEditor_Widget::clear()
+{
+ myEditor->clear();
+}
+
+/*!
+ \brief Set/clear modified flag of editor.
+ \param on 'Modified' flag's value.
+*/
+void PyEditor_Widget::setModified( bool on )
+{
+ myEditor->document()->setModified( on );
+}
+
+/*!
+ \brief Get modified flag of editor.
+ \return 'Modified' flag's value.
+*/
+bool PyEditor_Widget::isModified()
+{
+ return myEditor->document()->isModified();
+}
+
+/*!
+ \brief Set text to editor.
+ \param text Text to be put into editor.
+*/
+void PyEditor_Widget::setText( const QString& text )
+{
+ myEditor->setPlainText( text );
+}
+
+/*!
+ \brief Get text from editor.
+ \return Current editor contents.
+*/
+QString PyEditor_Widget::text() const
+{
+ return myEditor->toPlainText();
+}
diff --git a/tools/PyEditor/src/PyEditor_Widget.h b/tools/PyEditor/src/PyEditor_Widget.h
new file mode 100644
index 000000000..0927a9fe8
--- /dev/null
+++ b/tools/PyEditor/src/PyEditor_Widget.h
@@ -0,0 +1,85 @@
+// Copyright (C) 2015-2016 OPEN CASCADE
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+//
+// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+//
+// File : PyEditor_Widget.h
+// Author : Vadim SANDLER, Open CASCADE S.A.S. (vadim.sandler@opencascade.com)
+//
+
+#ifndef PYEDITOR_WIDGET_H
+#define PYEDITOR_WIDGET_H
+
+#include "PyEditor.h"
+
+#include
+
+class PyEditor_Editor;
+class PyEditor_FindTool;
+
+class PYEDITOR_EXPORT PyEditor_Widget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ PyEditor_Widget( QWidget* = 0 );
+
+ PyEditor_Editor* editor();
+ PyEditor_FindTool* findTool();
+
+ bool isModified();
+
+ QString text() const;
+
+ QStringList keywords() const;
+ void appendKeywords( const QStringList&, int, const QColor& = QColor() );
+ void removeKeywords( const QStringList& );
+
+ int completionPolicy() const;
+ void setCompletionPolicy( int );
+
+public slots:
+ void find();
+ void replace();
+
+ void undo();
+ void redo();
+ void cut();
+ void copy();
+ void paste();
+ void deleteSelected();
+ void selectAll();
+ void clear();
+
+ void setModified( bool );
+
+ void setText( const QString& );
+
+signals:
+ void modificationChanged( bool );
+ void undoAvailable( bool );
+ void redoAvailable( bool );
+ void copyAvailable( bool );
+ void selectionChanged();
+ void textChanged();
+ void cursorPositionChanged();
+
+private:
+ PyEditor_Editor* myEditor;
+ PyEditor_FindTool* myFindTool;
+};
+
+#endif // PYEDITOR_WIDGET_H
diff --git a/tools/PyEditor/src/PyEditor_Window.cxx b/tools/PyEditor/src/PyEditor_Window.cxx
index 74c0be042..125a27b81 100644
--- a/tools/PyEditor/src/PyEditor_Window.cxx
+++ b/tools/PyEditor/src/PyEditor_Window.cxx
@@ -21,12 +21,13 @@
//
#include "PyEditor_Window.h"
-#include "PyEditor_Editor.h"
+#include "PyEditor_Widget.h"
#include "PyEditor_Settings.h"
#include "PyEditor_SettingsDlg.h"
#include
#include
+#include
#include
#include
#include
@@ -48,9 +49,9 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
{
Q_INIT_RESOURCE( PyEditor );
- // Create editor and set it as a central widget.
- myTextEditor = new PyEditor_Editor( this );
- setCentralWidget( myTextEditor );
+ // Create central widget.
+ myEditor = new PyEditor_Widget( this );
+ setCentralWidget( myEditor );
// Create actions.
QAction* action;
@@ -81,7 +82,7 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
action->setShortcut( QKeySequence::Save );
connect( action, SIGNAL( triggered( bool ) ), this, SLOT( onSave() ) );
action->setEnabled( false );
- connect( myTextEditor->document(), SIGNAL( modificationChanged( bool ) ),
+ connect( myEditor, SIGNAL( modificationChanged( bool ) ),
action, SLOT( setEnabled( bool ) ) );
myActions[ SaveId ] = action;
@@ -109,9 +110,9 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
action->setToolTip( tr( "TTP_UNDO" ) );
action->setStatusTip( tr( "DSC_UNDO" ) );
action->setShortcut( QKeySequence::Undo );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( undo() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( undo() ) );
action->setEnabled( false );
- connect( myTextEditor->document(), SIGNAL( undoAvailable( bool ) ),
+ connect( myEditor, SIGNAL( undoAvailable( bool ) ),
action, SLOT( setEnabled( bool ) ) );
myActions[ UndoId ] = action;
@@ -121,9 +122,9 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
action->setToolTip( tr( "TTP_REDO" ) );
action->setStatusTip( tr( "DSC_REDO" ) );
action->setShortcut( QKeySequence::Redo );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( redo() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( redo() ) );
action->setEnabled( false );
- connect( myTextEditor->document(), SIGNAL( redoAvailable( bool ) ),
+ connect( myEditor, SIGNAL( redoAvailable( bool ) ),
action, SLOT( setEnabled( bool ) ) );
myActions[ RedoId ] = action;
@@ -133,9 +134,9 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
action->setToolTip( tr( "TTP_CUT" ) );
action->setStatusTip( tr( "DSC_CUT" ) );
action->setShortcut( QKeySequence::Cut );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( cut() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( cut() ) );
action->setEnabled( false );
- connect( myTextEditor, SIGNAL( copyAvailable( bool ) ),
+ connect( myEditor, SIGNAL( copyAvailable( bool ) ),
action, SLOT( setEnabled( bool ) ) );
myActions[ CutId ] = action;
@@ -145,9 +146,9 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
action->setToolTip( tr( "TTP_COPY" ) );
action->setStatusTip( tr( "DSC_COPY" ) );
action->setShortcut( QKeySequence::Copy );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( copy() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( copy() ) );
action->setEnabled( false );
- connect( myTextEditor, SIGNAL( copyAvailable( bool ) ),
+ connect( myEditor, SIGNAL( copyAvailable( bool ) ),
action, SLOT( setEnabled( bool ) ) );
myActions[ CopyId ] = action;
@@ -157,7 +158,7 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
action->setToolTip( tr( "TTP_PASTE" ) );
action->setStatusTip( tr( "DSC_PASTE" ) );
action->setShortcut( QKeySequence::Paste );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( paste() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( paste() ) );
myActions[ PasteId ] = action;
// . Delete
@@ -166,9 +167,9 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
action->setToolTip( tr( "TTP_DELETE" ) );
action->setStatusTip( tr( "DSC_DELETE" ) );
action->setShortcut( QKeySequence::Delete );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( deleteSelected() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( deleteSelected() ) );
action->setEnabled( false );
- connect( myTextEditor, SIGNAL( copyAvailable( bool ) ),
+ connect( myEditor, SIGNAL( copyAvailable( bool ) ),
action, SLOT( setEnabled( bool ) ) );
myActions[ DeleteId ] = action;
@@ -178,9 +179,29 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
action->setToolTip( tr( "TTP_SELECT_ALL" ) );
action->setStatusTip( tr( "DSC_SELECT_ALL" ) );
action->setShortcut( QKeySequence::SelectAll );
- connect( action, SIGNAL( triggered( bool ) ), myTextEditor, SLOT( selectAll() ) );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( selectAll() ) );
myActions[ SelectAllId ] = action;
+ // . Find
+ action = new QAction( QIcon( ":/images/py_find.png" ),
+ tr( "ACT_FIND" ), this );
+ action->setToolTip( tr( "TTP_FIND" ) );
+ action->setStatusTip( tr( "DSC_FIND" ) );
+ action->setShortcut( QKeySequence::Find );
+ action->setShortcutContext( Qt::WidgetShortcut );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( find() ) );
+ myActions[ FindId ] = action;
+
+ // . Replace
+ action = new QAction( QIcon( ":/images/py_replace.png" ),
+ tr( "ACT_REPLACE" ), this );
+ action->setToolTip( tr( "TTP_REPLACE" ) );
+ action->setStatusTip( tr( "DSC_REPLACE" ) );
+ action->setShortcut( QKeySequence::Replace );
+ action->setShortcutContext( Qt::WidgetShortcut );
+ connect( action, SIGNAL( triggered( bool ) ), myEditor, SLOT( replace() ) );
+ myActions[ ReplaceId ] = action;
+
// . Preferences
action = new QAction( QIcon( ":/images/py_preferences.png" ),
tr( "ACT_PREFERENCES" ), this );
@@ -218,6 +239,9 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
menu->addSeparator();
menu->addAction( myActions[ SelectAllId ] );
menu->addSeparator();
+ menu->addAction( myActions[ FindId ] );
+ menu->addAction( myActions[ ReplaceId ] );
+ menu->addSeparator();
menu->addAction( myActions[ PreferencesId ] );
menu = menuBar()->addMenu( tr( "MNU_HELP" ) );
@@ -242,6 +266,9 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
toolbar->addAction( myActions[ DeleteId ] );
toolbar->addAction( myActions[ SelectAllId ] );
toolbar->addSeparator();
+ toolbar->addAction( myActions[ FindId ] );
+ toolbar->addAction( myActions[ ReplaceId ] );
+ toolbar->addSeparator();
toolbar->addAction( myActions[ PreferencesId ] );
toolbar->addSeparator();
toolbar->addAction( myActions[ HelpId ] );
@@ -250,7 +277,7 @@ PyEditor_Window::PyEditor_Window( QWidget* parent ) :
setCurrentFile( QString() );
// Additional set-up for main window.
- connect( myTextEditor->document(), SIGNAL( modificationChanged( bool ) ),
+ connect( myEditor, SIGNAL( modificationChanged( bool ) ),
this, SLOT( setWindowModified( bool ) ) );
// Initialize status bar.
@@ -283,7 +310,7 @@ void PyEditor_Window::onNew()
{
if ( whetherSave() )
{
- myTextEditor->clear();
+ myEditor->clear();
setCurrentFile( QString() );
}
}
@@ -343,7 +370,7 @@ bool PyEditor_Window::onSaveAs()
*/
void PyEditor_Window::onPreferences()
{
- PyEditor_SettingsDlg dlg( myTextEditor, true, this );
+ PyEditor_SettingsDlg dlg( myEditor->editor(), true, this );
connect( &dlg, SIGNAL( help() ), this, SLOT( onHelp() ) );
dlg.exec();
}
@@ -355,7 +382,7 @@ void PyEditor_Window::onPreferences()
void PyEditor_Window::setCurrentFile( const QString& filePath )
{
myURL = filePath;
- myTextEditor->document()->setModified( false );
+ myEditor->setModified( false );
setWindowModified( false );
@@ -369,7 +396,7 @@ void PyEditor_Window::setCurrentFile( const QString& filePath )
*/
bool PyEditor_Window::whetherSave()
{
- if ( myTextEditor->document()->isModified() )
+ if ( myEditor->isModified() )
{
QMessageBox::StandardButton answer = QMessageBox::warning( this,
tr( "NAME_PYEDITOR" ),
@@ -394,19 +421,20 @@ bool PyEditor_Window::whetherSave()
\brief Open file.
\param filePath file path
*/
-void PyEditor_Window::loadFile( const QString& filePath )
+void PyEditor_Window::loadFile( const QString& filePath, bool verbose )
{
QFile aFile( filePath );
if ( !aFile.open(QFile::ReadOnly | QFile::Text) )
{
- QMessageBox::warning( this, tr( "NAME_PYEDITOR" ),
- tr( "WRN_READ_FILE" ).arg( filePath ).arg( aFile.errorString() ) );
+ if ( verbose )
+ QMessageBox::warning( this, tr( "NAME_PYEDITOR" ),
+ tr( "WRN_READ_FILE" ).arg( filePath ).arg( aFile.errorString() ) );
return;
}
QTextStream anInput( &aFile );
QApplication::setOverrideCursor( Qt::WaitCursor );
- myTextEditor->setPlainText( anInput.readAll() );
+ myEditor->setText( anInput.readAll() );
QApplication::restoreOverrideCursor();
setCurrentFile( filePath );
@@ -419,19 +447,20 @@ void PyEditor_Window::loadFile( const QString& filePath )
\brief Save file.
\param filePath file path
*/
-bool PyEditor_Window::saveFile( const QString& filePath )
+bool PyEditor_Window::saveFile( const QString& filePath, bool verbose )
{
QFile aFile( filePath );
if ( !aFile.open( QFile::WriteOnly | QFile::Text ) )
{
- QMessageBox::warning( this, tr( "NAME_PYEDITOR" ),
- tr( "WRN_WRITE_FILE" ).arg( filePath ).arg( aFile.errorString() ) );
+ if ( verbose )
+ QMessageBox::warning( this, tr( "NAME_PYEDITOR" ),
+ tr( "WRN_WRITE_FILE" ).arg( filePath ).arg( aFile.errorString() ) );
return false;
}
QTextStream anOutput( &aFile );
QApplication::setOverrideCursor( Qt::WaitCursor );
- anOutput << myTextEditor->toPlainText();
+ anOutput << myEditor->text();
QApplication::restoreOverrideCursor();
setCurrentFile( filePath );
diff --git a/tools/PyEditor/src/PyEditor_Window.h b/tools/PyEditor/src/PyEditor_Window.h
index 209739e77..1369db1cd 100644
--- a/tools/PyEditor/src/PyEditor_Window.h
+++ b/tools/PyEditor/src/PyEditor_Window.h
@@ -29,7 +29,7 @@
#include
class QAction;
-class PyEditor_Editor;
+class PyEditor_Widget;
class PYEDITOR_EXPORT PyEditor_Window : public QMainWindow
{
@@ -38,11 +38,15 @@ class PYEDITOR_EXPORT PyEditor_Window : public QMainWindow
public:
enum { NewId, OpenId, SaveId, SaveAsId, ExitId,
UndoId, RedoId, CutId, CopyId, PasteId, DeleteId, SelectAllId,
+ FindId, ReplaceId,
PreferencesId, HelpId };
PyEditor_Window( QWidget* = 0 );
~PyEditor_Window();
+ void loadFile( const QString&, bool = true );
+ bool saveFile( const QString&, bool = true );
+
protected:
virtual void closeEvent( QCloseEvent* );
@@ -54,16 +58,12 @@ private Q_SLOTS:
void onPreferences();
void onHelp();
-private:
- void loadFile( const QString& );
- bool saveFile( const QString& );
-
void setCurrentFile( const QString& );
bool whetherSave();
QString defaultName() const;
private:
- PyEditor_Editor* myTextEditor;
+ PyEditor_Widget* myEditor;
QString myURL;
QMap myActions;
};
diff --git a/tools/PyEditor/src/python/PyEditorPy.sip b/tools/PyEditor/src/python/PyEditorPy.sip
index 499ee9574..d6b530be9 100644
--- a/tools/PyEditor/src/python/PyEditorPy.sip
+++ b/tools/PyEditor/src/python/PyEditorPy.sip
@@ -103,3 +103,75 @@ private:
PyEditor_Editor( const PyEditor_Editor& );
PyEditor_Editor& operator=( const PyEditor_Editor& );
};
+
+class PyEditor_FindTool : public QWidget
+{
+%TypeHeaderCode
+#include
+%End
+
+public:
+ explicit PyEditor_FindTool( PyEditor_Editor* /TransferThis/, QWidget* /TransferThis/ = 0 );
+
+public slots:
+ void activateFind();
+ void activateReplace();
+
+private:
+ PyEditor_FindTool( const PyEditor_FindTool& );
+ PyEditor_FindTool& operator=( const PyEditor_FindTool& );
+};
+
+class PyEditor_Widget : public QWidget
+{
+%TypeHeaderCode
+#include
+%End
+
+public:
+ explicit PyEditor_Widget( QWidget* /TransferThis/ = 0 );
+
+ PyEditor_Editor* editor();
+ PyEditor_FindTool* findTool();
+
+ bool isModified();
+
+ QString text() const;
+
+ QStringList keywords() const;
+ void appendKeywords( const QStringList&, int, const QColor& = QColor() );
+ void removeKeywords( const QStringList& );
+
+ int completionPolicy() const;
+ void setCompletionPolicy( int );
+
+public slots:
+ void find();
+ void replace();
+
+ void undo();
+ void redo();
+ void cut();
+ void copy();
+ void paste();
+ void deleteSelected();
+ void selectAll();
+ void clear();
+
+ void setModified( bool );
+
+ void setText( const QString& );
+
+signals:
+ void modificationChanged( bool );
+ void undoAvailable( bool );
+ void redoAvailable( bool );
+ void copyAvailable( bool );
+ void selectionChanged();
+ void textChanged();
+ void cursorPositionChanged();
+
+private:
+ PyEditor_Widget( const PyEditor_Widget& );
+ PyEditor_Widget& operator=( const PyEditor_Widget& );
+};
diff --git a/tools/PyEditor/src/resources/PyEditor.qrc b/tools/PyEditor/src/resources/PyEditor.qrc
index 7df3d3208..d2ab431f7 100644
--- a/tools/PyEditor/src/resources/PyEditor.qrc
+++ b/tools/PyEditor/src/resources/PyEditor.qrc
@@ -5,14 +5,19 @@
images/py_delete.png
images/py_editor.png
images/py_exit.png
+ images/py_find.png
+ images/py_find_next.png
+ images/py_find_previous.png
images/py_help.png
images/py_new.png
images/py_open.png
images/py_paste.png
images/py_preferences.png
images/py_redo.png
+ images/py_replace.png
images/py_save.png
images/py_save_as.png
+ images/py_search.png
images/py_select_all.png
images/py_undo.png
about.txt
diff --git a/tools/PyEditor/src/resources/about.txt b/tools/PyEditor/src/resources/about.txt
index 75e476a52..9f914c70c 100644
--- a/tools/PyEditor/src/resources/about.txt
+++ b/tools/PyEditor/src/resources/about.txt
@@ -1,10 +1,16 @@
Python Editor
+
Python Editor is a simple program for writing Python scripts.
+
+
+
Program provides standard editing operations like copy/cut/paste, undo/redo, select all, delete, etc.
-Also it supports syntax highlighting and auto-indentation of Python code.
+It supports syntax highlighting and auto-indentation of Python code.
+
+
Most often used editing operations are available via the toolbar:
New : creates new document.
@@ -22,7 +28,13 @@ Most often used editing operations are available via the toolbar:
Preferences : opens Preferences dialog that allows specifying advanced parameters for the program.
Help : shows this help information.
+
+
+
+Also, editor supports standard Find and Replace operations.
+
+
The behavior of the editor can be customized via the Preferences dialog. The following options can be customized:
Font settings : choose the font.
@@ -35,3 +47,4 @@ The behavior of the editor can be customized via the Preferences dialog.
Display tab delimiters : displays tab marks at a given number of white spaces.
Save settings as default : saves chosen options as a default ones. These settings will be restored after application restart.
+
diff --git a/tools/PyEditor/src/resources/images/py_find.png b/tools/PyEditor/src/resources/images/py_find.png
new file mode 100644
index 0000000000000000000000000000000000000000..be705fd8322a1535f7f65bf9c56db04a56d4cd4c
GIT binary patch
literal 19794
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