From fe79131481b143cb13ff6a4ae1aab0b05fa8ab53 Mon Sep 17 00:00:00 2001 From: vsv Date: Wed, 2 Apr 2014 16:39:10 +0400 Subject: [PATCH] Python console added to the application --- CMakeCommon/FindPython.cmake | 2 - CMakeLists.txt | 4 + src/ModelAPI/CMakeLists.txt | 2 - src/PyConsole/CMakeLists.txt | 64 ++ src/PyConsole/PyConsole.h | 24 + src/PyConsole/PyConsole_Console.cxx | 341 ++++++ src/PyConsole/PyConsole_Console.h | 88 ++ src/PyConsole/PyConsole_Editor.cxx | 1105 +++++++++++++++++++ src/PyConsole/PyConsole_Editor.h | 72 ++ src/PyConsole/PyConsole_EnhEditor.cxx | 464 ++++++++ src/PyConsole/PyConsole_EnhEditor.h | 78 ++ src/PyConsole/PyConsole_EnhInterp.cxx | 135 +++ src/PyConsole/PyConsole_EnhInterp.h | 47 + src/PyConsole/PyConsole_Event.cxx | 3 + src/PyConsole/PyConsole_Event.h | 48 + src/PyConsole/PyConsole_Interp.cxx | 118 ++ src/PyConsole/PyConsole_Interp.h | 20 + src/PyConsole/PyConsole_Request.cxx | 99 ++ src/PyConsole/PyConsole_Request.h | 86 ++ src/PyConsole/resources/PyConsole_msg_en.ts | 42 + src/PyConsole/resources/PyConsole_msg_fr.ts | 42 + src/PyConsole/resources/PyConsole_msg_ja.ts | 42 + src/PyEvent/CMakeLists.txt | 24 + src/PyEvent/PyEvent.h | 19 + src/PyEvent/PyEvent_Event.cxx | 287 +++++ src/PyEvent/PyEvent_Event.h | 204 ++++ src/PyEvent/PyEvent_EventFilter.cxx | 60 + src/PyEvent/PyEvent_EventFilter.h | 43 + src/PyInterp/CMakeLists.txt | 46 + src/PyInterp/PyInterp.h | 40 + src/PyInterp/PyInterp_Dispatcher.cxx | 199 ++++ src/PyInterp/PyInterp_Dispatcher.h | 46 + src/PyInterp/PyInterp_Event.cxx | 9 + src/PyInterp/PyInterp_Event.h | 52 + src/PyInterp/PyInterp_Interp.cxx | 533 +++++++++ src/PyInterp/PyInterp_Interp.h | 95 ++ src/PyInterp/PyInterp_Request.cxx | 4 + src/PyInterp/PyInterp_Request.h | 81 ++ src/PyInterp/PyInterp_Watcher.h | 24 + src/XGUI/CMakeLists.txt | 8 +- src/XGUI/XGUI_MainMenu.cpp | 1 + src/XGUI/XGUI_MainMenu.h | 5 + src/XGUI/XGUI_MainWindow.cpp | 57 +- src/XGUI/XGUI_MainWindow.h | 9 +- src/XGUI/XGUI_ViewPort.cpp | 2 - src/XGUI/XGUI_ViewWindow.cpp | 11 +- src/XGUI/XGUI_ViewWindow.h | 6 +- src/XGUI/XGUI_Viewer.cpp | 3 +- src/XGUI/XGUI_Workshop.cpp | 3 +- src/XGUI/XGUI_msg_en.ts | 8 +- 50 files changed, 4769 insertions(+), 36 deletions(-) create mode 100644 src/PyConsole/CMakeLists.txt create mode 100644 src/PyConsole/PyConsole.h create mode 100644 src/PyConsole/PyConsole_Console.cxx create mode 100644 src/PyConsole/PyConsole_Console.h create mode 100644 src/PyConsole/PyConsole_Editor.cxx create mode 100644 src/PyConsole/PyConsole_Editor.h create mode 100644 src/PyConsole/PyConsole_EnhEditor.cxx create mode 100644 src/PyConsole/PyConsole_EnhEditor.h create mode 100644 src/PyConsole/PyConsole_EnhInterp.cxx create mode 100644 src/PyConsole/PyConsole_EnhInterp.h create mode 100644 src/PyConsole/PyConsole_Event.cxx create mode 100644 src/PyConsole/PyConsole_Event.h create mode 100644 src/PyConsole/PyConsole_Interp.cxx create mode 100644 src/PyConsole/PyConsole_Interp.h create mode 100644 src/PyConsole/PyConsole_Request.cxx create mode 100644 src/PyConsole/PyConsole_Request.h create mode 100644 src/PyConsole/resources/PyConsole_msg_en.ts create mode 100644 src/PyConsole/resources/PyConsole_msg_fr.ts create mode 100644 src/PyConsole/resources/PyConsole_msg_ja.ts create mode 100644 src/PyEvent/CMakeLists.txt create mode 100644 src/PyEvent/PyEvent.h create mode 100644 src/PyEvent/PyEvent_Event.cxx create mode 100644 src/PyEvent/PyEvent_Event.h create mode 100644 src/PyEvent/PyEvent_EventFilter.cxx create mode 100644 src/PyEvent/PyEvent_EventFilter.h create mode 100644 src/PyInterp/CMakeLists.txt create mode 100644 src/PyInterp/PyInterp.h create mode 100644 src/PyInterp/PyInterp_Dispatcher.cxx create mode 100644 src/PyInterp/PyInterp_Dispatcher.h create mode 100644 src/PyInterp/PyInterp_Event.cxx create mode 100644 src/PyInterp/PyInterp_Event.h create mode 100644 src/PyInterp/PyInterp_Interp.cxx create mode 100644 src/PyInterp/PyInterp_Interp.h create mode 100644 src/PyInterp/PyInterp_Request.cxx create mode 100644 src/PyInterp/PyInterp_Request.h create mode 100644 src/PyInterp/PyInterp_Watcher.h diff --git a/CMakeCommon/FindPython.cmake b/CMakeCommon/FindPython.cmake index b1aa8c531..543d830ad 100644 --- a/CMakeCommon/FindPython.cmake +++ b/CMakeCommon/FindPython.cmake @@ -9,8 +9,6 @@ ENDIF() FILE(TO_CMAKE_PATH "$ENV{PYTHON_LIB_DIR}/${PYTHON_LIBRARY_DLL}" PYTHON_LIBRARY) FILE(TO_CMAKE_PATH $ENV{PYTHON_INC_DIR} PYTHON_INCLUDE_DIR) -# TOOD: Clean a mess with python interpreter -# FIND_PACKAGE(PythonInterp) FIND_PACKAGE(PythonLibs) INCLUDE_DIRECTORIES(${PYTHON_INCLUDE_DIRS}) diff --git a/CMakeLists.txt b/CMakeLists.txt index 68d6a4f2e..f13e37375 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ SET(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeCommon" ${CMAKE_MODULE_PATH}) INCLUDE(Common) INCLUDE(FindQt5) +INCLUDE(FindPython) ADD_SUBDIRECTORY (src/Event) ADD_SUBDIRECTORY (src/Model) @@ -13,4 +14,7 @@ ADD_SUBDIRECTORY (src/ModelAPI) ADD_SUBDIRECTORY (src/Config) ADD_SUBDIRECTORY (src/PartSet) ADD_SUBDIRECTORY (src/PartSetPlugin) +ADD_SUBDIRECTORY (src/PyEvent) +ADD_SUBDIRECTORY (src/PyInterp) +ADD_SUBDIRECTORY (src/PyConsole) ADD_SUBDIRECTORY (src/XGUI) diff --git a/src/ModelAPI/CMakeLists.txt b/src/ModelAPI/CMakeLists.txt index 33fe1bcf8..0013e1b6a 100644 --- a/src/ModelAPI/CMakeLists.txt +++ b/src/ModelAPI/CMakeLists.txt @@ -1,9 +1,7 @@ CMAKE_MINIMUM_REQUIRED(VERSION 2.8.11) -INCLUDE(Common) FIND_PACKAGE(SWIG REQUIRED) INCLUDE(${SWIG_USE_FILE}) -INCLUDE(FindPython) INCLUDE(FindBoost) INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/PyConsole/CMakeLists.txt b/src/PyConsole/CMakeLists.txt new file mode 100644 index 000000000..5d8b5820e --- /dev/null +++ b/src/PyConsole/CMakeLists.txt @@ -0,0 +1,64 @@ + +SET(CMAKE_AUTOMOC ON) + +# header files +SET(PROJECT_HEADERS + PyConsole.h + PyConsole_Console.h + PyConsole_Editor.h + PyConsole_EnhEditor.h + PyConsole_EnhInterp.h + PyConsole_Event.h + PyConsole_Interp.h + PyConsole_Request.h +) + +SET(PROJECT_AUTOMOC + ${CMAKE_CURRENT_BINARY_DIR}/PyConsole_automoc.cpp +) + +# resource files / to be processed by lrelease +SET(TEXT_RESOURCES + resources/PyConsole_msg_en.ts + resources/PyConsole_msg_fr.ts + resources/PyConsole_msg_ja.ts +) +# sources / static +SET(PROJECT_SOURCES + PyConsole_Console.cxx + PyConsole_Editor.cxx + PyConsole_EnhEditor.cxx + PyConsole_EnhInterp.cxx + PyConsole_Event.cxx + PyConsole_Interp.cxx + PyConsole_Request.cxx +) + +SET(PROJECT_LIBRARIES + PyInterp + ${Qt5Widgets_LIBRARIES} +) + +QT5_ADD_TRANSLATION(QM_RESOURCES ${TEXT_RESOURCES}) + +SOURCE_GROUP ("Generated Files" FILES ${PROJECT_AUTOMOC} ${QM_RESOURCES}) + +INCLUDE_DIRECTORIES( + ${PROJECT_SOURCE_DIR}/src/PyEvent + ${PROJECT_SOURCE_DIR}/src/PyInterp +) + +ADD_DEFINITIONS(-DPYCONSOLE_EXPORTS -DHAVE_DEBUG_PYTHON) + +ADD_LIBRARY(PyConsole STATIC + ${PROJECT_HEADERS} + ${PROJECT_SOURCES} + ${TEXT_RESOURCES} + ${QM_RESOURCES} +) + +TARGET_LINK_LIBRARIES(PyConsole ${PROJECT_LIBRARIES}) + +#INSTALL(TARGETS PyConsole DESTINATION bin) + + diff --git a/src/PyConsole/PyConsole.h b/src/PyConsole/PyConsole.h new file mode 100644 index 000000000..1df9180c1 --- /dev/null +++ b/src/PyConsole/PyConsole.h @@ -0,0 +1,24 @@ + +#if !defined ( PYCONSOLE_H ) +#define PYCONSOLE_H + +// ======================================================== +// set dllexport type for Win platform +#ifdef WIN32 +# if defined PYCONSOLE_EXPORTS || defined PyConsole_EXPORTS +# define PYCONSOLE_EXPORT __declspec(dllexport) +# else +# define PYCONSOLE_EXPORT __declspec(dllimport) +# endif +#else // WIN32 +# define PYCONSOLE_EXPORT +#endif // WIN32 + +// ======================================================== +// avoid warning messages +#ifdef WIN32 +#pragma warning (disable : 4786) +#pragma warning (disable : 4251) +#endif + +#endif // PYCONSOLE_H diff --git a/src/PyConsole/PyConsole_Console.cxx b/src/PyConsole/PyConsole_Console.cxx new file mode 100644 index 000000000..04e244ed0 --- /dev/null +++ b/src/PyConsole/PyConsole_Console.cxx @@ -0,0 +1,341 @@ + +/*! + \class PyConsole_Console + \brief Python console widget. +*/ + +#include "PyConsole_Interp.h" /// !!! WARNING !!! THIS INCLUDE MUST BE VERY FIRST !!! +#include "PyConsole_Console.h" +#include "PyConsole_EnhEditor.h" +#include "PyConsole_EnhInterp.h" + +//#include + +#include +#include +#include +#include +#include +#include + +/*! + \brief Constructor. + + Creates new python console widget. + \param parent parent widget + \param interp python interpreter +*/ +PyConsole_Console::PyConsole_Console( QWidget* parent, PyConsole_Interp* interp ) +: QWidget( parent ) +{ + // create python interpreter + myInterp = interp; + if ( !myInterp ) + myInterp = new PyConsole_Interp(); + + // initialize Python interpretator + myInterp->initialize(); + + // create editor console + QVBoxLayout* lay = new QVBoxLayout( this ); + lay->setMargin( 0 ); + myEditor = new PyConsole_Editor( myInterp, this ); + char* synchronous = getenv("PYTHON_CONSOLE_SYNC"); + if (synchronous && atoi(synchronous)) + { + myEditor->setIsSync(true); + } + myEditor->viewport()->installEventFilter( this ); + lay->addWidget( myEditor ); + + createActions(); +} + +/** + * Protected constructor. + */ +PyConsole_Console::PyConsole_Console( QWidget* parent, PyConsole_Interp* i, PyConsole_Editor* e) + : QWidget (parent), myEditor(e), myInterp(i) +{} + +/*! + \brief Destructor. + + Does nothing for the moment. +*/ +PyConsole_Console::~PyConsole_Console() +{ +} + +/*! + \brief Execute python command in the interpreter. + \param command string with command and arguments +*/ +void PyConsole_Console::exec( const QString& command ) +{ + if ( myEditor ) + myEditor->exec( command ); +} + +/*! + \brief Execute python command in the interpreter + and wait until it is finished. + + Block execution of main application until the python command is executed. + \param command string with command and arguments +*/ +void PyConsole_Console::execAndWait( const QString& command ) +{ + if ( myEditor ) + myEditor->execAndWait( command ); +} + +/*! + \brief Get synchronous mode flag value. + + \sa setIsSync() + \return True if python console works in synchronous mode +*/ +bool PyConsole_Console::isSync() const +{ + return myEditor->isSync(); +} + +/*! + \brief Set synchronous mode flag value. + + In synhronous mode the Python commands are executed in the GUI thread + and the GUI is blocked until the command is finished. In the asynchronous + mode each Python command is executed in the separate thread that does not + block the main GUI loop. + + \param on synhronous mode flag +*/ +void PyConsole_Console::setIsSync( const bool on ) +{ + myEditor->setIsSync( on ); +} + +/*! + \brief Get suppress output flag value. + + \sa setIsSuppressOutput() + \return True if python console output is suppressed. +*/ +bool PyConsole_Console::isSuppressOutput() const +{ + return myEditor->isSuppressOutput(); +} + +/*! + \brief Set suppress output flag value. + + In case if suppress output flag is true, the python + console output suppressed. + + \param on suppress output flag +*/ +void PyConsole_Console::setIsSuppressOutput( const bool on ) +{ + myEditor->setIsSuppressOutput(on); +} + +/*! + \brief Get 'show banner' flag value. + + \sa setIsShowBanner() + \return \c true if python console shows banner +*/ +bool PyConsole_Console::isShowBanner() const +{ + return myEditor->isShowBanner(); +} + +/*! + \brief Set 'show banner' flag value. + + The banner is shown in the top of the python console window. + + \sa isShowBanner() + \param on 'show banner' flag +*/ +void PyConsole_Console::setIsShowBanner( const bool on ) +{ + myEditor->setIsShowBanner( on ); +} + +/*! + \brief Change the python console's font. + \param f new font +*/ +void PyConsole_Console::setFont( const QFont& f ) +{ + if( myEditor ) + myEditor->setFont( f ); +} + +/*! + \brief Get python console font. + \return current python console's font +*/ +QFont PyConsole_Console::font() const +{ + QFont res; + if( myEditor ) + res = myEditor->font(); + return res; +} + +/*! + \brief Event handler. + + Handles context menu request event. + + \param o object + \param e event + \return True if the event is processed and further processing should be stopped +*/ +bool PyConsole_Console::eventFilter( QObject* o, QEvent* e ) +{ + if ( o == myEditor->viewport() && e->type() == QEvent::ContextMenu ) + { + //contextMenuRequest( (QContextMenuEvent*)e ); + return true; + } + return QWidget::eventFilter( o, e ); +} + +/*! + \brief Create the context popup menu. + + Fill in the popup menu with the commands. + + \param menu context popup menu +*/ +void PyConsole_Console::contextMenuPopup( QMenu* menu ) +{ + if ( myEditor->isReadOnly() ) + return; + + menu->addAction( myActions[CopyId] ); + menu->addAction( myActions[PasteId] ); + menu->addAction( myActions[ClearId] ); + menu->addSeparator(); + menu->addAction( myActions[SelectAllId] ); + menu->addSeparator(); + menu->addAction( myActions[DumpCommandsId] ); + + //Qtx::simplifySeparators( menu ); + + updateActions(); +} + +/*! + \brief Set actions to be visible in the context popup menu. + + Actions, which IDs are set in \a flags parameter, will be shown in the + context popup menu. Other actions will not be shown. + + \param flags ORed together actions flags +*/ +void PyConsole_Console::setMenuActions( const int flags ) +{ + myActions[CopyId]->setVisible( flags & CopyId ); + myActions[PasteId]->setVisible( flags & PasteId ); + myActions[ClearId]->setVisible( flags & ClearId ); + myActions[SelectAllId]->setVisible( flags & SelectAllId ); + myActions[DumpCommandsId]->setVisible( flags & DumpCommandsId ); +} + +/*! + \brief Get menu actions which are currently visible in the context popup menu. + \return ORed together actions flags + \sa setMenuActions() +*/ +int PyConsole_Console::menuActions() const +{ + int ret = 0; + ret = ret | ( myActions[CopyId]->isVisible() ? CopyId : 0 ); + ret = ret | ( myActions[PasteId]->isVisible() ? PasteId : 0 ); + ret = ret | ( myActions[ClearId]->isVisible() ? ClearId : 0 ); + ret = ret | ( myActions[SelectAllId]->isVisible() ? SelectAllId : 0 ); + ret = ret | ( myActions[DumpCommandsId]->isVisible() ? DumpCommandsId : 0 ); + return ret; +} + +/*! + \brief Create menu actions. + + Create context popup menu actions. +*/ +void PyConsole_Console::createActions() +{ + QAction* a = new QAction( tr( "EDIT_COPY_CMD" ), this ); + a->setStatusTip( tr( "EDIT_COPY_CMD" ) ); + connect( a, SIGNAL( triggered( bool ) ), myEditor, SLOT( copy() ) ); + myActions.insert( CopyId, a ); + + a = new QAction( tr( "EDIT_PASTE_CMD" ), this ); + a->setStatusTip( tr( "EDIT_PASTE_CMD" ) ); + connect( a, SIGNAL( triggered( bool ) ), myEditor, SLOT( paste() ) ); + myActions.insert( PasteId, a ); + + a = new QAction( tr( "EDIT_CLEAR_CMD" ), this ); + a->setStatusTip( tr( "EDIT_CLEAR_CMD" ) ); + connect( a, SIGNAL( triggered( bool ) ), myEditor, SLOT( clear() ) ); + myActions.insert( ClearId, a ); + + a = new QAction( tr( "EDIT_SELECTALL_CMD" ), this ); + a->setStatusTip( tr( "EDIT_SELECTALL_CMD" ) ); + connect( a, SIGNAL( triggered( bool ) ), myEditor, SLOT( selectAll() ) ); + myActions.insert( SelectAllId, a ); + + a = new QAction( tr( "EDIT_DUMPCOMMANDS_CMD" ), this ); + a->setStatusTip( tr( "EDIT_DUMPCOMMANDS_CMD" ) ); + connect( a, SIGNAL( triggered( bool ) ), myEditor, SLOT( dump() ) ); + myActions.insert( DumpCommandsId, a ); +} + +/*! + \brief Update menu actions. + + Update context popup menu action state. +*/ +void PyConsole_Console::updateActions() +{ + myActions[CopyId]->setEnabled( myEditor->textCursor().hasSelection() ); + myActions[PasteId]->setEnabled( !myEditor->isReadOnly() && !QApplication::clipboard()->text().isEmpty() ); + myActions[SelectAllId]->setEnabled( !myEditor->document()->isEmpty() ); +} + +/** + * Similar to constructor of the base class but using enhanced objects. + * TODO: this should really be done in a factory to avoid code duplication. + * @param parent + * @param interp + */ +PyConsole_EnhConsole::PyConsole_EnhConsole( QWidget* parent, PyConsole_EnhInterp* interp) + : PyConsole_Console(parent, interp, 0) +{ + // create python interpreter + myInterp = interp; + if ( !myInterp ) + myInterp = new PyConsole_EnhInterp(); + + // initialize Python interpretator + myInterp->initialize(); + + // create editor console + QVBoxLayout* lay = new QVBoxLayout( this ); + lay->setMargin( 0 ); + myEditor = new PyConsole_EnhEditor( static_cast(myInterp), this ); + char* synchronous = getenv("PYTHON_CONSOLE_SYNC"); + if (synchronous && atoi(synchronous)) + { + myEditor->setIsSync(true); + } + myEditor->viewport()->installEventFilter( this ); + lay->addWidget( myEditor ); + + createActions(); +} diff --git a/src/PyConsole/PyConsole_Console.h b/src/PyConsole/PyConsole_Console.h new file mode 100644 index 000000000..0f2a9be5a --- /dev/null +++ b/src/PyConsole/PyConsole_Console.h @@ -0,0 +1,88 @@ + +#ifndef PYCONSOLE_CONSOLE_H +#define PYCONSOLE_CONSOLE_H + +#include "PyConsole.h" + +//#include +#include +#include + +class PyConsole_Interp; +class PyConsole_Editor; +class PyConsole_EnhInterp; +class QMenu; + +class PYCONSOLE_EXPORT PyConsole_Console : public QWidget//, public SUIT_PopupClient +{ + Q_OBJECT + +public: + //! Context popup menu actions flags + enum + { + CopyId = 0x01, //!< "Copy" menu action + PasteId = 0x02, //!< "Paste" menu action + ClearId = 0x04, //!< "Clear" menu action + SelectAllId = 0x08, //!< "Select All" menu action + DumpCommandsId = 0x16, //!< "DumpCommands" menu action + All = CopyId | PasteId | ClearId | SelectAllId | DumpCommandsId //!< all menu actions + }; + +public: + PyConsole_Console( QWidget* parent, PyConsole_Interp* interp = 0 ); + virtual ~PyConsole_Console(); + + //! \brief Get python interperter + PyConsole_Interp* getInterp() { return myInterp; } + QFont font() const; + virtual void setFont( const QFont& ); + + bool isSync() const; + void setIsSync( const bool ); + + bool isSuppressOutput() const; + void setIsSuppressOutput( const bool ); + + bool isShowBanner() const; + void setIsShowBanner( const bool ); + + void exec( const QString& ); + void execAndWait( const QString& ); + + virtual bool eventFilter( QObject*, QEvent* ); + + //! \brief Get popup client symbolic name + virtual QString popupClientType() const { return QString( "PyConsole" ); } + virtual void contextMenuPopup( QMenu* ); + + void setMenuActions( const int ); + int menuActions() const; + +protected: + void createActions(); + void updateActions(); + + PyConsole_Console( QWidget* parent, PyConsole_Interp*, PyConsole_Editor*); + + + PyConsole_Interp* myInterp; //!< python interpreter + PyConsole_Editor* myEditor; //!< python console editor widget + QMap myActions; //!< menu actions list +}; + +/** + * Enhance console object providing auto-completion. + * Similar to PyConsole_Console except that an enhanced interpreter and enhanced editor + * are encapsulated. + */ +class PYCONSOLE_EXPORT PyConsole_EnhConsole: public PyConsole_Console +{ + Q_OBJECT + +public: + PyConsole_EnhConsole( QWidget* parent, PyConsole_EnhInterp* interp = 0); + virtual ~PyConsole_EnhConsole() {} +}; + +#endif // PYCONSOLE_CONSOLE_H diff --git a/src/PyConsole/PyConsole_Editor.cxx b/src/PyConsole/PyConsole_Editor.cxx new file mode 100644 index 000000000..62ef9e8c3 --- /dev/null +++ b/src/PyConsole/PyConsole_Editor.cxx @@ -0,0 +1,1105 @@ + +/*! + \class PyConsole_Editor + \brief Python command line interpreter front-end GUI widget. + + This class provides simple GUI interface to the Python interpreter, including basic + navigation operations, executing commands (both interactively and programmatically), + copy-paste operations, history of the commands and so on. + + Here below is the shortcut keyboard boundings used for navigation and other operations: + - : execute current command + - : clear current command + - : clear current command + - : previous command in the history + - : move cursor one row up with selection + - : move cursor one row up without selection + - : move cursor one row up with selection + - : next command in the history + - : move cursor one row down with selection + - : move cursor one row down without selection + - : move cursor one row down with selection + - : move one symbol left without selection + - : move one symbol left with selection + - : move one word left without selection + - : move one word left with selection + - : move one symbol right without selection + - : move one symbol right with selection + - : move one word right without selection + - : move one word right with selection + - : first command in the history + - : move one page up with selection + - : move one page up without selection + - : scroll one page up + - : last command in the history + - : move one page down with selection + - : move one page down without selection + - : scroll one page down + - : move to the beginning of the line without selection + - : move to the beginning of the line with selection + - : move to the very first symbol without selection + - : move to the very first symbol with selection + - : move to the end of the line without selection + - : move to the end of the line with selection + - : move to the very last symbol without selection + - : move to the very last symbol with selection + - : delete symbol before the cursor + / remove selected text and put it to the clipboard (cut) + - : delete previous word + / remove selected text and put it to the clipboard (cut) + - : delete text from the cursor to the beginning of the line + / remove selected text and put it to the clipboard (cut) + - : delete symbol after the cursor + / remove selected text and put it to the clipboard (cut) + - : delete next word + / remove selected text and put it to the clipboard (cut) + - : delete text from the cursor to the end of the line + / remove selected text and put it to the clipboard (cut) + - : copy + - : paste + - : paste + - : copy + - : cut + - : paste +*/ + +#include "PyConsole_Interp.h" // !!! WARNING !!! THIS INCLUDE MUST BE THE VERY FIRST !!! +#include "PyConsole_Editor.h" +#include "PyConsole_Event.h" +#include "PyInterp_Event.h" +#include "PyInterp_Dispatcher.h" +#include "PyConsole_Request.h" + +//#include +//#include +//#include +//#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static QString READY_PROMPT = ">>> "; +static QString DOTS_PROMPT = "... "; + +/*class DumpCommandsFileValidator : public SUIT_FileValidator +{ + public: + DumpCommandsFileValidator( QWidget* parent = 0 ) : SUIT_FileValidator ( parent ) {}; + virtual ~DumpCommandsFileValidator() {}; + virtual bool canSave( const QString& file, bool permissions ); +}; + +bool DumpCommandsFileValidator::canSave(const QString& file, bool permissions) +{ + QFileInfo fi( file ); + if ( !QRegExp( "[A-Za-z_][A-Za-z0-9_]*" ).exactMatch( fi.completeBaseName() ) ) { + SUIT_MessageBox::critical( parent(), + QObject::tr("WRN_WARNING"), + QObject::tr("WRN_FILE_NAME_BAD") ); + return false; + } + return SUIT_FileValidator::canSave( file, permissions); +} +*/ +void staticCallbackStdout( void* data, char* c ) +{ + if(!((PyConsole_Editor*)data)->isSuppressOutput()) + QApplication::postEvent( (PyConsole_Editor*)data, new PrintEvent( c, false ) ); +} + +void staticCallbackStderr( void* data, char* c ) +{ + if(!((PyConsole_Editor*)data)->isSuppressOutput()) + QApplication::postEvent( (PyConsole_Editor*)data, new PrintEvent( c, true ) ); +} + + +/*! + \brief Constructor. + + Creates python editor window. + \param theInterp python interper + \param theParent parent widget +*/ +PyConsole_Editor::PyConsole_Editor( PyConsole_Interp* theInterp, + QWidget* theParent ) +: QTextEdit( theParent ), + myInterp( 0 ), + myCmdInHistory( -1 ), + myEventLoop( 0 ), + myShowBanner( true ), + myIsSync( false ), + myIsSuppressOutput( false ) +{ + //QString fntSet( "" ); + QFont aFont = QFont( "Courier", 11 );//SUIT_Tools::stringToFont( fntSet ); + setFont( aFont ); + setUndoRedoEnabled( false ); + + myPrompt = READY_PROMPT; + setLineWrapMode( QTextEdit::WidgetWidth ); + setWordWrapMode( QTextOption::WrapAnywhere ); + setAcceptRichText( false ); + + theInterp->setvoutcb( staticCallbackStdout, this ); + theInterp->setverrcb( staticCallbackStderr, this ); + + // san - This is necessary for troubleless initialization + onPyInterpChanged( theInterp ); +} + +/*! + \brief Destructor. + + Does nothing for the moment. +*/ +PyConsole_Editor::~PyConsole_Editor() +{ +} + +/*! + \brief Get synchronous mode flag value. + + \sa setIsSync() + \return True if python console works in synchronous mode +*/ +bool PyConsole_Editor::isSync() const +{ + return myIsSync; +} + +/*! + \brief Set synchronous mode flag value. + + In synhronous mode the Python commands are executed in the GUI thread + and the GUI is blocked until the command is finished. In the asynchronous + mode each Python command is executed in the separate thread that does not + block the main GUI loop. + + \param on synhronous mode flag +*/ +void PyConsole_Editor::setIsSync( const bool on ) +{ + myIsSync = on; +} + +/*! + \brief Get suppress output flag value. + + \sa setIsSuppressOutput() + \return \c true if python console output is suppressed. +*/ +bool PyConsole_Editor::isSuppressOutput() const +{ + return myIsSuppressOutput; +} + +/*! + \brief Set suppress output flag value. + + In case if suppress output flag is true, the python + console output suppressed. + + \param on suppress output flag +*/ +void PyConsole_Editor::setIsSuppressOutput( const bool on ) +{ + myIsSuppressOutput = on; +} + +/*! + \brief Get 'show banner' flag value. + + \sa setIsShowBanner() + \return \c true if python console shows banner +*/ +bool PyConsole_Editor::isShowBanner() const +{ + return myShowBanner; +} + +/*! + \brief Set 'show banner' flag value. + + The banner is shown in the top of the python console window. + + \sa isShowBanner() + \param on 'show banner' flag +*/ +void PyConsole_Editor::setIsShowBanner( const bool on ) +{ + if ( myShowBanner != on ) { + myShowBanner = on; + clear(); + } +} + +/*! + \brief Get size hint for the Python console window + \return size hint value +*/ +QSize PyConsole_Editor::sizeHint() const +{ + QFontMetrics fm( font() ); + int nbLines = ( isShowBanner() ? myBanner.split("\n").count() : 0 ) + 1; + QSize s(100, fm.lineSpacing()*nbLines); + return s; +} + +/*! + \brief Put the string \a str to the python editor. + \param str string to be put in the command line of the editor + \param newBlock if True, then the string is printed on a new line + \param isError if true, the text is printed in dark red +*/ +void PyConsole_Editor::addText( const QString& str, + const bool newBlock, + const bool isError) +{ + QTextCursor theCursor(textCursor()); + QTextCharFormat cf; + + moveCursor( QTextCursor::End ); + if ( newBlock ) + theCursor.insertBlock(); + if (isError) + cf.setForeground(QBrush(Qt::red)); + else + cf.setForeground(QBrush(Qt::black)); + theCursor.insertText( str, cf); + moveCursor( QTextCursor::End ); + ensureCursorVisible(); +} + +/*! + \brief Convenient method for executing a Python command, + as if the user typed it manually. + \param command python command to be executed + + !!! WARNING: doesn't work properly with multi-line commands. !!! +*/ +void PyConsole_Editor::exec( const QString& command ) +{ + if ( isReadOnly() ) { + // some interactive command is being executed in this editor... + // shedule the command to the queue + myQueue.push_back( command ); + return; + } + // remove last line + moveCursor( QTextCursor::End ); + moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor ); + textCursor().removeSelectedText(); + // set "ready" prompt + myPrompt = READY_PROMPT; + // clear command buffer + myCommandBuffer.truncate( 0 ); + // unset history browsing mode + myCmdInHistory = -1; + // print command line by line + QString cmd = command; + if ( !cmd.endsWith( "\n" ) ) cmd += "\n"; + QStringList lines = command.split( "\n" ); + for ( int i = 0; i < lines.size(); i++ ) { + if ( !lines[i].trimmed().isEmpty() ) + myHistory.push_back( lines[i] ); + addText( ( i == 0 ? READY_PROMPT : DOTS_PROMPT ) + lines[i], i != 0 ); + } + // IPAL20182 + addText( "", true ); + // set read-only mode + setReadOnly( true ); + // set busy cursor + setCursor( Qt::BusyCursor ); + + // post a request to execute Python command; + // editor will be informed via a custom event that execution has been completed + PyInterp_Dispatcher::Get()->Exec( createRequest( cmd ) ); +} + +/*! + \brief Create request to the python dispatcher for the command execution. + + \param command python command to be executed + */ +PyInterp_Request* PyConsole_Editor::createRequest( const QString& command ) +{ + return new ExecCommand( myInterp, command, this, isSync() ); +} + +/*! + \brief Execute command in the python interpreter + and wait until it is finished. + + \param command python command to be executed + */ +void PyConsole_Editor::execAndWait( const QString& command ) +{ + // already running ? + if( myEventLoop ) + return; + + // create new event loop + myEventLoop = new QEventLoop( this ); + // execute command + exec( command ); + // run event loop + myEventLoop->exec(); + // delete event loop after command is processed + delete myEventLoop; + myEventLoop = 0; +} + +/*! + \brief Process "Enter" key press event. + + Execute the command entered by the user. +*/ +void PyConsole_Editor::handleReturn() +{ + // Position cursor at the end + QTextCursor curs(textCursor()); + curs.movePosition(QTextCursor::End); + setTextCursor(curs); + + // get last line + QTextBlock par = document()->end().previous(); + if ( !par.isValid() ) return; + + // get command + QString cmd = par.text().remove( 0, promptSize() ); + // extend the command buffer with the current command + myCommandBuffer.append( cmd ); + // add command to the history + if ( !cmd.trimmed().isEmpty() ) + myHistory.push_back( cmd ); + + // IPAL19397 + addText( "", true ); + + // set read-only mode + setReadOnly( true ); + // set busy cursor + setCursor( Qt::BusyCursor ); + + // post a request to execute Python command; + // editor will be informed via a custom event that execution has been completed + PyInterp_Dispatcher::Get()->Exec( createRequest( myCommandBuffer ) ); +} + +/*! + \brief Process drop event. + + Paste dragged text. + \param event drop event +*/ +void PyConsole_Editor::dropEvent( QDropEvent* event ) +{ + // get the initial drop position + QPoint pos = event->pos(); + QTextCursor cur = cursorForPosition( event->pos() ); + // if the position is not in the last line move it to the end of the command line + if ( cur.position() < document()->end().previous().position() + promptSize() ) { + moveCursor( QTextCursor::End ); + pos = cursorRect().center(); + } + // create new drop event and use it instead of the original + QDropEvent de( pos, + event->possibleActions(), + event->mimeData(), + event->mouseButtons(), + event->keyboardModifiers(), + event->type() ); + QTextEdit::dropEvent( &de ); + // accept the original event + event->acceptProposedAction(); +} + +/*! + \brief Process mouse button release event. + + Left mouse button: copy selection to the clipboard. + Middle mouse button: paste clipboard's contents. + \param event mouse event +*/ +void PyConsole_Editor::mouseReleaseEvent( QMouseEvent* event ) +{ + if ( event->button() == Qt::LeftButton ) { + QTextEdit::mouseReleaseEvent( event ); + //copy(); + } + else if ( event->button() == Qt::MidButton ) { + QTextCursor cur = cursorForPosition( event->pos() ); + // if the position is not in the last line move it to the end of the command line + if ( cur.position() < document()->end().previous().position() + promptSize() ) { + moveCursor( QTextCursor::End ); + } + else { + setTextCursor( cur ); + } + const QMimeData* md = QApplication::clipboard()->mimeData( QApplication::clipboard()->supportsSelection() ? + QClipboard::Selection : QClipboard::Clipboard ); + if ( md ) + insertFromMimeData( md ); + } + else { + QTextEdit::mouseReleaseEvent( event ); + } +} + +/*! + \brief Check if the string is command. + + Return True if the string \a str is likely to be the command + (i.e. it is started from the '>>>' or '...'). + \param str string to be checked +*/ +bool PyConsole_Editor::isCommand( const QString& str ) const +{ + return str.startsWith( READY_PROMPT ) || str.startsWith( DOTS_PROMPT ); +} + +/*! + \brief Handle keyboard event. + + Implement navigation, history browsing, copy/paste and other common + operations. + \param event keyboard event +*/ +void PyConsole_Editor::keyPressEvent( QKeyEvent* event ) +{ + // get cursor position + QTextCursor cur = textCursor(); + int curLine = cur.blockNumber(); + int curCol = cur.columnNumber(); + + // get last edited line + int endLine = document()->blockCount()-1; + + // get pressed key code + int aKey = event->key(); + + // check if is pressed + bool ctrlPressed = event->modifiers() & Qt::ControlModifier; + // check if is pressed + bool shftPressed = event->modifiers() & Qt::ShiftModifier; + + if ( aKey == Qt::Key_Escape || ( ctrlPressed && aKey == -1 ) ) { + // process + key-binding and key: clear current command + myCommandBuffer.truncate( 0 ); + myPrompt = READY_PROMPT; + addText( myPrompt, true ); + horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() ); + return; + } + else if ( ctrlPressed && aKey == Qt::Key_C ) { + // process + key-binding : copy + copy(); + return; + } + else if ( ctrlPressed && aKey == Qt::Key_X ) { + // process + key-binding : cut + cut(); + return; + } + else if ( ctrlPressed && aKey == Qt::Key_V ) { + // process + key-binding : paste + paste(); + return; + } + + // check for printed key + // #### aKey = ( aKey < Qt::Key_Space || aKey > Qt::Key_ydiaeresis ) ? aKey : 0; + // Better: + aKey = !(QChar(aKey).isPrint()) ? aKey : 0; + + switch ( aKey ) { + case 0 : + // any printed key: just print it + { + if ( curLine < endLine || curCol < promptSize() ) { + moveCursor( QTextCursor::End ); + } + QTextEdit::keyPressEvent( event ); + break; + } + case Qt::Key_Return: + case Qt::Key_Enter: + // key: process the current command + { + handleReturn(); + break; + } + case Qt::Key_Up: + // arrow key: process as follows: + // - without , modifiers: previous command in history + // - with modifier key pressed: move cursor one row up without selection + // - with modifier key pressed: move cursor one row up with selection + // - with + modifier keys pressed: scroll one row up + { + if ( ctrlPressed && shftPressed ) { + int value = verticalScrollBar()->value(); + int spacing = fontMetrics().lineSpacing(); + verticalScrollBar()->setValue( value > spacing ? value-spacing : 0 ); + } + else if ( shftPressed || ctrlPressed ) { + if ( curLine > 0 ) + moveCursor( QTextCursor::Up, + shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor ); + } + else { + if ( myCmdInHistory < 0 && myHistory.count() > 0 ) { + // set history browsing mode + myCmdInHistory = myHistory.count(); + // remember current command + QTextBlock par = document()->end().previous(); + myCurrentCommand = par.text().remove( 0, promptSize() ); + } + if ( myCmdInHistory > 0 ) { + myCmdInHistory--; + // get previous command in the history + QString previousCommand = myHistory.at( myCmdInHistory ); + // print previous command + moveCursor( QTextCursor::End ); + moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor ); + textCursor().removeSelectedText(); + addText( myPrompt + previousCommand ); + // move cursor to the end + moveCursor( QTextCursor::End ); + } + } + break; + } + case Qt::Key_Down: + // arrow key: process as follows: + // - without , modifiers: next command in history + // - with modifier key pressed: move cursor one row down without selection + // - with modifier key pressed: move cursor one row down with selection + // - with + modifier keys pressed: scroll one row down + { + if ( ctrlPressed && shftPressed ) { + int value = verticalScrollBar()->value(); + int maxval = verticalScrollBar()->maximum(); + int spacing = fontMetrics().lineSpacing(); + verticalScrollBar()->setValue( value+spacing < maxval ? value+spacing : maxval ); + } + else if ( shftPressed || ctrlPressed) { + if ( curLine < endLine ) + moveCursor( QTextCursor::Down, + shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor ); + } + else { + if ( myCmdInHistory >= 0 ) { + // get next command in the history + myCmdInHistory++; + QString nextCommand; + if ( myCmdInHistory < myHistory.count() ) { + // next command in history + nextCommand = myHistory.at( myCmdInHistory ); + } + else { + // end of history is reached + // last printed command + nextCommand = myCurrentCommand; + // unset history browsing mode + myCmdInHistory = -1; + } + // print next or current command + moveCursor( QTextCursor::End ); + moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor ); + textCursor().removeSelectedText(); + addText( myPrompt + nextCommand ); + // move cursor to the end + moveCursor( QTextCursor::End ); + } + } + break; + } + case Qt::Key_Left: + // arrow key: process as follows: + // - without , modifiers: move one symbol left (taking into account prompt) + // - with modifier key pressed: move one word left (taking into account prompt) + // - with modifier key pressed: move one symbol left with selection + // - with + modifier keys pressed: move one word left with selection + { + QString txt = textCursor().block().text(); + if ( !shftPressed && isCommand( txt ) && curCol <= promptSize() ) { + moveCursor( QTextCursor::Up ); + moveCursor( QTextCursor::EndOfBlock ); + } + else { + QTextEdit::keyPressEvent( event ); + } + break; + } + case Qt::Key_Right: + // arrow key: process as follows: + // - without , modifiers: move one symbol right (taking into account prompt) + // - with modifier key pressed: move one word right (taking into account prompt) + // - with modifier key pressed: move one symbol right with selection + // - with + modifier keys pressed: move one word right with selection + { + QString txt = textCursor().block().text(); + if ( !shftPressed ) { + if ( curCol < txt.length() ) { + if ( isCommand( txt ) && curCol < promptSize() ) { + cur.setPosition( cur.block().position() + promptSize() ); + setTextCursor( cur ); + break; + } + } + else { + if ( curLine < endLine && isCommand( textCursor().block().next().text() ) ) { + cur.setPosition( cur.position() + promptSize()+1 ); + setTextCursor( cur ); + horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() ); + break; + } + } + } + QTextEdit::keyPressEvent( event ); + break; + } + case Qt::Key_PageUp: + // key: process as follows: + // - without , modifiers: first command in history + // - with modifier key pressed: move cursor one page up without selection + // - with modifier key pressed: move cursor one page up with selection + // - with + modifier keys pressed: scroll one page up + { + if ( ctrlPressed && shftPressed ) { + verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub); + } + else if ( shftPressed || ctrlPressed ) { + bool moved = false; + qreal lastY = cursorRect( cur ).top(); + qreal distance = 0; + // move using movePosition to keep the cursor's x + do { + qreal y = cursorRect( cur ).top(); + distance += qAbs( y - lastY ); + lastY = y; + moved = cur.movePosition( QTextCursor::Up, + shftPressed ? QTextCursor::KeepAnchor : + QTextCursor::MoveAnchor ); + } while ( moved && distance < viewport()->height() ); + if ( moved ) { + cur.movePosition( QTextCursor::Down, + shftPressed ? QTextCursor::KeepAnchor : + QTextCursor::MoveAnchor ); + verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub ); + } + setTextCursor( cur ); + } + else { + if ( myCmdInHistory < 0 && myHistory.count() > 0 ) { + // set history browsing mode + myCmdInHistory = myHistory.count(); + // remember current command + QTextBlock par = document()->end().previous(); + myCurrentCommand = par.text().remove( 0, promptSize() ); + } + if ( myCmdInHistory > 0 ) { + myCmdInHistory = 0; + // get very first command in the history + QString firstCommand = myHistory.at( myCmdInHistory ); + // print first command + moveCursor( QTextCursor::End ); + moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor ); + textCursor().removeSelectedText(); + addText( myPrompt + firstCommand ); + // move cursor to the end + moveCursor( QTextCursor::End ); + } + } + break; + } + case Qt::Key_PageDown: + // key: process as follows: + // - without , modifiers: last command in history + // - with modifier key pressed: move cursor one page down without selection + // - with modifier key pressed: move cursor one page down with selection + // - with + modifier keys pressed: scroll one page down + { + if ( ctrlPressed && shftPressed ) { + verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd); + } + else if ( shftPressed || ctrlPressed ) { + bool moved = false; + qreal lastY = cursorRect( cur ).top(); + qreal distance = 0; + // move using movePosition to keep the cursor's x + do { + qreal y = cursorRect( cur ).top(); + distance += qAbs( y - lastY ); + lastY = y; + moved = cur.movePosition( QTextCursor::Down, + shftPressed ? QTextCursor::KeepAnchor : + QTextCursor::MoveAnchor ); + } while ( moved && distance < viewport()->height() ); + if ( moved ) { + cur.movePosition( QTextCursor::Up, + shftPressed ? QTextCursor::KeepAnchor : + QTextCursor::MoveAnchor ); + verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub ); + } + setTextCursor( cur ); + } + else { + if ( myCmdInHistory >= 0 ) { + // unset history browsing mode + myCmdInHistory = -1; + // print current command + moveCursor( QTextCursor::End ); + moveCursor( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor ); + textCursor().removeSelectedText(); + addText( myPrompt + myCurrentCommand ); + // move cursor to the end + moveCursor( QTextCursor::End ); + } + } + break; + } + case Qt::Key_Home: + // key: process as follows: + // - without , modifiers: move cursor to the beginning of the current line without selection + // - with modifier key pressed: move cursor to the very first symbol without selection + // - with modifier key pressed: move cursor to the beginning of the current line with selection + // - with + modifier keys pressed: move cursor to the very first symbol with selection + { + if ( ctrlPressed ) { + moveCursor( QTextCursor::Start, + shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor ); + } + else { + QString txt = textCursor().block().text(); + if ( isCommand( txt ) ) { + if ( shftPressed ) { + if ( curCol > promptSize() ) { + cur.movePosition( QTextCursor::StartOfLine, QTextCursor::KeepAnchor ); + cur.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, promptSize() ); + } + } + else { + cur.movePosition( QTextCursor::StartOfLine ); + cur.movePosition( QTextCursor::Right, QTextCursor::MoveAnchor, promptSize() ); + } + setTextCursor( cur ); + } + else { + moveCursor( QTextCursor::StartOfBlock, + shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor ); + } + horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() ); + } + break; + } + case Qt::Key_End: + // key: process as follows: + // - without , modifiers: move cursor to the end of the current line without selection + // - with modifier key pressed: move cursor to the very last symbol without selection + // - with modifier key pressed: move cursor to the end of the current line with selection + // - with + modifier keys pressed: move cursor to the very last symbol with selection + { + moveCursor( ctrlPressed ? QTextCursor::End : QTextCursor::EndOfBlock, + shftPressed ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor ); + break; + } + case Qt::Key_Backspace : + // key: process as follows + // - without any modifiers : delete symbol before the cursor / selection (taking into account prompt) + // - with modifier key pressed: delete previous word + // - with modifier key pressed: delete text from the cursor to the line beginning + // works only for last (command) line + { + if ( cur.hasSelection() ) { + cut(); + } + else if ( cur.position() > document()->end().previous().position() + promptSize() ) { + if ( shftPressed ) { + moveCursor( QTextCursor::PreviousWord, QTextCursor::KeepAnchor ); + textCursor().removeSelectedText(); + } + else if ( ctrlPressed ) { + cur.setPosition( document()->end().previous().position() + promptSize(), + QTextCursor::KeepAnchor ); + setTextCursor( cur ); + textCursor().removeSelectedText(); + } + else { + QTextEdit::keyPressEvent( event ); + } + } + else { + cur.setPosition( document()->end().previous().position() + promptSize() ); + setTextCursor( cur ); + horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() ); + } + break; + } + case Qt::Key_Delete : + // key: process as follows + // - without any modifiers : delete symbol after the cursor / selection (taking into account prompt) + // - with modifier key pressed: delete next word + // - with modifier key pressed: delete text from the cursor to the end of line + // works only for last (command) line + { + if ( cur.hasSelection() ) { + cut(); + } + else if ( cur.position() > document()->end().previous().position() + promptSize()-1 ) { + if ( shftPressed ) { + moveCursor( QTextCursor::NextWord, QTextCursor::KeepAnchor ); + textCursor().removeSelectedText(); + } + else if ( ctrlPressed ) { + moveCursor( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); + textCursor().removeSelectedText(); + } + else { + QTextEdit::keyPressEvent( event ); + } + } + else { + cur.setPosition( document()->end().previous().position() + promptSize() ); + setTextCursor( cur ); + horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() ); + } + break; + } + case Qt::Key_Insert : + // key: process as follows + // - with modifier key pressed: copy() + // - with modifier key pressed: paste() to the command line + { + if ( ctrlPressed ) { + copy(); + } + else if ( shftPressed ) { + paste(); + } + else + QTextEdit::keyPressEvent( event ); + break; + } + } +} + +/*! + \brief Handle notification event coming from Python dispatcher. + \param event notification event +*/ +void PyConsole_Editor::customEvent( QEvent* event ) +{ + switch( event->type() ) + { + case PrintEvent::EVENT_ID: + { + PrintEvent* pe=(PrintEvent*)event; + addText( pe->text(), false, pe->isError()); + return; + } + case PyInterp_Event::ES_OK: + case PyInterp_Event::ES_ERROR: + { + // clear command buffer + myCommandBuffer.truncate( 0 ); + // add caret return line if necessary + QTextBlock par = document()->end().previous(); + QString txt = par.text(); + txt.truncate( txt.length() - 1 ); + // IPAL19397 : addText moved to handleReturn() method + //if ( !txt.isEmpty() ) + // addText( "", true ); + // set "ready" prompt + myPrompt = READY_PROMPT; + addText( myPrompt ); + // unset busy cursor + unsetCursor(); + // stop event loop (if running) + if ( myEventLoop ) + myEventLoop->exit(); + break; + } + case PyInterp_Event::ES_INCOMPLETE: + { + // extend command buffer (multi-line command) + myCommandBuffer.append( "\n" ); + // add caret return line if necessary + QTextBlock par = document()->end().previous(); + QString txt = par.text(); + txt.truncate( txt.length() - 1 ); + // IPAL19397 : addText moved to handleReturn() method + //if ( !txt.isEmpty() ) + // addText( "", true ); + // set "dot" prompt + myPrompt = DOTS_PROMPT; + addText( myPrompt/*, true*/ ); // IPAL19397 + // unset busy cursor + unsetCursor(); + // stop event loop (if running) + if ( myEventLoop ) + myEventLoop->exit(); + break; + } + default: + QTextEdit::customEvent( event ); + } + + // unset read-only state + setReadOnly( false ); + // unset history browsing mode + myCmdInHistory = -1; + + if ( (int)event->type() == (int)PyInterp_Event::ES_OK && myQueue.count() > 0 ) + { + // process the next sheduled command from the queue (if there is any) + QString nextcmd = myQueue[0]; + myQueue.pop_front(); + exec( nextcmd ); + } +} + +/*! + \brief Handle Python interpreter change. + + Perform initialization actions if the interpreter is changed. + \param interp python interpreter is being set +*/ +void PyConsole_Editor::onPyInterpChanged( PyConsole_Interp* interp ) +{ + if ( myInterp != interp + // Force read-only state and wait cursor when myInterp is NULL + || !myInterp ) { + myInterp = interp; + if ( myInterp ) { + // print banner + myBanner = myInterp->getbanner().c_str(); + if ( isShowBanner() ) + addText( myBanner ); + // clear command buffer + myCommandBuffer.truncate(0); + // unset read-only state + setReadOnly( false ); + // unset history browsing mode + myCmdInHistory = -1; + // add prompt + addText( myPrompt ); + // unset busy cursor + viewport()->unsetCursor(); + // stop event loop (if running) + if( myEventLoop) + myEventLoop->exit(); + } + else { + // clear contents + clear(); + // set read-only state + setReadOnly( true ); + // set busy cursor + setCursor( Qt::WaitCursor ); + } + } +} + +/*! + \brief "Copy" operation. + + Reimplemented from Qt. + Warning! In Qt4 this method is not virtual. + */ +void PyConsole_Editor::cut() +{ + QTextCursor cur = textCursor(); + if ( cur.hasSelection() ) { + QApplication::clipboard()->setText( cur.selectedText() ); + int startSelection = cur.selectionStart(); + if ( startSelection < document()->end().previous().position() + promptSize() ) + startSelection = document()->end().previous().position() + promptSize(); + int endSelection = cur.selectionEnd(); + if ( endSelection < document()->end().previous().position() + promptSize() ) + endSelection = document()->end().previous().position() + promptSize(); + cur.setPosition( startSelection ); + cur.setPosition( endSelection, QTextCursor::KeepAnchor ); + horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() ); + setTextCursor( cur ); + textCursor().removeSelectedText(); + } +} + +/*! + \brief "Paste" operation. + + Reimplemented from Qt. + Warning! In Qt4 this method is not virtual. + */ +void PyConsole_Editor::paste() +{ + QTextCursor cur = textCursor(); + if ( cur.hasSelection() ) { + int startSelection = cur.selectionStart(); + if ( startSelection < document()->end().previous().position() + promptSize() ) + startSelection = document()->end().previous().position() + promptSize(); + int endSelection = cur.selectionEnd(); + if ( endSelection < document()->end().previous().position() + promptSize() ) + endSelection = document()->end().previous().position() + promptSize(); + cur.setPosition( startSelection ); + cur.setPosition( endSelection, QTextCursor::KeepAnchor ); + horizontalScrollBar()->setValue( horizontalScrollBar()->minimum() ); + setTextCursor( cur ); + textCursor().removeSelectedText(); + } + if ( textCursor().position() < document()->end().previous().position() + promptSize() ) + moveCursor( QTextCursor::End ); + QTextEdit::paste(); +} + +/*! + \brief "Clear" operation. + + Reimplemented from Qt. + Warning! In Qt4 this method is not virtual. + */ +void PyConsole_Editor::clear() +{ + QTextEdit::clear(); + if ( isShowBanner() ) + addText( myBanner ); + myPrompt = READY_PROMPT; + addText( myPrompt ); +} + +/*! + \brief "Dump commands" operation. + */ +/*void PyConsole_Editor::dump() +{ + QStringList aFilters; + aFilters.append( tr( "PYTHON_FILES_FILTER" ) ); + + QString fileName = SUIT_FileDlg::getFileName( this, QString(), + aFilters, tr( "TOT_DUMP_PYCOMMANDS" ), + false, true, new DumpCommandsFileValidator( this ) ); + if ( fileName != "" ) { + QFile file( fileName ); + if ( !file.open( QFile::WriteOnly ) ) + return; + + QTextStream out (&file); + + for( int i=0; i + +class PyConsole_Interp; +class PyInterp_Request; +class QEventLoop; + +class PYCONSOLE_EXPORT PyConsole_Editor : public QTextEdit +{ + Q_OBJECT; + +public: + PyConsole_Editor( PyConsole_Interp* theInterp, QWidget *theParent = 0 ); + ~PyConsole_Editor(); + + virtual void addText( const QString& str, const bool newBlock = false, const bool isError = false ); + bool isCommand( const QString& str ) const; + + virtual void exec( const QString& command ); + void execAndWait( const QString& command ); + + bool isSync() const; + void setIsSync( const bool ); + + bool isSuppressOutput() const; + void setIsSuppressOutput(const bool); + + bool isShowBanner() const; + void setIsShowBanner( const bool ); + + virtual QSize sizeHint() const; + +public slots: + void cut(); + void paste(); + void clear(); + void handleReturn(); + void onPyInterpChanged( PyConsole_Interp* ); + //.void dump(); + +protected: + virtual void dropEvent( QDropEvent* event ); + virtual void mouseReleaseEvent( QMouseEvent* event ); + virtual void keyPressEvent ( QKeyEvent* event); + virtual void customEvent( QEvent* event); + + virtual PyInterp_Request* createRequest( const QString& ); + + /** Convenience function */ + inline int promptSize() const { return myPrompt.size(); } + + PyConsole_Interp* myInterp; //!< python interpreter + + QString myCommandBuffer; //!< python command buffer + QString myCurrentCommand; //!< currently being printed command + QString myPrompt; //!< current command line prompt + int myCmdInHistory; //!< current history command index + QStringList myHistory; //!< commands history buffer + QEventLoop* myEventLoop; //!< internal event loop + QString myBanner; //!< current banner + bool myShowBanner; //!< 'show banner' flag + QStringList myQueue; //!< python commands queue + bool myIsSync; //!< synchronous mode flag + bool myIsSuppressOutput; //!< suppress output flag +}; + +#endif // PYCONSOLE_EDITOR_H diff --git a/src/PyConsole/PyConsole_EnhEditor.cxx b/src/PyConsole/PyConsole_EnhEditor.cxx new file mode 100644 index 000000000..2dd8207ab --- /dev/null +++ b/src/PyConsole/PyConsole_EnhEditor.cxx @@ -0,0 +1,464 @@ + + +#include "PyConsole.h" +#include + +#include +#include +#include +#include +#include +#include + +#include "PyConsole_EnhEditor.h" +#include "PyConsole_EnhInterp.h" +#include "PyConsole_Request.h" +#include "PyInterp_Dispatcher.h" + +// Initialize list of valid separators +static const char * tmp_a[] = {" ", "(", "[","+", "-", "*", "/", ";", "^", "="}; +const std::vector PyConsole_EnhEditor::SEPARATORS = \ + std::vector(tmp_a, tmp_a + sizeof(tmp_a)/sizeof(tmp_a[0])); + +/** + * Constructor. + * @param interp the interpreter linked to the editor + * @param parent parent widget + */ +PyConsole_EnhEditor::PyConsole_EnhEditor(PyConsole_EnhInterp * interp, QWidget * parent) : + PyConsole_Editor(interp, parent), + _tab_mode(false), + _cursor_pos(-1), + _multi_line_paste(false), + _multi_line_content() +{ + document()->setUndoRedoEnabled(true); +} + +/** + * Overrides. Catches the TAB and Ctrl+TAB combinations. + * @param event + */ +void PyConsole_EnhEditor::keyPressEvent ( QKeyEvent* event) +{ + // check if is pressed + bool ctrlPressed = event->modifiers() & Qt::ControlModifier; + // check if is pressed + bool shftPressed = event->modifiers() & Qt::ShiftModifier; + + if (event->key() == Qt::Key_Tab && !shftPressed) + { + if (!ctrlPressed) + handleTab(); + else + { + clearCompletion(); + handleBackTab(); + } + PyConsole_Editor::keyPressEvent(event); + } + else + { + // If ctrl is not pressed (and sth else is pressed with it), + // or if ctrl is not pressed alone + if (!ctrlPressed || (ctrlPressed && event->key() != Qt::Key_Control)) + { + clearCompletion(); + _cursor_pos = -1; + } + // Discard ctrl pressed alone: + if (event->key() != Qt::Key_Control) + PyConsole_Editor::keyPressEvent(event); + } +} + +/** + * Whenever the mouse is clicked, clear the completion. + * @param e + */ +void PyConsole_EnhEditor::mousePressEvent(QMouseEvent* e) +{ + clearCompletion(); + _cursor_pos = -1; + PyConsole_Editor::mousePressEvent(e); +} + +/** + * Clear in the editor the block of text displayed after having hit . + */ +void PyConsole_EnhEditor::clearCompletion() +{ + // Delete completion text if present + if (_tab_mode) + { + // Remove completion display + document()->undo(); + // Remove trailing line return: + QTextCursor tc(textCursor()); + tc.setPosition(document()->characterCount()-1); + setTextCursor(tc); + textCursor().deletePreviousChar(); + // TODO: before wait for any TAB event to be completed + static_cast(myInterp)->clearCompletion(); + } + _tab_mode = false; +} + +/** + * Handle the sequence of events after having hit + */ +void PyConsole_EnhEditor::handleTab() +{ + if (_tab_mode) + { + // Already tab mode - nothing to do ! + return; + } + + QTextCursor cursor(textCursor()); + + // Cursor at end of input + cursor.movePosition(QTextCursor::End); + setTextCursor(cursor); + + // Save cursor position if needed + if (_cursor_pos == -1) + _cursor_pos = textCursor().position(); + + // get last line + QTextBlock par = document()->end().previous(); + if ( !par.isValid() ) return; + + // Switch to completion mode + _tab_mode = true; + + QString cmd = par.text().mid(promptSize()); + + // Post completion request + // Editor will be informed via a custom event that completion has been run + PyInterp_Request* req = createTabRequest(cmd); + PyInterp_Dispatcher::Get()->Exec(req); +} + +/** + * Handles what happens after hitting Ctrl-TAB + */ +void PyConsole_EnhEditor::handleBackTab() +{ + QTextCursor cursor(textCursor()); + + if (_cursor_pos == -1) + { + // Invalid cursor position - we can't do anything: + return; + } + // Ensure cursor is at the end of command line + cursor.setPosition(_cursor_pos); + cursor.movePosition(QTextCursor::EndOfLine); + //setCursor(cursor); + + // Delete last completed text + int i = cursor.position() - _cursor_pos; + cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, i); + cursor.removeSelectedText(); + _cursor_pos = -1; +} + +/** + * Create the Python requested that will be posted to the interpreter to + * get the completions. + * @param input line typed by the user at the time TAB was hit + * @return a CompletionCommand + * @sa CompletionCommand + */ +PyInterp_Request* PyConsole_EnhEditor::createTabRequest( const QString& input ) +{ + // Parse input to extract on what part the dir() has to be executed + QString input2(input); + + // Split up to the last syntaxical separator + int lastSp = -1; + for (std::vector::const_iterator i = SEPARATORS.begin(); i != SEPARATORS.end(); i++) + { + int j = input2.lastIndexOf(*i); + if (j > lastSp) + lastSp = j; + } + if (lastSp >= 0) + input2 = input.mid(lastSp+1); + + // Detect a qualified name (with a point) + int lastPt = input2.lastIndexOf(QString(".")); + + // Split the 2 surrounding parts of the qualified name + if (lastPt != -1) + { + _compl_before_point = input2.left(lastPt); + _compl_after_point = input2.mid(lastPt+1); + } + else + { + // No point found - do a global matching - + // (the following will call dir() with an empty string) + _compl_after_point = input2; + _compl_before_point = QString(""); + } + + return new CompletionCommand( static_cast(myInterp), _compl_before_point, + _compl_after_point, this, isSync() ); +} + +/** + * Format completion results - this is where we should create 3 columns etc ... + * @param matches list of possible completions + * @param result return value + */ +void PyConsole_EnhEditor::formatCompletion(const std::vector & matches, QString & result) const +{ + int sz = matches.size(); + + if (sz > MAX_COMPLETIONS) + { + sz = MAX_COMPLETIONS; + result.append("[Too many matches! Displaying first ones only ...]\n"); + } + + for (int i = 0; i < sz; ++i) + { + result.append(matches[i]); + result.append("\n"); + } +} + +/** + * Override. Catches the events generated by the enhanced interpreter after the execution + * of a completion request. + * @param event + */ +void PyConsole_EnhEditor::customEvent( QEvent* event ) +{ + std::vector matches; + QString first_match, comple_text, doc, base; + QTextCursor cursor(textCursor()); + QTextBlockFormat bf; + QTextCharFormat cf; + PyConsole_EnhInterp * interp = static_cast(myInterp); + int cursorPos; + + switch( event->type() ) + { + case PyInterp_Event::ES_TAB_COMPLETE_OK: + // Extract corresponding matches from the interpreter + matches = interp->getLastMatches(); + + if (matches.size() == 0) + { + // Completion successful but nothing returned. + _tab_mode = false; + _cursor_pos = -1; + return; + } + + // Only one match - complete directly and update doc string window + doc = interp->getDocStr(); + if (matches.size() == 1) + { + first_match = matches[0].mid(_compl_after_point.size()); + cursor.insertText(first_match); + _tab_mode = false; + if (doc == QString("")) + emit updateDoc(formatDocHTML("(no documentation available)\n")); + else + emit updateDoc(formatDocHTML(doc)); + } + else + { + // Detect if there is a common base to all available completion + // In this case append this base to the text already + extractCommon(matches, base); + first_match = base.mid(_compl_after_point.size()); + cursor.insertText(first_match); + + // If this happens to match exaclty the first completion + // also provide doc + if (base == matches[0]) + { + doc = formatDocHTML(doc); + emit updateDoc(doc); + } + + // Print all matching completion in a "undo-able" block + cursorPos = cursor.position(); + cursor.insertBlock(); + cursor.beginEditBlock(); + + // Insert all matches + QTextCharFormat cf; + cf.setForeground(QBrush(Qt::darkGreen)); + cursor.setCharFormat(cf); + formatCompletion(matches, comple_text); + cursor.insertText(comple_text); + cursor.endEditBlock(); + + // Position cursor where it was before inserting the completion list: + cursor.setPosition(cursorPos); + setTextCursor(cursor); + } + break; + case PyInterp_Event::ES_TAB_COMPLETE_ERR: + // Tab completion was unsuccessful, switch off mode: + _tab_mode = false; + _cursor_pos = -1; + break; + case PyInterp_Event::ES_OK: + case PyInterp_Event::ES_ERROR: + case PyInterp_Event::ES_INCOMPLETE: + // Before everything else, call super() + PyConsole_Editor::customEvent(event); + // If we are in multi_paste_mode, process the next item: + multiLineProcessNextLine(); + break; + default: + PyConsole_Editor::customEvent( event ); + break; + } +} + +/** + * Extract the common leading part of all strings in matches. + * @param matches + * @param result + */ +void PyConsole_EnhEditor::extractCommon(const std::vector & matches, QString & result) const +{ + result = ""; + int charIdx = 0; + + if (matches.size() < 2) + return; + + while (true) + { + if (charIdx >= matches[0].size()) + return; + QChar ch = matches[0][charIdx]; + for (int j = 1; j < matches.size(); j++) + if (charIdx >= matches[j].size() || matches[j][charIdx] != ch) + return; + result += ch; + charIdx++; + } +} + +/** + * Format the doc string in HTML format with the first line in bold blue + * @param doc initial doc string + * @return HTML string + */ +QString PyConsole_EnhEditor::formatDocHTML(const QString & doc) const +{ + QString templ = QString("\n ") + + QString(" ") + + QString(" ") + + QString("\n") + + QString("

") + + QString("%1

") + + QString("

%2

") + + QString(""); + + QString fst, rest(""); + + // Extract first line of doc + int idx = doc.indexOf("\n"); + if (idx > 0) + { + fst = doc.left(idx); + rest = doc.mid(idx+1); + } + else + { + fst = doc; + } + + fst = fst.replace("\n", " "); + rest = rest.replace("\n", " "); + return templ.arg(fst).arg(rest); +} + +/** + * Handle properly multi-line pasting. Qt4 doc recommends overriding this function. + * If the pasted text doesn't contain a line return, no special treatment is done. + * @param source + */ +void PyConsole_EnhEditor::insertFromMimeData(const QMimeData * source) +{ + if (_multi_line_paste) + return; + + if (source->hasText()) + { + QString s = source->text(); + if (s.contains("\n")) + multilinePaste(s); + else + PyConsole_Editor::insertFromMimeData(source); + } + else + { + PyConsole_Editor::insertFromMimeData(source); + } +} + + +void PyConsole_EnhEditor::multilinePaste(const QString & s) +{ + // Turn on multi line pasting mode + _multi_line_paste = true; + + // Split the string: + QString s2 = s; + s2.replace("\r", ""); // Windows string format converted to Unix style + + QStringList lst = s2.split(QChar('\n'), QString::KeepEmptyParts); + + // Perform the proper paste operation for the first line to handle the case where + // sth was already there: + QMimeData source; + source.setText(lst[0]); + PyConsole_Editor::insertFromMimeData(&source); + + // Prepare what will have to be executed after the first line: + _multi_line_content = std::queue(); + for (int i = 1; i < lst.size(); ++i) + _multi_line_content.push(lst[i]); + + // Trigger the execution of the first (mixed) line + handleReturn(); + + // See customEvent() and multiLineProcessNext() for the rest of the handling. +} + +/** + * Process the next line in the queue of a multiple copy/paste: + */ +void PyConsole_EnhEditor::multiLineProcessNextLine() +{ + if (!_multi_line_paste) + return; + + QString line(_multi_line_content.front()); + _multi_line_content.pop(); + if (!_multi_line_content.size()) + { + // last line in the queue, just paste it + addText(line, false, false); + _multi_line_paste = false; + } + else + { + // paste the line and simulate a key stroke + addText(line, false, false); + handleReturn(); + } +} diff --git a/src/PyConsole/PyConsole_EnhEditor.h b/src/PyConsole/PyConsole_EnhEditor.h new file mode 100644 index 000000000..03b5e19a1 --- /dev/null +++ b/src/PyConsole/PyConsole_EnhEditor.h @@ -0,0 +1,78 @@ + + +#ifndef PYCONSOLE_ENHEDITOR_H_ +#define PYCONSOLE_ENHEDITOR_H_ + +#include "PyConsole.h" + +#include "PyConsole_Editor.h" +#include +#include + +class PyConsole_EnhInterp; + +/** + * Enhanced Python editor handling tab completion. + */ +class PYCONSOLE_EXPORT PyConsole_EnhEditor: public PyConsole_Editor +{ + Q_OBJECT; + +public: + PyConsole_EnhEditor(PyConsole_EnhInterp * interp, QWidget * parent=0); + virtual ~PyConsole_EnhEditor() {} + +signals: + /** + * Signal emitted by the editor widget when the doc string should be updated. + * @param doc a HTML block with the formatted doc string. + * TODO: for now this signal is left uncaught. + */ + void updateDoc(QString doc); + +protected: + /** List of separators identifying the last parsable token for completion */ + static const std::vector SEPARATORS; + + /** Maximum number of completions shown at once */ + static const int MAX_COMPLETIONS = 70; + + /** Are we in completion mode */ + bool _tab_mode; + + /** String on which the dir() comamnd is executed */ + QString _compl_before_point; + /** String on which the results of the dir() are matched */ + QString _compl_after_point; + + /** Cursor position when is hit */ + int _cursor_pos; + + /** Are we currently pasting several lines */ + bool _multi_line_paste; + + /** Queue of lines being pasted */ + std::queue _multi_line_content; + + // Overrides: + virtual void keyPressEvent ( QKeyEvent* event); + virtual void customEvent( QEvent* event); + virtual void mousePressEvent( QMouseEvent* event ); + virtual void insertFromMimeData(const QMimeData * source); + + virtual PyInterp_Request* createTabRequest( const QString& input ); + virtual void handleTab(); + virtual void handleBackTab(); + virtual void clearCompletion(); + virtual void formatCompletion(const std::vector & matches, QString & result) const; + virtual QString formatDocHTML(const QString & doc) const; + + virtual void multilinePaste(const QString & s); + virtual void multiLineProcessNextLine(); + +private: + void extractCommon(const std::vector & matches, QString & result) const; + +}; + +#endif /* PYCONSOLE_ENHEDITOR_H_ */ diff --git a/src/PyConsole/PyConsole_EnhInterp.cxx b/src/PyConsole/PyConsole_EnhInterp.cxx new file mode 100644 index 000000000..4332f3112 --- /dev/null +++ b/src/PyConsole/PyConsole_EnhInterp.cxx @@ -0,0 +1,135 @@ + + + +#include "PyConsole.h" + +#include "PyConsole_EnhInterp.h" + +#include +#include +#include + +static const char * tmp_k[] = {"and", "as", "assert", "break", "class", + "continue", "def", "del", + "elif", "else", "except", "exec", "finally", "for", "from", "global", "if", + "import", "in", "is", "lambda", "not", "or", "pass", "print", "raise", + "return", "try", "while", "with", "yield"}; + +const std::vector PyConsole_EnhInterp::PYTHON_KEYWORDS = \ + std::vector(tmp_k, tmp_k+sizeof(tmp_k)/sizeof(tmp_k[0])); + +/*! + \brief Run Python dir() command and saves the result internally in _lastPy + \param dirArgument Python expression to pass to the dir command. The parsing of what the + user actually started typing is dedicated to the caller + \param startMatch string representing the begining of the patter to be completed. For example when + the user types "a_string_variable.rsp ", this is "rsp". + \return command exit status - 0 = success +*/ +int PyConsole_EnhInterp::runDirCommand(const QString & dirArgument, const QString & startMatch) +{ + int ret; + std::vector v; + + clearCompletion(); + if ( (ret = runDirAndExtract(dirArgument, startMatch, _last_matches)) ) + return ret; + + // If dirArgument is empty, we append the __builtins__ + if (dirArgument.isEmpty()) + { + if ( (ret = runDirAndExtract(QString("__builtins__"), startMatch, _last_matches, false)) ) + return ret; + + // ... and we match on Python's keywords as well: + for (std::vector::const_iterator it = PYTHON_KEYWORDS.begin(); it != PYTHON_KEYWORDS.end(); it++) + if ((*it).startsWith(startMatch)) + _last_matches.push_back(*it); + } + + // Try to get doc string of the first match + if (_last_matches.size() > 0) + { + QString cmd(""); + if (dirArgument.trimmed() != "") + cmd = dirArgument + "."; + cmd += _last_matches[0] + ".__doc__"; + PyObject * str = PyRun_String(cmd.toStdString().c_str(), Py_eval_input, _g, _g); + if (!str || str == Py_None || !PyString_Check(str)) + { + if (!str) + PyErr_Clear(); + _doc_str = ""; + } + else + _doc_str = QString(PyString_AsString(str)); + Py_XDECREF(str); + } + + // The command has been successfully executed + return 0; +} + +/** + * See runDirCommand(). + * @param dirArgument see runDirCommand() + * @param startMatch see runDirCommand() + * @param[out] result the list of matches + * @param discardSwig if true a regular expression is used to discard all static method generated + * by SWIG. typically: MEDCouplingUMesh_Blabla + * @return -1 if the call to dir() or the parsing of the result failed, 0 otherwise. + */ +int PyConsole_EnhInterp::runDirAndExtract(const QString& dirArgument, + const QString & startMatch, std::vector & result, + bool discardSwig) const +{ + QRegExp re("^[A-Z].+_[A-Z]+[a-z]+.+$"); // discard SWIG static method, e.g. MEDCouplingUMesh_Blabla + QString command("dir(" + dirArgument + ")"); + PyObject * plst = PyRun_String(command.toStdString().c_str(), Py_eval_input, _g, _g); + if(!plst || plst == Py_None) { + if(!plst) + PyErr_Clear(); + + Py_XDECREF(plst); + return -1; + } + + // Extract the returned list and convert it to a vector<> + if (!PySequence_Check(plst)) + { + // Should never happen ... + //std::cerr << "not a list!" << std::endl; + Py_XDECREF(plst); + return -1; + } + + // Convert plst to a vector of QString + int n = PySequence_Length(plst); + for (int i = 0; i < n; i++) + { + PyObject * it; + it = PySequence_GetItem(plst, i); + QString s(PyString_AsString(it)); + // if the method is not from swig, not static (guessed from the reg exp) and matches + // what is already there + if (s.startsWith(startMatch)) + if(!discardSwig || (!re.exactMatch(s) && !s.contains("swig"))) + result.push_back(s); + Py_DECREF(it); + } + Py_DECREF(plst); + + return 0; +} + +/** + * Clear internal members containing the last completion results. + */ +void PyConsole_EnhInterp::clearCompletion() +{ + _last_matches.resize(0); + _doc_str = QString(""); +} + + + diff --git a/src/PyConsole/PyConsole_EnhInterp.h b/src/PyConsole/PyConsole_EnhInterp.h new file mode 100644 index 000000000..1af977cc0 --- /dev/null +++ b/src/PyConsole/PyConsole_EnhInterp.h @@ -0,0 +1,47 @@ + + +#ifndef PYCONSOLE_ENHINTERP_H_ +#define PYCONSOLE_ENHINTERP_H_ + +#include "PyConsole.h" + +#include +#include "PyConsole_Interp.h" + +#include +#include + +/** + * Enhanced Python interpreter used for auto-completion. + * This extends PyConsole_Interp with an API wrapping the Python dir() command nicely. + */ +class PYCONSOLE_EXPORT PyConsole_EnhInterp: public PyConsole_Interp +{ +public: + PyConsole_EnhInterp() + : PyConsole_Interp(), _last_matches(0), _doc_str("") + {} + + virtual ~PyConsole_EnhInterp() {} + + const std::vector& getLastMatches() const { return _last_matches; } + const QString & getDocStr() const { return _doc_str; } + + virtual int runDirCommand(const QString& dirArgument, const QString& startMatch); + virtual void clearCompletion(); + +protected: + /** Hard coded list of Python keywords */ + static const std::vector PYTHON_KEYWORDS; + + /** Last computed matches */ + std::vector _last_matches; + /** Doc string of the first match - when only one match it will be displayed by the Editor*/ + QString _doc_str; + + virtual int runDirAndExtract(const QString& dirArgument, const QString & startMatch, + std::vector & result, bool discardSwig=true) const; + +}; + +#endif /* PYCONSOLE_ENHINTERP_H_ */ diff --git a/src/PyConsole/PyConsole_Event.cxx b/src/PyConsole/PyConsole_Event.cxx new file mode 100644 index 000000000..a3e2b05ae --- /dev/null +++ b/src/PyConsole/PyConsole_Event.cxx @@ -0,0 +1,3 @@ + + +#include "PyConsole_Event.h" diff --git a/src/PyConsole/PyConsole_Event.h b/src/PyConsole/PyConsole_Event.h new file mode 100644 index 000000000..84771b85c --- /dev/null +++ b/src/PyConsole/PyConsole_Event.h @@ -0,0 +1,48 @@ + + +#ifndef PYCONSOLE_EVENT_H +#define PYCONSOLE_EVENT_H + +#include "PyConsole.h" + +#include +#include + +/*! + \class PrintEvent + \brief Python command output backend event. + \internal +*/ +class PrintEvent : public QEvent +{ +public: + static const int EVENT_ID = 65432; + + /*! + \brief Constructor + \param c message text (python trace) + \param isError default to false - if true indicates that an error is being printed. + */ + PrintEvent( const char* c, bool isError = false) : + QEvent( (QEvent::Type)EVENT_ID ), myText( c ), errorFlag(isError) + {} + + /*! + \brief Get message + \return message text (python trace) + */ + QString text() const { return myText; } + + /** + * @return true if this is an error message + */ + bool isError() const { return errorFlag; } + +protected: + QString myText; //!< Event message (python trace) + + /** Set to true if an error msg is to be displayed */ + bool errorFlag; +}; + +#endif // PYCONSOLE_EVENT_H diff --git a/src/PyConsole/PyConsole_Interp.cxx b/src/PyConsole/PyConsole_Interp.cxx new file mode 100644 index 000000000..21af783d1 --- /dev/null +++ b/src/PyConsole/PyConsole_Interp.cxx @@ -0,0 +1,118 @@ + +#include "PyConsole_Interp.h" + +/*! + \class PyConsole_Interp + \brief Python interpreter to be embedded to the SALOME study's GUI. + + Python interpreter is created one per SALOME study. + + Call initialize method defined in the base class PyInterp_Interp, + to intialize interpreter after instance creation. + + The method initialize() calls virtuals methods + - initPython() to initialize global Python interpreter + - initState() to initialize embedded interpreter state + - initContext() to initialize interpreter internal context + - initRun() to prepare interpreter for running commands + + /EDF-CCAR/ + When SALOME uses multi Python interpreter feature, + every study has its own interpreter and thread state (_tstate = Py_NewInterpreter()). + This is fine because every study has its own modules (sys.modules) stdout and stderr. + + But some Python modules must be imported only once. In multi interpreter + context Python modules (*.py) are imported several times. + For example, the PyQt module must be imported only once because + it registers classes in a C module. + + It's quite the same with omniorb modules (internals and generated with omniidl). + + This problem is handled with "shared modules" defined in salome_shared_modules.py. + These "shared modules" are imported only once and only copied in all + the other interpreters. + + But it's not the only problem. Every interpreter has its own + __builtin__ module. That's fine but if we have copied some modules + and imported others problems may arise with operations that are not allowed + in restricted execution environment. So we must impose that all interpreters + have identical __builtin__ module. +*/ + +/*! + \brief Constructor. + + Creates new python interpreter. +*/ +PyConsole_Interp::PyConsole_Interp(): PyInterp_Interp() +{ +} + +/*! + \brief Destructor. + + Does nothing for the moment. +*/ +PyConsole_Interp::~PyConsole_Interp() +{ +} + +/*! + \brief Initialize internal Python interpreter state. + + When calling initState the GIL is not held + It must not be held on exit + + \return \c true on success +*/ +bool PyConsole_Interp::initState() +{ + PyEval_AcquireLock(); + _tstate = Py_NewInterpreter(); // create an interpreter and save current state + PySys_SetArgv(PyInterp_Interp::_argc,PyInterp_Interp::_argv); // initialize sys.argv + + if(!builtinmodule) // PAL18041: deepcopy function don't work in Salome + { + //builtinmodule is static member of PyInterp class + //If it is not NULL (initialized to the builtin module of the main interpreter + //all the sub interpreters will have the same builtin + //_interp is a static member and is the main interpreter + //The first time we initialized it to the builtin of main interpreter + builtinmodule=PyDict_GetItemString(_interp->modules, "__builtin__"); + } + + //If builtinmodule has been initialized all the sub interpreters + // will have the same __builtin__ module + if(builtinmodule){ + PyObject *m = PyImport_GetModuleDict(); + PyDict_SetItemString(m, "__builtin__", builtinmodule); + _tstate->interp->builtins = PyModule_GetDict(builtinmodule); + Py_INCREF(_tstate->interp->builtins); + } + PyEval_ReleaseThread(_tstate); + return true; +} + +/*! + \brief Initialize python interpeter context. + + The GIL is assumed to be held. + It is the caller responsability to acquire the GIL. + It must still be held on initContext() exit. + + \return \c true on success +*/ +bool PyConsole_Interp::initContext() +{ + PyObject *m = PyImport_AddModule("__main__"); // interpreter main module (module context) + if(!m){ + PyErr_Print(); + return false; + } + _g = PyModule_GetDict(m); // get interpreter dictionnary context + + if(builtinmodule){ + PyDict_SetItemString(_g, "__builtins__", builtinmodule); // assign singleton __builtin__ module + } + return true; +} diff --git a/src/PyConsole/PyConsole_Interp.h b/src/PyConsole/PyConsole_Interp.h new file mode 100644 index 000000000..d65c9e620 --- /dev/null +++ b/src/PyConsole/PyConsole_Interp.h @@ -0,0 +1,20 @@ + +#ifndef PYCONSOLE_INTERP_H +#define PYCONSOLE_INTERP_H + +#include "PyConsole.h" + +#include /// !!! WARNING !!! THIS INCLUDE MUST BE VERY FIRST !!! + +class PYCONSOLE_EXPORT PyConsole_Interp : public PyInterp_Interp +{ +public: + PyConsole_Interp(); + ~PyConsole_Interp(); + +protected: + virtual bool initState(); + virtual bool initContext(); +}; + +#endif // PYCONSOLE_INTERP_H diff --git a/src/PyConsole/PyConsole_Request.cxx b/src/PyConsole/PyConsole_Request.cxx new file mode 100644 index 000000000..3e31b90eb --- /dev/null +++ b/src/PyConsole/PyConsole_Request.cxx @@ -0,0 +1,99 @@ + + +#include "PyConsole_Request.h" + +#include "PyInterp_Event.h" +#include "PyConsole_Event.h" +#include "PyConsole_EnhInterp.h" +#include "PyConsole_EnhEditor.h" + +#include + +/** + * Constructor. + * @param theInterp interpreter that will execute the command + * @param theCommand command text + * @param theListener editor object that will receive the response events after execution + * of the request + * @param sync + */ +ExecCommand::ExecCommand( PyInterp_Interp* theInterp, + const QString& theCommand, + PyConsole_Editor* theListener, + bool sync ) + : PyInterp_LockRequest( theInterp, theListener, sync ), + myCommand( theCommand ), myState( PyInterp_Event::ES_OK ) + {} + +/** + * Execute the command by calling the run() method of the embedded interpreter. + */ +void ExecCommand::execute() +{ + if ( myCommand != "" ) + { + int ret = getInterp()->run( myCommand.toUtf8().data() ); + if ( ret < 0 ) + myState = PyInterp_Event::ES_ERROR; + else if ( ret > 0 ) + myState = PyInterp_Event::ES_INCOMPLETE; + } +} + +/** + * Create the event indicating the status of the request execution. + * @return a QEvent + */ +QEvent* ExecCommand::createEvent() +{ + if ( IsSync() ) + QCoreApplication::sendPostedEvents( listener(), PrintEvent::EVENT_ID ); + return new PyInterp_Event( myState, this ); +} + + +/*! + Constructor. + Creates a new python completion request. + \param theInterp python interpreter + \param input string containing the dir() command to be executed + \param startMatch part to be matched with the results of the dir() command + \param theListener widget to get the notification messages + \param sync if True the request is processed synchronously +*/ +CompletionCommand::CompletionCommand( PyConsole_EnhInterp* theInterp, + const QString& input, + const QString& startMatch, + PyConsole_EnhEditor* theListener, + bool sync) + : PyInterp_LockRequest( theInterp, theListener, sync ), + _tabSuccess(false), _dirArg(input), _startMatch(startMatch) +{} + +/** + * Execute the completion command by wrapping the runDirCommand() of the + * embedded enhanced interpreter. + */ +void CompletionCommand::execute() +{ + PyConsole_EnhInterp * interp = static_cast(getInterp()); + int ret = interp->runDirCommand( _dirArg, _startMatch); + if (ret == 0) + _tabSuccess = true; + else + _tabSuccess = false; +} + +/** + * Create the event indicating the return value of the completion command. + * @return + */ +QEvent* CompletionCommand::createEvent() +{ + int typ = _tabSuccess ? PyInterp_Event::ES_TAB_COMPLETE_OK : PyInterp_Event::ES_TAB_COMPLETE_ERR; + + return new PyInterp_Event( typ, this); +} + + + diff --git a/src/PyConsole/PyConsole_Request.h b/src/PyConsole/PyConsole_Request.h new file mode 100644 index 000000000..da5f94a79 --- /dev/null +++ b/src/PyConsole/PyConsole_Request.h @@ -0,0 +1,86 @@ + + + +#ifndef PYCONSOLE_REQUEST_H_ +#define PYCONSOLE_REQUEST_H_ + +#include "PyConsole.h" +#include "PyInterp_Request.h" + +#include +#include +#include + +class PyInterp_Interp; +class PyConsole_Editor; + +/*! + \class ExecCommand + \brief Python command execution request. + \internal +*/ +class ExecCommand : public PyInterp_LockRequest +{ +public: + /*! + \brief Constructor. + + Creates new python command execution request. + \param theInterp python interpreter + \param theCommand python command + \param theListener widget to get the notification messages + \param sync if True the request is processed synchronously + */ + ExecCommand( PyInterp_Interp* theInterp, + const QString& theCommand, + PyConsole_Editor* theListener, + bool sync = false ); + +protected: + /*! + \brief Execute the python command in the interpreter and + get its execution status. + */ + virtual void execute(); + + /*! + \brief Create and return a notification event. + \return new notification event + */ + virtual QEvent* createEvent(); + +private: + QString myCommand; //!< Python command + int myState; //!< Python command execution status +}; + +class PyConsole_EnhInterp; +class PyConsole_EnhEditor; + +class CompletionCommand : public PyInterp_LockRequest +{ +public: + CompletionCommand( PyConsole_EnhInterp* theInterp, + const QString& input, + const QString& startMatch, + PyConsole_EnhEditor* theListener, + bool sync = false ); + + +protected: + /** List of separators identifying the last parsable token for completion */ + static const std::vector SEPARATORS; + + /** String to be passed to the dir() command */ + QString _dirArg; + /** Begining of the command (as typed by the user) */ + QString _startMatch; + /** was the completion command successful */ + bool _tabSuccess; + + virtual void execute(); + virtual QEvent* createEvent(); + +}; + +#endif /* PYCONSOLE_REQUEST_H_ */ diff --git a/src/PyConsole/resources/PyConsole_msg_en.ts b/src/PyConsole/resources/PyConsole_msg_en.ts new file mode 100644 index 000000000..85393a2bf --- /dev/null +++ b/src/PyConsole/resources/PyConsole_msg_en.ts @@ -0,0 +1,42 @@ + + + + + PyConsole_Console + + + EDIT_COPY_CMD + &Copy + + + + EDIT_PASTE_CMD + &Paste + + + + EDIT_CLEAR_CMD + Clea&r + + + + EDIT_SELECTALL_CMD + Select &All + + + EDIT_DUMPCOMMANDS_CMD + D&ump commands + + + + PyConsole_Editor + + TOT_DUMP_PYCOMMANDS + Dump commands + + + PYTHON_FILES_FILTER + PYTHON Files (*.py) + + + diff --git a/src/PyConsole/resources/PyConsole_msg_fr.ts b/src/PyConsole/resources/PyConsole_msg_fr.ts new file mode 100644 index 000000000..910911562 --- /dev/null +++ b/src/PyConsole/resources/PyConsole_msg_fr.ts @@ -0,0 +1,42 @@ + + + + + PyConsole_Console + + + EDIT_COPY_CMD + &Copier + + + + EDIT_PASTE_CMD + C&oller + + + + EDIT_CLEAR_CMD + &Effacer + + + + EDIT_SELECTALL_CMD + &Tout sélectionner + + + EDIT_DUMPCOMMANDS_CMD + &Générer le script des commandes + + + + PyConsole_Editor + + TOT_DUMP_PYCOMMANDS + &Générer le script des commandes + + + PYTHON_FILES_FILTER + Fichiers PYTHON (*.py) + + + diff --git a/src/PyConsole/resources/PyConsole_msg_ja.ts b/src/PyConsole/resources/PyConsole_msg_ja.ts new file mode 100644 index 000000000..c8d0fb17d --- /dev/null +++ b/src/PyConsole/resources/PyConsole_msg_ja.ts @@ -0,0 +1,42 @@ + + + + + PyConsole_Console + + + EDIT_COPY_CMD + コピー(&C) + + + + EDIT_PASTE_CMD + 貼り付け(&P) + + + + EDIT_CLEAR_CMD + 削除(&r) + + + + EDIT_SELECTALL_CMD + すべて選択します。(&A) + + + EDIT_DUMPCOMMANDS_CMD + スクリプト コマンドを生成します。(&u) + + + + PyConsole_Editor + + TOT_DUMP_PYCOMMANDS + スクリプト コマンドを生成します。 + + + PYTHON_FILES_FILTER + ファイル (*.py) PYTHON + + + diff --git a/src/PyEvent/CMakeLists.txt b/src/PyEvent/CMakeLists.txt new file mode 100644 index 000000000..393b79774 --- /dev/null +++ b/src/PyEvent/CMakeLists.txt @@ -0,0 +1,24 @@ + +SET(PROJECT_HEADERS + PyEvent.h + PyEvent_Event.h + PyEvent_EventFilter.h +) + +SET(PROJECT_SOURCES + PyEvent_Event.cxx + PyEvent_EventFilter.cxx +) + +#INCLUDE_DIRECTORIES(${Qt5Widgets_INCLUDES}) + +ADD_DEFINITIONS(-DPYEVENT_EXPORTS) + +ADD_LIBRARY(PyEvent STATIC + ${PROJECT_SOURCES} + ${PROJECT_HEADERS} +) + +TARGET_LINK_LIBRARIES(PyEvent ${Qt5Widgets_LIBRARIES}) + +#INSTALL(TARGETS PyEvent DESTINATION bin) diff --git a/src/PyEvent/PyEvent.h b/src/PyEvent/PyEvent.h new file mode 100644 index 000000000..9c2f34fc3 --- /dev/null +++ b/src/PyEvent/PyEvent.h @@ -0,0 +1,19 @@ + +#if !defined ( PYEVENT_H ) +#define PYEVENT_H + +#ifdef WIN32 +# if defined PYEVENT_EXPORTS || defined PyEvent_EXPORTS +# define PYEVENT_EXPORT __declspec(dllexport) +# else +# define PYEVENT_EXPORT __declspec(dllimport) +# endif +#else //WIN32 +# define PYEVENT_EXPORT +#endif //WIN32 + +#if defined WIN32 +#pragma warning ( disable: 4251 ) +#endif + +#endif // PYEVENT_H diff --git a/src/PyEvent/PyEvent_Event.cxx b/src/PyEvent/PyEvent_Event.cxx new file mode 100644 index 000000000..fef847f5c --- /dev/null +++ b/src/PyEvent/PyEvent_Event.cxx @@ -0,0 +1,287 @@ + +#include "PyEvent_Event.h" + +#include +#include + +// asv 21.02.05 : introducing multi-platform approach of thread comparison +// - on Unix using pthread_t type for storing ThreadId +// - on Win32 using integer type for storing ThreadId +// NOT using integer ThreadId on both Unix and Win32 because (from documentation): +// "...Do not allow your program to rely on the internal structure or size of the pthread_t..." + +#ifdef WIN32 +#include +static DWORD myThread; +#else +#include +static pthread_t myThread; +#endif + +/*! + \class InitEvent + \brief Helper event class responsible for initializing PyEvent_Event + mechanism by the main thread ID + */ +class InitEvent : public PyEvent_Event +{ +public: + InitEvent(); + virtual ~InitEvent(); + virtual void Execute(); +}; + +/*! + \brief Constructor, initializes the event mechanism by the current thread ID. + It is asssumed to be the main thread ID, so be careful! +*/ +InitEvent::InitEvent() +{ + GetSessionThread(); +} + +/*! + \brief Destructor, does nothing. +*/ +InitEvent::~InitEvent() +{ +} + +/*! + \brief Nothing to be executed for this kind of event. +*/ +void InitEvent::Execute() +{ +} + +// NOTE: Here the SALOME event mechanism is initalized by the +// current thread ID that is always assumed to be the main thread ID. +// This should be revised as soon as the application library is no longer +// linked against the Event library (i.e. this static object is not created or created +// outside the main thread). +static InitEvent myInitEvent; + +/*! + \class PyEvent_CustomEvent + \brief Generic event class for user-defined events + + This class contains a generic void* data member that may be used + for transferring event-specific data to the receiver. + + \warning The internal data is not destroyed by the class destructor. +*/ + +/*! + \brief Constructor. + \param type event type +*/ +PyEvent_CustomEvent::PyEvent_CustomEvent( int type ) +: QEvent( (QEvent::Type)type ), d( 0 ) +{ +} + +/*! + \brief Constructor. + \param type event type + \param data custom data +*/ +PyEvent_CustomEvent::PyEvent_CustomEvent( QEvent::Type type, void* data ) +: QEvent( type ), d( data ) +{ +} + +/*! + \brief Get custom data. + \return pointer to the internal data +*/ +void* PyEvent_CustomEvent::data() const +{ + return d; +} + +/*! + \brief Set custom data. + \param data pointer to the internal data +*/ +void PyEvent_CustomEvent::setData( void* data ) +{ + d = data; +} + +/*! + \class PyEvent_Event + \brief The class which encapsulates data and functionality required for + posting component-specific events to perform arbitrary operations + in the main GUI thread. + + PyEvent_Event objects can be posted by any thread belonging to the GUI process. + + It is necessary to derive a custom event class from PyEvent_Event and + re-implement virtual Execute() method. This method should actually perform + the desirable operation. To pass all the required data to Execute() and + store a return value, arbitrary data fields can be added to the custom + event class. There is no need to protect such fields with a mutex, for only + one thread working with a PyEvent_Event object is active at any moment. + + Usage: + - Create PyEvent_Event. Components can derive their own event class from + PyEvent_Event in order to pass custom data to the event handler. + - Call process() method to post the event. After process() execution + it is possible to examine fields of your custom event object. + - Perform delete operator on the event to wake up the desktop (you can also + set parameter to \c true to automatically wake up desktop after + process(). + + The method processed() is used by the desktop to signal that event processing + has been completed. + + To make all this work, it is necessary to call static method GetSessionThread() + during the application initialization, i.e. from main() function. + It is important to call this method from the primary application thread. + + Caveats: + - there are no. +*/ + +//! Total number of semaphore resources +const int NumberOfResources = 2; + +/*! + \brief Initialize event mechanism. + + This function sets up the main application thread. It should be called + during the application initialization, i.e. main() function. +*/ +void PyEvent_Event::GetSessionThread(){ +#ifdef WIN32 + myThread = ::GetCurrentThreadId(); +#else + myThread = pthread_self(); +#endif +} + +/*! + \brief Check if the processing is in the main application thread. + \return \c true if this method is called from the main application thread +*/ +bool PyEvent_Event::IsSessionThread(){ + bool aResult = false; +#ifdef WIN32 + aResult = myThread == ::GetCurrentThreadId(); +#else + aResult = myThread == pthread_self(); +#endif + return aResult; +} + +/*! + \brief Constructor. +*/ +PyEvent_Event::PyEvent_Event(){ + // Prepare the semaphore + mySemaphore = new QSemaphore( NumberOfResources ); + mySemaphore->acquire( NumberOfResources ); +} + +/*! + \brief Destructor. +*/ +PyEvent_Event::~PyEvent_Event(){ + if ( mySemaphore->available() < NumberOfResources ) + mySemaphore->release( NumberOfResources - mySemaphore->available() ); + delete mySemaphore; +} + +/*! + \brief This method should be called by the main GUI thread + in order to execute the code specific for this event and finally + to inform the calling thread that the event + has been processed waking it up with help of the semaphore . + */ +void PyEvent_Event::ExecutePostedEvent() +{ + // Diagnose incorrect usage of PyEvent_Event API + if ( !IsSessionThread() ){ + qWarning( "PyEvent_Event::ExecutePostedEvent() is called from a secondary thread that might mean an error in application logic!" ); + } + // Actual execution specific for particular kind of event + Execute(); + // Signal the calling thread that the event has been processed + processed(); +} + +/*! + \brief Post the event and wait for its completion. + process() should be called from a secondary thread only. + \sa processed() +*/ +void PyEvent_Event::process() +{ + // Diagnose incorrect usage of PyEvent_Event API + if ( IsSessionThread() ){ + qWarning( "PyEvent_Event::process() is called from the main GUI thread that might mean an error in application logic!" ); + } + + QApplication::postEvent( qApp, new PyEvent_CustomEvent( PyEvent_EVENT, (void*)this ) ); + mySemaphore->acquire( 1 ); +} + +/*! + \brief Use this method to signal that this event has been processed. +*/ +void PyEvent_Event::processed() +{ + mySemaphore->release( 1 ); +} + +/*! + \fn virtual void PyEvent_Event::Execute(); + \brief This method should be redefined in the successor classes + to do real work. +*/ + +/*! + \class TMemFunEvent + \brief Template class for event which calls the function + without arguments and returning result. +*/ + +/*! + \class TVoidMemFunEvent + \brief Template class for event which calls the function + without arguments and without return value. +*/ + +/*! + \class TMemFun1ArgEvent + \brief Template class for event which calls the function + with one argument and returning result. +*/ + +/*! + \class TVoidMemFun1ArgEvent + \brief Template class for event which calls the function + with one argument and without return value. +*/ + +/*! + \class TMemFun2ArgEvent + \brief Template class for event which calls the function + with two arguments and returning result. +*/ + +/*! + \class TVoidMemFun2ArgEvent + \brief Template class for event which calls the function + with two arguments and without return value. +*/ + +/*! + \fn ProcessEvent + \brief Template function for processing events with return value. +*/ + +/*! + \fn ProcessVoidEvent + \brief Template function for processing events without return value. +*/ diff --git a/src/PyEvent/PyEvent_Event.h b/src/PyEvent/PyEvent_Event.h new file mode 100644 index 000000000..ed1ea7b74 --- /dev/null +++ b/src/PyEvent/PyEvent_Event.h @@ -0,0 +1,204 @@ + +#ifndef PyEvent_PYEVENT_H +#define PyEvent_PYEVENT_H + +#include "PyEvent.h" + +#include + +//! SALOME custom event type +#define PyEvent_EVENT QEvent::Type( QEvent::User + 10000 ) + +class PYEVENT_EXPORT PyEvent_CustomEvent : public QEvent +{ +public: + PyEvent_CustomEvent( int type ); + PyEvent_CustomEvent( QEvent::Type type, void* data ); + + void* data() const; + void setData( void* data ); + +private: + void *d; //!< internal data +}; + +class QSemaphore; + +class PYEVENT_EXPORT PyEvent_Event +{ +public: + PyEvent_Event(); + virtual ~PyEvent_Event(); + + void ExecutePostedEvent(); + virtual void Execute() = 0; + + static bool IsSessionThread(); + void process(); + +protected: + void processed(); + static void GetSessionThread(); + +private: + QSemaphore* mySemaphore; //!< internal semaphore +}; + +template class TMemFunEvent : public PyEvent_Event +{ +public: + typedef TRes TResult; + TResult myResult; + typedef TResult (TObject::* TAction)(); + TMemFunEvent(TObject* theObject, TAction theAction, + TResult theResult = TResult()): + myObject(theObject), + myAction(theAction), + myResult(theResult) + {} + virtual void Execute() + { + myResult = (myObject->*myAction)(); + } +private: + TObject* myObject; + TAction myAction; +}; + +template class TVoidMemFunEvent : public PyEvent_Event +{ +public: + typedef void (TObject::* TAction)(); + TVoidMemFunEvent(TObject* theObject, TAction theAction): + myObject(theObject), + myAction(theAction) + {} + virtual void Execute() + { + (myObject->*myAction)(); + } +private: + TObject* myObject; + TAction myAction; +}; + +template +class TMemFun1ArgEvent : public PyEvent_Event +{ +public: + typedef TRes TResult; + TResult myResult; + typedef TResult (TObject::* TAction)(TArg); + TMemFun1ArgEvent(TObject* theObject, TAction theAction, TArg theArg, + TResult theResult = TResult()): + myObject(theObject), + myAction(theAction), + myResult(theResult), + myArg(theArg) + {} + virtual void Execute() + { + myResult = (myObject->*myAction)(myArg); + } +private: + TObject* myObject; + TAction myAction; + TStoreArg myArg; +}; + +template +class TVoidMemFun1ArgEvent : public PyEvent_Event +{ +public: + typedef void (TObject::* TAction)(TArg); + TVoidMemFun1ArgEvent(TObject* theObject, TAction theAction, TArg theArg): + myObject(theObject), + myAction(theAction), + myArg(theArg) + {} + virtual void Execute() + { + (myObject->*myAction)(myArg); + } +private: + TObject* myObject; + TAction myAction; + TStoreArg myArg; +}; + +template +class TMemFun2ArgEvent: public PyEvent_Event +{ +public: + typedef TRes TResult; + TResult myResult; + typedef TResult (TObject::* TAction)(TArg,TArg1); + TMemFun2ArgEvent(TObject* theObject, TAction theAction, + TArg theArg, TArg1 theArg1, + TResult theResult = TResult()): + myObject(theObject), + myAction(theAction), + myResult(theResult), + myArg(theArg), + myArg1(theArg1) + {} + virtual void Execute() + { + myResult = (myObject->*myAction)(myArg,myArg1); + } +private: + TObject* myObject; + TAction myAction; + TStoreArg myArg; + TStoreArg1 myArg1; +}; + +template +class TVoidMemFun2ArgEvent : public PyEvent_Event +{ +public: + typedef void (TObject::* TAction)(TArg,TArg1); + TVoidMemFun2ArgEvent(TObject* theObject, TAction theAction, TArg theArg, TArg1 theArg1): + myObject(theObject), + myAction(theAction), + myArg(theArg), + myArg1(theArg1) + {} + virtual void Execute() + { + (myObject->*myAction)(myArg,myArg1); + } +private: + TObject* myObject; + TAction myAction; + TStoreArg myArg; + TStoreArg1 myArg1; +}; + +template inline typename TEvent::TResult ProcessEvent(TEvent* theEvent) +{ + typename TEvent::TResult aResult; + if(PyEvent_Event::IsSessionThread()) { + theEvent->Execute(); + aResult = theEvent->myResult; + } + else { + theEvent->process(); + aResult = theEvent->myResult; + } + delete theEvent; + return aResult; +} + +inline void ProcessVoidEvent(PyEvent_Event* theEvent) +{ + if(PyEvent_Event::IsSessionThread()) { + theEvent->Execute(); + } + else { + theEvent->process(); + } + delete theEvent; +} + +#endif // PyEvent_PYEVENT_H diff --git a/src/PyEvent/PyEvent_EventFilter.cxx b/src/PyEvent/PyEvent_EventFilter.cxx new file mode 100644 index 000000000..ac12e2013 --- /dev/null +++ b/src/PyEvent/PyEvent_EventFilter.cxx @@ -0,0 +1,60 @@ + +#include "PyEvent_EventFilter.h" +#include "PyEvent_Event.h" + +#include + +PyEvent_EventFilter* PyEvent_EventFilter::myFilter = NULL; + +/*!Constructor.*/ +PyEvent_EventFilter::PyEvent_EventFilter() +: QObject() +{ + /* VSR 13/01/03 : installing global event filter for the application */ + qApp->installEventFilter( this ); +} + +/*!Destructor.*/ +PyEvent_EventFilter::~PyEvent_EventFilter() +{ + qApp->removeEventFilter( this ); +} + +/*! + Custom event filter +*/ +bool PyEvent_EventFilter::eventFilter( QObject* o, QEvent* e ) +{ + if ( e->type() == PyEvent_EVENT ) + { + PyEvent_Event* aSE = (PyEvent_Event*)((PyEvent_CustomEvent*)e)->data(); + processEvent(aSE); + ((PyEvent_CustomEvent*)e)->setData( 0 ); + return true; + } + return QObject::eventFilter( o, e ); +} + +/*!Process event.*/ +void PyEvent_EventFilter::processEvent( PyEvent_Event* theEvent ) +{ + if(theEvent) + theEvent->ExecutePostedEvent(); +} + +/*!Create new instance of PyEvent_EventFilter*/ +void PyEvent_EventFilter::Init() +{ + if( myFilter==NULL ) + myFilter = new PyEvent_EventFilter(); +} + +/*!Destroy filter.*/ +void PyEvent_EventFilter::Destroy() +{ + if( myFilter ) + { + delete myFilter; + myFilter = NULL; + } +} diff --git a/src/PyEvent/PyEvent_EventFilter.h b/src/PyEvent/PyEvent_EventFilter.h new file mode 100644 index 000000000..23132de7e --- /dev/null +++ b/src/PyEvent/PyEvent_EventFilter.h @@ -0,0 +1,43 @@ + +#ifndef PyEvent_EVENTFILTER_H +#define PyEvent_EVENTFILTER_H + +#include "PyEvent.h" +#include + +#if defined WIN32 +#pragma warning( disable: 4251 ) +#endif + +class PyEvent_Event; + +/*! + Event filter class for QApplication object that handles custom events posted by PyEvent_Event objects. + It assumes that such custom events are alwys posted, not sent. + This event filter can be installed by any application that intends to use PyEvent_Event mechanism asynchronously. + This class replaced SalomeApp_EventFilter. +*/ +class PYEVENT_EXPORT PyEvent_EventFilter: public QObject +{ +public: + static void Init(); + static void Destroy(); + +protected: + PyEvent_EventFilter(); + virtual ~PyEvent_EventFilter(); + +private: + /*! global event filter for qapplication */ + virtual bool eventFilter( QObject* o, QEvent* e ); + void processEvent( PyEvent_Event* ); + +private: + static PyEvent_EventFilter* myFilter; +}; + +#if defined WIN32 +#pragma warning( default: 4251 ) +#endif + +#endif diff --git a/src/PyInterp/CMakeLists.txt b/src/PyInterp/CMakeLists.txt new file mode 100644 index 000000000..09585ebb8 --- /dev/null +++ b/src/PyInterp/CMakeLists.txt @@ -0,0 +1,46 @@ + +SET(CMAKE_AUTOMOC ON) + +# header files +SET(PROJECT_HEADERS + PyInterp.h + PyInterp_Dispatcher.h + PyInterp_Event.h + PyInterp_Interp.h + PyInterp_Request.h + PyInterp_Watcher.h +) + +SET(PROJECT_AUTOMOC + ${CMAKE_CURRENT_BINARY_DIR}/PyInterp_automoc.cpp +) + +# sources / static +SET(PROJECT_SOURCES + PyInterp_Dispatcher.cxx + PyInterp_Event.cxx + PyInterp_Interp.cxx + PyInterp_Request.cxx +) + +SET(PROJECT_LIBRARIES + PyEvent + ${Qt5Core_LIBRARIES} +) + +SOURCE_GROUP ("Generated Files" FILES ${PROJECT_AUTOMOC}) + +ADD_DEFINITIONS(-DPYINTERP_EXPORTS -DHAVE_DEBUG_PYTHON) + +INCLUDE_DIRECTORIES( + ${PROJECT_SOURCE_DIR}/src/PyEvent +) + +ADD_LIBRARY(PyInterp STATIC + ${PROJECT_SOURCES} + ${PROJECT_HEADERS} +) + +TARGET_LINK_LIBRARIES(PyInterp ${PROJECT_LIBRARIES}) + +#INSTALL(TARGETS PyInterp DESTINATION bin) diff --git a/src/PyInterp/PyInterp.h b/src/PyInterp/PyInterp.h new file mode 100644 index 000000000..4977fce11 --- /dev/null +++ b/src/PyInterp/PyInterp.h @@ -0,0 +1,40 @@ + +#if !defined ( PYINTERP_H ) +#define PYINTERP_H + +// ======================================================== +// set dllexport type for Win platform +#ifdef WIN32 +# if defined PYINTERP_EXPORTS || defined PyInterp_EXPORTS +# define PYINTERP_EXPORT __declspec(dllexport) +# else +# define PYINTERP_EXPORT __declspec(dllimport) +# endif +#else // WIN32 +# define PYINTERP_EXPORT +#endif // WIN32 + +// ======================================================== +// little trick - if we do not have debug python libraries +#ifdef _DEBUG + #ifndef HAVE_DEBUG_PYTHON + #undef _DEBUG + #endif +#endif + +#include + +#ifdef _DEBUG + #ifndef HAVE_DEBUG_PYTHON + #define _DEBUG + #endif +#endif + +// ======================================================== +// avoid warning messages +#ifdef WIN32 +#pragma warning (disable : 4786) +#pragma warning (disable : 4251) +#endif + +#endif // PYINTERP_H diff --git a/src/PyInterp/PyInterp_Dispatcher.cxx b/src/PyInterp/PyInterp_Dispatcher.cxx new file mode 100644 index 000000000..1999e716f --- /dev/null +++ b/src/PyInterp/PyInterp_Dispatcher.cxx @@ -0,0 +1,199 @@ + +#include "PyInterp_Dispatcher.h" // !!! WARNING !!! THIS INCLUDE MUST BE THE VERY FIRST !!! +#include "PyInterp_Interp.h" +#include "PyInterp_Watcher.h" +#include "PyInterp_Request.h" + +#include +#include + +PyInterp_Dispatcher* PyInterp_Dispatcher::myInstance = 0; + +void PyInterp_Request::process() +{ + safeExecute(); + + bool isSync = IsSync(); + + if ( !isSync ) + myMutex.lock(); + + if ( listener() ) + processEvent( listener() ); + + if ( !isSync ) + myMutex.unlock(); +} + +void PyInterp_Request::safeExecute() +{ + //ProcessVoidEvent( new PyInterp_ExecuteEvent( this ) ); + execute(); +} + +void PyInterp_Request::Destroy( PyInterp_Request* request ) +{ + // Lock and unlock the mutex to avoid errors on its deletion + request->myMutex.lock(); + request->myMutex.unlock(); + delete request; +} + +QEvent* PyInterp_Request::createEvent() +{ + return new PyInterp_Event( PyInterp_Event::ES_NOTIFY, this ); +} + +void PyInterp_Request::processEvent( QObject* o ) +{ + if ( !o ) + return; + + QEvent* e = createEvent(); + if ( !e ) + return; + + if ( !IsSync() ) + QCoreApplication::postEvent( o, e ); + else + { + QCoreApplication::sendEvent( o, e ); + delete e; + } +} + +void PyInterp_Request::setListener( QObject* o ) +{ + myMutex.lock(); + myListener = o; + myMutex.unlock(); +} + +void PyInterp_LockRequest::safeExecute() +{ + if ( getInterp() ){ + PyLockWrapper aLock = getInterp()->GetLockWrapper(); + //ProcessVoidEvent( new PyInterp_ExecuteEvent( this ) ); + execute(); + } +} + +PyInterp_Event::~PyInterp_Event() +{ + PyInterp_Request::Destroy( myRequest ); + myRequest = 0; +} + +PyInterp_Dispatcher* PyInterp_Dispatcher::Get() +{ + if ( !myInstance ) + myInstance = new PyInterp_Dispatcher(); + return myInstance; +} + +PyInterp_Dispatcher::PyInterp_Dispatcher() +: QThread() +{ + myWatcher = new PyInterp_Watcher(); +} + +PyInterp_Dispatcher::~PyInterp_Dispatcher() +{ + // Clear the request queue + myQueueMutex.lock(); + + QListIterator it( myQueue ); + while ( it.hasNext() ) + PyInterp_Request::Destroy( it.next() ); + myQueue.clear(); + + myQueueMutex.unlock(); + + // Wait for run() to finish + wait(); + + delete myWatcher; + myWatcher = 0; +} + +bool PyInterp_Dispatcher::IsBusy() const +{ + return isRunning(); +} + +void PyInterp_Dispatcher::Exec( PyInterp_Request* theRequest ) +{ + if ( !theRequest ) + return; + + //if ( theRequest->IsSync() && !IsBusy() ) // synchronous processing - nothing is done if dispatcher is busy! + if ( theRequest->IsSync() ) // synchronous processing + processRequest( theRequest ); + else // asynchronous processing + { + myQueueMutex.lock(); + myQueue.enqueue( theRequest ); + if ( theRequest->listener() ) + QObject::connect( theRequest->listener(), SIGNAL( destroyed( QObject* ) ), myWatcher, SLOT( onDestroyed( QObject* ) ) ); + myQueueMutex.unlock(); + + if ( !IsBusy() ) + start(); + } +} + +void PyInterp_Dispatcher::run() +{ +// MESSAGE("*** PyInterp_Dispatcher::run(): STARTED") + PyInterp_Request* aRequest; + + // prepare for queue size check + myQueueMutex.lock(); + + while( myQueue.size() ) { +// MESSAGE("*** PyInterp_Dispatcher::run(): next request taken from the queue") + aRequest = myQueue.head(); + + // let other threads append their requests to the end of the queue + myQueueMutex.unlock(); + + // processRequest() may delete a request, so this pointer must not be used + // after request is processed! + processRequest( aRequest ); + + // prepare for removal of the first request in the queue + myQueueMutex.lock(); + // IMPORTANT: the first item could have been removed by objectDestroyed() --> we have to check it + if ( myQueue.head() == aRequest ) // It's still here --> remove it + myQueue.dequeue(); + +// MESSAGE("*** PyInterp_Dispatcher::run(): request processed") + } + + myQueueMutex.unlock(); +// MESSAGE("*** PyInterp_Dispatcher::run(): FINISHED") +} + +void PyInterp_Dispatcher::processRequest( PyInterp_Request* theRequest ) +{ + theRequest->process(); +} + +void PyInterp_Dispatcher::objectDestroyed( const QObject* o ) +{ + // prepare for modification of the queue + myQueueMutex.lock(); + + QMutableListIterator it( myQueue ); + while ( it.hasNext() ) + { + RequestPtr r = it.next(); + if ( o == r->listener() ) + { + r->setListener( 0 ); // to prevent event posting + it.remove(); + } + } + + myQueueMutex.unlock(); +} diff --git a/src/PyInterp/PyInterp_Dispatcher.h b/src/PyInterp/PyInterp_Dispatcher.h new file mode 100644 index 000000000..fa98d30b0 --- /dev/null +++ b/src/PyInterp/PyInterp_Dispatcher.h @@ -0,0 +1,46 @@ + +#ifndef PYINTERP_DISPATCHER_H +#define PYINTERP_DISPATCHER_H + +#include "PyInterp.h" // !!! WARNING !!! THIS INCLUDE MUST BE THE VERY FIRST !!! + +#include "PyInterp_Request.h" // full include instead of forward declaration + // everyone inc'ing the Dispatcher will get the requests for free. + +#include +#include +#include + +class QObject; +class PyInterp_Watcher; + +class PYINTERP_EXPORT PyInterp_Dispatcher : protected QThread +{ + PyInterp_Dispatcher(); // private constructor + +public: + static PyInterp_Dispatcher* Get(); + + virtual ~PyInterp_Dispatcher(); + + bool IsBusy() const; + void Exec( PyInterp_Request* ); + +private: + virtual void run(); + void processRequest( PyInterp_Request* ); + void objectDestroyed( const QObject* ); + +private: + typedef PyInterp_Request* RequestPtr; + + QQueue myQueue; + QMutex myQueueMutex; + PyInterp_Watcher* myWatcher; + + static PyInterp_Dispatcher* myInstance; + + friend class PyInterp_Watcher; +}; + +#endif // PYINTERP_DISPATCHER_H diff --git a/src/PyInterp/PyInterp_Event.cxx b/src/PyInterp/PyInterp_Event.cxx new file mode 100644 index 000000000..524d06a08 --- /dev/null +++ b/src/PyInterp/PyInterp_Event.cxx @@ -0,0 +1,9 @@ + + +#include "PyInterp_Event.h" +#include "PyInterp_Request.h" + +void PyInterp_ExecuteEvent::Execute() +{ + myRequest->execute(); +} diff --git a/src/PyInterp/PyInterp_Event.h b/src/PyInterp/PyInterp_Event.h new file mode 100644 index 000000000..01bab92c1 --- /dev/null +++ b/src/PyInterp/PyInterp_Event.h @@ -0,0 +1,52 @@ + + +#ifndef PYINTERP_EVENT_H +#define PYINTERP_EVENT_H + +#include "PyInterp.h" + +#include + +#include + +class PyInterp_Request; + +class PyInterp_ExecuteEvent: public PyEvent_Event +{ +public: + PyInterp_ExecuteEvent( PyInterp_Request* r ) + : myRequest( r ) {} + + virtual void Execute(); + +protected: + PyInterp_Request* myRequest; +}; + +/** + * Events thrown by the interpreter having executed a command and indicating + * the return status. + */ +class PYINTERP_EXPORT PyInterp_Event : public QEvent +{ + PyInterp_Event(); + PyInterp_Event( const PyInterp_Event& ); + +public: + //Execution state + enum { ES_NOTIFY = QEvent::User + 5000, ES_OK, ES_ERROR, ES_INCOMPLETE, + ES_TAB_COMPLETE_OK, ES_TAB_COMPLETE_ERR, ES_LAST }; + + PyInterp_Event( int type, PyInterp_Request* request ) + : QEvent( (QEvent::Type)type ), myRequest( request ) {} + + virtual ~PyInterp_Event(); + + PyInterp_Request* GetRequest() const { return myRequest; } + operator PyInterp_Request*() const { return myRequest; } + +private: + PyInterp_Request* myRequest; +}; + +#endif // PYINTERP_EVENT_H diff --git a/src/PyInterp/PyInterp_Interp.cxx b/src/PyInterp/PyInterp_Interp.cxx new file mode 100644 index 000000000..7e633b59f --- /dev/null +++ b/src/PyInterp/PyInterp_Interp.cxx @@ -0,0 +1,533 @@ + +#include "PyInterp_Interp.h" // !!! WARNING !!! THIS INCLUDE MUST BE THE VERY FIRST !!! +#include + +#include +#include + +#include +#include +#include +#include +#include + +#define TOP_HISTORY_PY "--- top of history ---" +#define BEGIN_HISTORY_PY "--- begin of history ---" + +// a map to store python thread states that have been created for a given system thread (key=thread id,value=thread state) +std::map currentThreadMap; + +/*! + \class PyLockWrapper + \brief Python GIL wrapper. +*/ + +/*! + \brief Constructor. Automatically acquires GIL. + \param theThreadState python thread state +*/ +PyLockWrapper::PyLockWrapper(PyThreadState* theThreadState): + myThreadState(theThreadState), + mySaveThreadState(0) +{ + if (myThreadState->interp == PyInterp_Interp::_interp) + _savestate = PyGILState_Ensure(); + else + PyEval_AcquireThread(myThreadState); +} + +/*! + \brief Destructor. Automatically releases GIL. +*/ +PyLockWrapper::~PyLockWrapper() +{ + if (myThreadState->interp == PyInterp_Interp::_interp) + PyGILState_Release(_savestate); + else + PyEval_ReleaseThread(myThreadState); +} + +/*! + \brief Get Python GIL wrapper. + \return GIL lock wrapper (GIL is automatically acquired here) +*/ +PyLockWrapper PyInterp_Interp::GetLockWrapper() +{ + if (_tstate->interp == PyInterp_Interp::_interp) + return _tstate; + + // If we are here, we have a secondary python interpreter. Try to get a thread state synchronized with the system thread + long currentThreadid=PyThread_get_thread_ident(); // the system thread id + PyThreadState* theThreadState; + if(currentThreadMap.count(currentThreadid) != 0) + { + //a thread state exists for this thread id + PyThreadState* oldThreadState=currentThreadMap[currentThreadid]; + if(_tstate->interp ==oldThreadState->interp) + { + //The old thread state has the same python interpreter as this one : reuse the threadstate + theThreadState=oldThreadState; + } + else + { + //The old thread state has not the same python interpreter as this one : delete the old threadstate and create a new one + PyEval_AcquireLock(); + PyThreadState_Clear(oldThreadState); + PyThreadState_Delete(oldThreadState); + PyEval_ReleaseLock(); + theThreadState=PyThreadState_New(_tstate->interp); + currentThreadMap[currentThreadid]=theThreadState; + } + } + else + { + // no old thread state for this thread id : create a new one + theThreadState=PyThreadState_New(_tstate->interp); + currentThreadMap[currentThreadid]=theThreadState; + } + return theThreadState; +} + +/* + The following functions are used to hook the Python + interpreter output. +*/ + +static void +PyStdOut_dealloc(PyStdOut *self) +{ + PyObject_Del(self); +} + +static PyObject* +PyStdOut_write(PyStdOut *self, PyObject *args) +{ + char *c; + int l; + if (!PyArg_ParseTuple(args, "t#:write",&c, &l)) + return NULL; + if(self->_cb==NULL) { + if ( self->_iscerr ) + std::cerr << c ; + else + std::cout << c ; + } + else { + self->_cb(self->_data,c); + } + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* +PyStdOut_flush(PyStdOut *self) +{ + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef PyStdOut_methods[] = { + {"write", (PyCFunction)PyStdOut_write, METH_VARARGS, PyDoc_STR("write(string) -> None")}, + {"flush", (PyCFunction)PyStdOut_flush, METH_NOARGS, PyDoc_STR("flush() -> None")}, + {NULL, NULL} /* sentinel */ +}; + +static PyMemberDef PyStdOut_memberlist[] = { + {(char*)"softspace", T_INT, offsetof(PyStdOut, softspace), 0, + (char*)"flag indicating that a space needs to be printed; used by print"}, + {NULL} /* Sentinel */ +}; + +static PyTypeObject PyStdOut_Type = { + /* The ob_type field must be initialized in the module init function + * to be portable to Windows without using C++. */ + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "PyOut", /*tp_name*/ + sizeof(PyStdOut), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)PyStdOut_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + PyObject_GenericGetAttr, /*tp_getattro*/ + /* softspace is writable: we must supply tp_setattro */ + PyObject_GenericSetAttr, /* tp_setattro */ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + PyStdOut_methods, /*tp_methods*/ + PyStdOut_memberlist, /*tp_members*/ + 0, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + 0, /*tp_new*/ + 0, /*tp_free*/ + 0, /*tp_is_gc*/ +}; + +#define PyStdOut_Check(v) ((v)->ob_type == &PyStdOut_Type) + +static PyStdOut* newPyStdOut( bool iscerr ) +{ + PyStdOut *self; + self = PyObject_New(PyStdOut, &PyStdOut_Type); + if (self == NULL) + return NULL; + self->softspace = 0; + self->_cb = NULL; + self->_iscerr = iscerr; + return self; +} + +/*! + \class PyInterp_Interp + \brief Generic embedded Python interpreter. +*/ + +int PyInterp_Interp::_argc = 1; +char* PyInterp_Interp::_argv[] = {(char*)""}; +PyObject* PyInterp_Interp::builtinmodule = NULL; +PyThreadState* PyInterp_Interp::_gtstate = NULL; +PyInterpreterState* PyInterp_Interp::_interp = NULL; + +/*! + \brief Basic constructor. + + After construction the interpreter instance successor classes + must call virtual method initalize(). +*/ +PyInterp_Interp::PyInterp_Interp(): + _tstate(0), _vout(0), _verr(0), _g(0) +{ +} + +/*! + \brief Destructor. +*/ +PyInterp_Interp::~PyInterp_Interp() +{ +} + +/*! + \brief Initialize embedded interpreter. + + This method shoud be called after construction of the interpreter. + The method initialize() calls virtuals methods + - initPython() to initialize global Python interpreter + - initState() to initialize embedded interpreter state + - initContext() to initialize interpreter internal context + - initRun() to prepare interpreter for running commands + which should be implemented in the successor classes, according to the + embedded Python interpreter policy (mono or multi interpreter, etc). +*/ +void PyInterp_Interp::initialize() +{ + _history.clear(); // start a new list of user's commands + _ith = _history.begin(); + + initPython(); + // Here the global lock is released + + initState(); + + PyEval_AcquireThread(_tstate); + + initContext(); + + // used to interpret & compile commands + PyObjWrapper m(PyImport_ImportModule("codeop")); + if(!m) { + PyErr_Print(); + PyEval_ReleaseThread(_tstate); + return; + } + + // Create python objects to capture stdout and stderr + _vout=(PyObject*)newPyStdOut( false ); // stdout + _verr=(PyObject*)newPyStdOut( true ); // stderr + + // All the initRun outputs are redirected to the standard output (console) + initRun(); + PyEval_ReleaseThread(_tstate); +} + +/*! + \brief Initialize Python interpreter. + + In case if Python is not initialized, it sets program name, initializes the interpreter, sets program arguments, + initializes threads. + Otherwise, it just obtains the global interpreter and thread states. This is important for light SALOME configuration, + as in full SALOME this is done at SalomeApp level. + \sa SalomeApp_PyInterp class + */ +void PyInterp_Interp::initPython() +{ + if (!Py_IsInitialized()){ + // Python is not initialized + Py_SetProgramName(_argv[0]); + Py_Initialize(); // Initialize the interpreter + PySys_SetArgv(_argc, _argv); + PyEval_InitThreads(); // Create (and acquire) the interpreter lock + } + + if ( _interp == NULL ) + _interp = PyThreadState_Get()->interp; + if (PyType_Ready(&PyStdOut_Type) < 0) { + PyErr_Print(); + } + if ( _gtstate == NULL ) + _gtstate = PyEval_SaveThread(); // Release global thread state +} + +/*! + \brief Get embedded Python interpreter banner. + \return banner string + */ +std::string PyInterp_Interp::getbanner() +{ + // Should we take the lock ? + // PyEval_RestoreThread(_tstate); + std::string aBanner("Python "); + aBanner = aBanner + Py_GetVersion() + " on " + Py_GetPlatform() ; + aBanner = aBanner + "\ntype help to get general information on environment\n"; + //PyEval_SaveThread(); + return aBanner; +} + +/*! + \brief Initialize run command. + + This method is used to prepare interpreter for running + Python commands. + + \return \c true on success and \c false on error +*/ +bool PyInterp_Interp::initRun() +{ + // + // probably all below code isn't required + // + /* + PySys_SetObject("stderr",_verr); + PySys_SetObject("stdout",_vout); + + //PyObject *m = PyImport_GetModuleDict(); + + PySys_SetObject("stdout",PySys_GetObject("__stdout__")); + PySys_SetObject("stderr",PySys_GetObject("__stderr__")); + */ + return true; +} + +/*! + \brief Compile Python command and evaluate it in the + python dictionary context if possible. + \internal + \param command Python command string + \param context Python context (dictionary) + \return -1 on fatal error, 1 if command is incomplete and 0 + if command is executed successfully + */ +static int run_command(const char *command, PyObject *context) +{ + PyObject *m = PyImport_AddModule("codeop"); + if(!m) { // Fatal error. No way to go on. + PyErr_Print(); + return -1; + } + PyObjWrapper v(PyObject_CallMethod(m,(char*)"compile_command",(char*)"s",command)); + if(!v) { + // Error encountered. It should be SyntaxError, + //so we don't write out traceback + PyObjWrapper exception, value, tb; + PyErr_Fetch(&exception, &value, &tb); + PyErr_NormalizeException(&exception, &value, &tb); + PyErr_Display(exception, value, NULL); + return -1; + } + else if (v == Py_None) { + // Incomplete text we return 1 : we need a complete text to execute + return 1; + } + else { + // Complete and correct text. We evaluate it. + //#if PY_VERSION_HEX < 0x02040000 // python version earlier than 2.4.0 + // PyObjWrapper r(PyEval_EvalCode(v,context,context)); + //#else + PyObjWrapper r(PyEval_EvalCode((PyCodeObject *)(void *)v,context,context)); + //#endif + if(!r) { + // Execution error. We return -1 + PyErr_Print(); + return -1; + } + // The command has been successfully executed. Return 0 + return 0; + } +} + +void replaceAll(std::string& str, const std::string& from, const std::string& to) { + if(from.empty()) + return; + size_t start_pos = 0; + while((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx' + } +} +/*! + \brief Compile Python command and evaluate it in the + python dictionary context if possible. Command might correspond to + the execution of a script with optional arguments. + In this case, command is: + execfile(r"/absolute/path/to/script.py [args:arg1,...,argn]") + \internal + \param command Python command string + \param context Python context (dictionary) + \return -1 on fatal error, 1 if command is incomplete and 0 + if command is executed successfully + */ +static int compile_command(const char *command,PyObject *context) +{ + // First guess if command is execution of a script with args, or a simple Python command + std::string singleCommand = command; + std::string commandArgs = ""; + + std::size_t pos = std::string(command).find("args:"); + if (pos != std::string::npos) { + commandArgs = singleCommand.substr(pos+5); + commandArgs = commandArgs.substr(0, commandArgs.length()-3); + singleCommand = singleCommand.substr(0, pos-1)+"\")"; + } + + if (commandArgs.empty()) { + // process command: expression + // process command: execfile(r"/absolute/path/to/script.py") (no args) + return run_command(singleCommand.c_str(), context); + } + else { + // process command: execfile(r"/absolute/path/to/script.py [args:arg1,...,argn]") + std::string script = singleCommand.substr(11); // remove leading execfile(r" + script = script.substr(0, script.length()-2); // remove trailing ") + + std::string preCommandBegin = "import sys; save_argv = sys.argv; sys.argv=["; + std::string preCommandEnd = "];"; + replaceAll(commandArgs, ",", "\",\""); + commandArgs = "\""+commandArgs+"\""; + std::string completeCommand = preCommandBegin+"\""+script+"\","+commandArgs+preCommandEnd+singleCommand+";sys.argv=save_argv"; + return run_command(completeCommand.c_str(), context); + } +} + +/*! + \brief Run Python command. + \param command Python command + \return command status +*/ +int PyInterp_Interp::run(const char *command) +{ + beforeRun(); + return simpleRun(command); +} + +/*! + \brief Run Python command (used internally). + \param command Python command + \param addToHistory if \c true (default), the command is added to the commands history + \return command status +*/ +int PyInterp_Interp::simpleRun(const char *command, const bool addToHistory) +{ + if( addToHistory && strcmp(command,"") != 0 ) { + _history.push_back(command); + _ith = _history.end(); + } + + // We come from C++ to enter Python world + // We need to acquire the Python global lock + //PyLockWrapper aLock(_tstate); // san - lock is centralized now + + // Reset redirected outputs before treatment + PySys_SetObject((char*)"stderr",_verr); + PySys_SetObject((char*)"stdout",_vout); + + int ier = compile_command(command,_g); + + // Outputs are redirected on standards outputs (console) + PySys_SetObject((char*)"stdout",PySys_GetObject((char*)"__stdout__")); + PySys_SetObject((char*)"stderr",PySys_GetObject((char*)"__stderr__")); + + return ier; +} + +/*! + \brief Get previous command in the commands history. + \return previous command +*/ +const char * PyInterp_Interp::getPrevious() +{ + if(_ith != _history.begin()){ + _ith--; + return (*_ith).c_str(); + } + else + return BEGIN_HISTORY_PY; +} + +/*! + \brief Get next command in the commands history. + \return next command +*/ +const char * PyInterp_Interp::getNext() +{ + if(_ith != _history.end()){ + _ith++; + } + if (_ith == _history.end()) + return TOP_HISTORY_PY; + else + return (*_ith).c_str(); +} + +/*! + \brief Set Python standard output device hook. + \param cb callback function + \param data callback function parameters +*/ +void PyInterp_Interp::setvoutcb(PyOutChanged* cb, void* data) +{ + ((PyStdOut*)_vout)->_cb=cb; + ((PyStdOut*)_vout)->_data=data; +} + +/*! + \brief Set Python standard error device hook. + \param cb callback function + \param data callback function parameters +*/ +void PyInterp_Interp::setverrcb(PyOutChanged* cb, void* data) +{ + ((PyStdOut*)_verr)->_cb=cb; + ((PyStdOut*)_verr)->_data=data; +} diff --git a/src/PyInterp/PyInterp_Interp.h b/src/PyInterp/PyInterp_Interp.h new file mode 100644 index 000000000..d92bc4fc9 --- /dev/null +++ b/src/PyInterp/PyInterp_Interp.h @@ -0,0 +1,95 @@ + +#ifndef PYINTERP_INTERP_H +#define PYINTERP_INTERP_H + +#include "PyInterp.h" // !!! WARNING !!! THIS INCLUDE MUST BE THE VERY FIRST !!! + +#include +#include + +class PYINTERP_EXPORT PyLockWrapper +{ + PyThreadState* myThreadState; + PyThreadState* mySaveThreadState; + PyGILState_STATE _savestate; +public: + PyLockWrapper(PyThreadState* theThreadState); + ~PyLockWrapper(); +}; + +typedef void PyOutChanged(void* data,char * c); + +class PYINTERP_EXPORT PyInterp_Interp +{ +public: + static int _argc; + static char* _argv[]; + static PyObject *builtinmodule; + static PyThreadState *_gtstate; + static PyInterpreterState *_interp; + + PyInterp_Interp(); + virtual ~PyInterp_Interp(); + + void initialize(); + + virtual int run(const char *command); + + PyLockWrapper GetLockWrapper(); + + std::string getbanner(); + void setverrcb(PyOutChanged*,void*); + void setvoutcb(PyOutChanged*,void*); + + const char * getPrevious(); + const char * getNext(); + +protected: + PyThreadState * _tstate; + PyObject * _vout; + PyObject * _verr; + PyObject * _g; + PyObject * _codeop; + std::list _history; + std::list::iterator _ith; + + virtual int beforeRun() { return 0; } + int simpleRun(const char* command, const bool addToHistory = true); + + virtual bool initRun(); + virtual void initPython(); + virtual bool initState() = 0; + virtual bool initContext() = 0; +}; + +class PYINTERP_EXPORT PyObjWrapper +{ + PyObject* myObject; +public: + PyObjWrapper(PyObject* theObject) : myObject(theObject) {} + PyObjWrapper() : myObject(0) {} + virtual ~PyObjWrapper() { Py_XDECREF(myObject); } + + operator PyObject*() { return myObject; } + PyObject* operator->() { return myObject; } + PyObject* get() { return myObject; } + bool operator!() { return !myObject; } + bool operator==(PyObject* theObject) { return myObject == theObject; } + PyObject** operator&() { return &myObject; } + PyObjWrapper& operator=(PyObjWrapper* theObjWrapper) + { + Py_XDECREF(myObject); + myObject = theObjWrapper->myObject; + return *this; + } +}; + +typedef struct { + PyObject_HEAD + int softspace; + PyOutChanged* _cb; + void* _data; + bool _iscerr; +} PyStdOut; + +#endif // PYINTERP_INTERP_H diff --git a/src/PyInterp/PyInterp_Request.cxx b/src/PyInterp/PyInterp_Request.cxx new file mode 100644 index 000000000..a4ce0992f --- /dev/null +++ b/src/PyInterp/PyInterp_Request.cxx @@ -0,0 +1,4 @@ + + +#include "PyInterp_Request.h" + diff --git a/src/PyInterp/PyInterp_Request.h b/src/PyInterp/PyInterp_Request.h new file mode 100644 index 000000000..9cc3a68ac --- /dev/null +++ b/src/PyInterp/PyInterp_Request.h @@ -0,0 +1,81 @@ + + +#ifndef PYINTERP_REQUEST_H +#define PYINTERP_REQUEST_H + +#include "PyInterp.h" +#include "PyInterp_Event.h" + +#include +#include + +class PyInterp_Interp; +class PyInterp_Watcher; +class PyInterp_Dispatcher; +class PyInterp_ExecuteEvent; +class PyConsole_Editor; + +class PYINTERP_EXPORT PyInterp_Request +{ + friend class PyInterp_Dispatcher; + friend class PyInterp_ExecuteEvent; + + PyInterp_Request(); + PyInterp_Request( const PyInterp_Request& ); + +protected: + virtual ~PyInterp_Request() {}; + // protected destructor - to control deletion of requests + +public: + PyInterp_Request( QObject* listener, bool sync = false ) + : myIsSync( sync ), myListener( listener ) {}; + + static void Destroy( PyInterp_Request* ); + // Deletes a request + + bool IsSync() const { return myIsSync; } + // Returns true if this request should be processed synchronously, + // without putting it to a queue + +protected: + virtual void safeExecute(); + + virtual void execute() = 0; + // Should be redefined in successors, contains actual request code + + virtual QEvent* createEvent(); + // This method can be overridden to customize notification event creation + + virtual void processEvent( QObject* ); + + QObject* listener() const { return myListener; } + void setListener( QObject* ); + +private: + void process(); + +private: + QMutex myMutex; + bool myIsSync; + QObject* myListener; +}; + +class PYINTERP_EXPORT PyInterp_LockRequest : public PyInterp_Request +{ +public: + + PyInterp_LockRequest( PyInterp_Interp* interp, QObject* listener = 0, bool sync = false ) + : PyInterp_Request( listener, sync ), myInterp( interp ) + {} + +protected: + PyInterp_Interp* getInterp() const { return myInterp; } + + virtual void safeExecute(); + +private: + PyInterp_Interp* myInterp; +}; + +#endif // PYINTERP_REQUEST_H diff --git a/src/PyInterp/PyInterp_Watcher.h b/src/PyInterp/PyInterp_Watcher.h new file mode 100644 index 000000000..91126b7e9 --- /dev/null +++ b/src/PyInterp/PyInterp_Watcher.h @@ -0,0 +1,24 @@ + +#ifndef PYINTERP_WATCHER_H +#define PYINTERP_WATCHER_H + +#include "PyInterp.h" // !!! WARNING !!! THIS INCLUDE MUST BE THE VERY FIRST !!! + +#include "PyInterp_Dispatcher.h" + +#include + +// Private class that keeps track of destructions of request listeners +class PYINTERP_EXPORT PyInterp_Watcher : public QObject +{ + Q_OBJECT + +public: + PyInterp_Watcher() : QObject( 0 ) {} + virtual ~PyInterp_Watcher() {} + +public slots: + void onDestroyed( QObject* o ) { PyInterp_Dispatcher::Get()->objectDestroyed( o ); } +}; + +#endif // PYINTERP_WATCHER_H diff --git a/src/XGUI/CMakeLists.txt b/src/XGUI/CMakeLists.txt index e749048fc..5093a6167 100644 --- a/src/XGUI/CMakeLists.txt +++ b/src/XGUI/CMakeLists.txt @@ -1,6 +1,5 @@ CMAKE_MINIMUM_REQUIRED(VERSION 2.8.11) -INCLUDE(Common) INCLUDE(FindCAS) SET(CMAKE_AUTOMOC ON) @@ -56,6 +55,9 @@ SET(PROJECT_LIBRARIES ${Qt5Widgets_LIBRARIES} ${CAS_VIEWER} ${CAS_KERNEL} + PyConsole + PyInterp + PyEvent ) QT5_ADD_RESOURCES(PROJECT_COMPILED_RESOURCES ${PROJECT_RESOURCES}) @@ -68,8 +70,12 @@ ADD_DEFINITIONS(${CAS_DEFINITIONS} ) INCLUDE_DIRECTORIES (${PROJECT_SOURCE_DIR}/src/Event ${PROJECT_SOURCE_DIR}/src/Config + ${PROJECT_SOURCE_DIR}/src/PyInterp + ${PROJECT_SOURCE_DIR}/src/PyConsole ${CAS_INCLUDE_DIRS}) +LINK_DIRECTORIES($ENV{PYTHON_LIB_DIR}) + ADD_EXECUTABLE(XGUI WIN32 ${PROJECT_SOURCES} ${PROJECT_HEADERS} diff --git a/src/XGUI/XGUI_MainMenu.cpp b/src/XGUI/XGUI_MainMenu.cpp index f6b703611..bb7bbd582 100644 --- a/src/XGUI/XGUI_MainMenu.cpp +++ b/src/XGUI/XGUI_MainMenu.cpp @@ -11,6 +11,7 @@ XGUI_MainMenu::XGUI_MainMenu(XGUI_MainWindow *parent) : QObject(parent), myDesktop(parent) { parent->setTabPosition(Qt::TopDockWidgetArea, QTabWidget::North); + myGeneralPage = addWorkbench(tr("GEN_MENU_TITLE")); } XGUI_MainMenu::~XGUI_MainMenu(void) diff --git a/src/XGUI/XGUI_MainMenu.h b/src/XGUI/XGUI_MainMenu.h index 42db895d6..c5837e757 100644 --- a/src/XGUI/XGUI_MainMenu.h +++ b/src/XGUI/XGUI_MainMenu.h @@ -24,9 +24,14 @@ public: XGUI_Workbench* addWorkbench(const QString& theId, const QString& theText = ""); XGUI_Workbench* findWorkbench(const QString& theId); + XGUI_Workbench* generalPage() const { return myGeneralPage; } + + QDockWidget* getLastDockWindow() const { return myMenuTabs.last(); } + private: XGUI_MainWindow* myDesktop; QList myMenuTabs; + XGUI_Workbench* myGeneralPage; }; #endif diff --git a/src/XGUI/XGUI_MainWindow.cpp b/src/XGUI/XGUI_MainWindow.cpp index 7ab8aafdf..15f36010c 100644 --- a/src/XGUI/XGUI_MainWindow.cpp +++ b/src/XGUI/XGUI_MainWindow.cpp @@ -3,6 +3,9 @@ #include "XGUI_ViewWindow.h" #include "XGUI_Viewer.h" +#include +#include + #include #include #include @@ -21,7 +24,9 @@ #include XGUI_MainWindow::XGUI_MainWindow(QWidget* parent) - : QMainWindow(parent), myObjectBrowser(0) + : QMainWindow(parent), + myObjectBrowser(0), + myPythonConsole(0) { setWindowTitle(tr("WINDOW_TITLE")); myMenuBar = new XGUI_MainMenu(this); @@ -36,25 +41,13 @@ XGUI_MainWindow::XGUI_MainWindow(QWidget* parent) addDockWidget(Qt::LeftDockWidgetArea, aDoc); //aDoc->hide(); - aDoc = new QDockWidget(this); - aDoc->setFeatures(QDockWidget::AllDockWidgetFeatures | QDockWidget::DockWidgetVerticalTitleBar); - aDoc->setMinimumHeight(0); - aDoc->setWindowTitle("Console"); - QTextEdit* aTextEdt = new QTextEdit(aDoc); - aTextEdt->setText(">>>"); - aDoc->setWidget(aTextEdt); - aTextEdt->setMinimumHeight(0); - addDockWidget(Qt::BottomDockWidgetArea, aDoc); - QMdiArea* aMdiArea = new QMdiArea(this); setCentralWidget(aMdiArea); myViewer = new XGUI_Viewer(this); - //aMdiArea->addSubWindow(new XGUI_ViewWindow(), Qt::FramelessWindowHint); - //aMdiArea->addSubWindow(new XGUI_ViewWindow(), Qt::FramelessWindowHint); - fillObjectBrowser(); - addPropertyPanel(); + //fillObjectBrowser(); + //addPropertyPanel(); } XGUI_MainWindow::~XGUI_MainWindow(void) @@ -79,6 +72,40 @@ void XGUI_MainWindow::hideObjectBrowser() myObjectBrowser->parentWidget()->hide(); } +//****************************************************** +void XGUI_MainWindow::showPythonConsole() +{ + if (!myPythonConsole) { + + QDockWidget* aDoc = new QDockWidget(this); + aDoc->setFeatures(QDockWidget::AllDockWidgetFeatures | QDockWidget::DockWidgetVerticalTitleBar); + aDoc->setMinimumHeight(0); + aDoc->setWindowTitle("Console"); + myPythonConsole = new PyConsole_EnhConsole( aDoc, new PyConsole_EnhInterp()); + //myPythonConsole = new QTextEdit(aDoc); + //myPythonConsole->setGeometry(0,0,200, 50); + //myPythonConsole->setText(">>>"); + aDoc->setWidget(myPythonConsole); + //myPythonConsole->setMinimumHeight(0); + addDockWidget(Qt::TopDockWidgetArea, aDoc); + tabifyDockWidget(myMenuBar->getLastDockWindow(), aDoc); + } + myPythonConsole->parentWidget()->show(); +} + +//****************************************************** +void XGUI_MainWindow::hidePythonConsole() +{ + if (myPythonConsole) + myPythonConsole->parentWidget()->hide(); +} + + + +//****************************************************** + +// TEST FUNCTIONS + //****************************************************** void XGUI_MainWindow::fillObjectBrowser() { diff --git a/src/XGUI/XGUI_MainWindow.h b/src/XGUI/XGUI_MainWindow.h index a4b05f3d9..ec5da26a6 100644 --- a/src/XGUI/XGUI_MainWindow.h +++ b/src/XGUI/XGUI_MainWindow.h @@ -7,6 +7,7 @@ class XGUI_MainMenu; class XGUI_Viewer; class QTreeWidget; class QMdiArea; +class PyConsole_EnhConsole; class XGUI_MainWindow: public QMainWindow { @@ -25,6 +26,7 @@ public: { return myObjectBrowser; } + void showObjectBrowser(); void hideObjectBrowser(); @@ -35,10 +37,11 @@ public: return myViewer; } + void showPythonConsole(); + void hidePythonConsole(); + private: //!! For test purposes only - //QWidget* getSubWindow(); - void fillObjectBrowser(); void addPropertyPanel(); @@ -46,6 +49,8 @@ private: QTreeWidget* myObjectBrowser; XGUI_Viewer* myViewer; + + PyConsole_EnhConsole* myPythonConsole; }; #endif diff --git a/src/XGUI/XGUI_ViewPort.cpp b/src/XGUI/XGUI_ViewPort.cpp index c7d5e3208..5a8b5cb7c 100644 --- a/src/XGUI/XGUI_ViewPort.cpp +++ b/src/XGUI/XGUI_ViewPort.cpp @@ -272,8 +272,6 @@ XGUI_ViewPort::XGUI_ViewPort(XGUI_ViewWindow* theParent, const Handle(V3d_Viewer myActiveView = myPerspView; } myActiveView->SetSurfaceDetail(V3d_TEX_ALL); - - //setBackground( Qtx::BackgroundData( Qt::black ) ); // set default background } //*********************************************** diff --git a/src/XGUI/XGUI_ViewWindow.cpp b/src/XGUI/XGUI_ViewWindow.cpp index db7283557..c536c7c41 100644 --- a/src/XGUI/XGUI_ViewWindow.cpp +++ b/src/XGUI/XGUI_ViewWindow.cpp @@ -133,7 +133,7 @@ XGUI_ViewWindow::XGUI_ViewWindow(XGUI_Viewer* theViewer, V3d_TypeOfView theType) myCurrPointType(XGUI::GRAVITY), myPrevPointType(XGUI::GRAVITY), myRotationPointSelection(false), - myClosable(false) + myClosable(true) { mySelectedPoint = gp_Pnt(0., 0., 0.); setFrameStyle(QFrame::Raised); @@ -1089,14 +1089,15 @@ void XGUI_ViewWindow::reset() void XGUI_ViewWindow::updateToolBar() { - myGripWgt->repaint(); - myViewBar->repaint(); - myWindowBar->repaint(); - //QTimer::singleShot(300, this, SLOT(repaintToolBar())); + myGripWgt->update(); + myViewBar->update(); + myWindowBar->update(); + //QTimer::singleShot(50, Qt::VeryCoarseTimer, this, SLOT(repaintToolBar())); } /*void XGUI_ViewWindow::repaintToolBar() { + QApplication::sync(); myGripWgt->repaint(); myViewBar->repaint(); myWindowBar->repaint(); diff --git a/src/XGUI/XGUI_ViewWindow.h b/src/XGUI/XGUI_ViewWindow.h index cd3db6862..3efd7bb87 100644 --- a/src/XGUI/XGUI_ViewWindow.h +++ b/src/XGUI/XGUI_ViewWindow.h @@ -126,7 +126,7 @@ private slots: void onMaximize(); void updateToolBar(); - //void repaintToolBar(); +// void repaintToolBar(); private: enum WindowState @@ -222,6 +222,9 @@ public: ViewerToolbar(QWidget* theParent, XGUI_ViewPort* thePort) : QToolBar(theParent), myVPort(thePort) { + setBackgroundRole(QPalette::NoRole); + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_PaintOnScreen); } void repaintBackground(); @@ -241,6 +244,7 @@ public: ViewerLabel(QWidget* theParent, XGUI_ViewPort* thePort) : QLabel(theParent), myVPort(thePort) { + setAttribute(Qt::WA_NoSystemBackground); } void repaintBackground(); diff --git a/src/XGUI/XGUI_Viewer.cpp b/src/XGUI/XGUI_Viewer.cpp index 147c7937e..881b91f87 100644 --- a/src/XGUI/XGUI_Viewer.cpp +++ b/src/XGUI/XGUI_Viewer.cpp @@ -169,7 +169,8 @@ QMdiSubWindow* XGUI_Viewer::createView(V3d_TypeOfView theType) if (myViews.size() == 0) setTrihedronShown(true); - view->setBackground(XGUI_ViewBackground(XGUI::VerticalGradient, Qt::green, Qt::blue)); + view->setBackground(XGUI_ViewBackground(XGUI::VerticalGradient, Qt::white, QColor(Qt::blue).lighter())); + //view->setBackground(XGUI_ViewBackground(Qt::black)); QMdiArea* aMDI = myMainWindow->mdiArea(); QMdiSubWindow* aWnd = aMDI->addSubWindow(view, Qt::FramelessWindowHint); diff --git a/src/XGUI/XGUI_Workshop.cpp b/src/XGUI/XGUI_Workshop.cpp index 16d693278..9de01423a 100644 --- a/src/XGUI/XGUI_Workshop.cpp +++ b/src/XGUI/XGUI_Workshop.cpp @@ -52,12 +52,13 @@ void XGUI_Workshop::startApplication() myMainWindow->show(); QMdiSubWindow* aWnd = myMainWindow->viewer()->createView(); aWnd->showMaximized(); + myMainWindow->showPythonConsole(); } //****************************************************** void XGUI_Workshop::initMenu() { - XGUI_Workbench* aPage = addWorkbench(tr("GEN_MENU_TITLE")); + XGUI_Workbench* aPage = myMainWindow->menuObject()->generalPage(); // File commands group XGUI_MenuGroupPanel* aGroup = aPage->addGroup("Default"); diff --git a/src/XGUI/XGUI_msg_en.ts b/src/XGUI/XGUI_msg_en.ts index 478964927..afe0ddcae 100644 --- a/src/XGUI/XGUI_msg_en.ts +++ b/src/XGUI/XGUI_msg_en.ts @@ -6,10 +6,6 @@ XGUI_Workshop - - GEN_MENU_TITLE - General - NEW_MENU New @@ -116,6 +112,10 @@ MENU_TITLE Menu + + GEN_MENU_TITLE + General + XGUI_Viewer -- 2.39.2