From 192fbfd5e5138ebe11d0f7ee4575d48d8b5fc430 Mon Sep 17 00:00:00 2001 From: Seth Hillbrand <seth@kipro-pcb.com> Date: Mon, 15 Mar 2021 14:56:13 -0700 Subject: [PATCH] Finalize Python Frame --- CMakeLists.txt | 2 + common/CMakeLists.txt | 2 +- common/eda_base_frame.cpp | 57 +- common/eda_dde.cpp | 8 +- common/eda_item.cpp | 1 - common/kiway.cpp | 4 +- common/kiway_player.cpp | 7 +- common/single_top.cpp | 4 +- include/eda_base_frame.h | 7 + include/eda_dde.h | 5 +- include/eda_draw_frame.h | 13 - include/kiway_player.h | 21 +- include/pgm_base.h | 2 + pcbnew/pcb_edit_frame.cpp | 4 + scripting/CMakeLists.txt | 5 +- scripting/kicad_pyshell/__init__.py | 40 +- scripting/kicad_pyshell/kicad_pyeditor.py | 1059 +++++++++++++++++++++ scripting/kicad_scripting_main.cpp | 89 +- scripting/kipython_frame.cpp | 91 ++ scripting/kipython_frame.h | 63 ++ scripting/python_scripting.cpp | 110 +-- 21 files changed, 1337 insertions(+), 257 deletions(-) create mode 100644 scripting/kicad_pyshell/kicad_pyeditor.py create mode 100644 scripting/kipython_frame.cpp create mode 100644 scripting/kipython_frame.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f4547e5da0..b8dc1c38c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -761,6 +761,8 @@ include( ${SWIG_USE_FILE} ) # pybind11 is header-only, so include the subdir add_subdirectory(thirdparty/pybind11) +include_directories( SYSTEM ${PYBIND11_INCLUDE_DIR} ) + set( PythonInterp_FIND_VERSION 3.6 ) set( PythonLibs_FIND_VERSION 3.6 ) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index b9ce2ec584..7fabf84ea3 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -19,7 +19,6 @@ include_directories( ${INC_AFTER} ) - if( NOT APPLE ) # windows and linux use openssl under curl find_package( OpenSSL REQUIRED ) include_directories( SYSTEM ${OPENSSL_INCLUDE_DIR} ) @@ -484,6 +483,7 @@ target_link_libraries( common kimath kiplatform gal + pybind11::embed ${Boost_LIBRARIES} ${CURL_LIBRARIES} ${OPENSSL_LIBRARIES} # empty on Apple diff --git a/common/eda_base_frame.cpp b/common/eda_base_frame.cpp index 357be95462..b63a997e6a 100644 --- a/common/eda_base_frame.cpp +++ b/common/eda_base_frame.cpp @@ -102,28 +102,24 @@ BEGIN_EVENT_TABLE( EDA_BASE_FRAME, wxFrame ) EVT_SYS_COLOUR_CHANGED( EDA_BASE_FRAME::onSystemColorChange ) END_EVENT_TABLE() -EDA_BASE_FRAME::EDA_BASE_FRAME( wxWindow* aParent, FRAME_T aFrameType, - const wxString& aTitle, const wxPoint& aPos, const wxSize& aSize, - long aStyle, const wxString& aFrameName, KIWAY* aKiway ) : - wxFrame( aParent, wxID_ANY, aTitle, aPos, aSize, aStyle, aFrameName ), - TOOLS_HOLDER(), - KIWAY_HOLDER( aKiway, KIWAY_HOLDER::FRAME ), - m_ident( aFrameType ), - m_maximizeByDefault( false ), - m_infoBar( nullptr ), - m_settingsManager( nullptr ), - m_fileHistory( nullptr ), - m_hasAutoSave( false ), - m_autoSaveState( false ), - m_autoSaveInterval(-1 ), - m_undoRedoCountMax( DEFAULT_MAX_UNDO_ITEMS ), - m_userUnits( EDA_UNITS::MILLIMETRES ), - m_isClosing( false ), - m_isNonUserClose( false ) + +void EDA_BASE_FRAME::commonInit( FRAME_T aFrameType ) { - m_autoSaveTimer = new wxTimer( this, ID_AUTO_SAVE_TIMER ); - m_mruPath = PATHS::GetDefaultUserProjectsPath(); - m_frameSize = defaultSize( aFrameType ); + m_ident = aFrameType; + m_maximizeByDefault = false; + m_infoBar = nullptr; + m_settingsManager = nullptr; + m_fileHistory = nullptr; + m_hasAutoSave = false; + m_autoSaveState = false; + m_autoSaveInterval = -1; + m_undoRedoCountMax = DEFAULT_MAX_UNDO_ITEMS; + m_userUnits = EDA_UNITS::MILLIMETRES; + m_isClosing = false; + m_isNonUserClose = false; + m_autoSaveTimer = new wxTimer( this, ID_AUTO_SAVE_TIMER ); + m_mruPath = PATHS::GetDefaultUserProjectsPath(); + m_frameSize = defaultSize( aFrameType ); m_auimgr.SetArtProvider( new WX_AUI_DOCK_ART() ); @@ -143,6 +139,25 @@ EDA_BASE_FRAME::EDA_BASE_FRAME( wxWindow* aParent, FRAME_T aFrameType, Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( EDA_BASE_FRAME::windowClosing ) ); initExitKey(); + +} + +EDA_BASE_FRAME::EDA_BASE_FRAME( FRAME_T aFrameType, KIWAY* aKiway ) : + wxFrame(), + TOOLS_HOLDER(), + KIWAY_HOLDER( aKiway, KIWAY_HOLDER::FRAME ) +{ + commonInit( aFrameType ); +} + +EDA_BASE_FRAME::EDA_BASE_FRAME( wxWindow* aParent, FRAME_T aFrameType, + const wxString& aTitle, const wxPoint& aPos, const wxSize& aSize, + long aStyle, const wxString& aFrameName, KIWAY* aKiway ) : + wxFrame( aParent, wxID_ANY, aTitle, aPos, aSize, aStyle, aFrameName ), + TOOLS_HOLDER(), + KIWAY_HOLDER( aKiway, KIWAY_HOLDER::FRAME ) +{ + commonInit( aFrameType ); } diff --git a/common/eda_dde.cpp b/common/eda_dde.cpp index 6dad2603e2..f7d59a481f 100644 --- a/common/eda_dde.cpp +++ b/common/eda_dde.cpp @@ -27,7 +27,7 @@ #include <thread> #include <eda_dde.h> -#include <eda_draw_frame.h> +#include <kiway_player.h> #include <id.h> #include <wx/wx.h> @@ -45,7 +45,7 @@ static char client_ipc_buffer[IPC_BUF_SIZE]; /* Function to initialize a server socket */ -void EDA_DRAW_FRAME::CreateServer( int service, bool local ) +void KIWAY_PLAYER::CreateServer( int service, bool local ) { wxIPV4address addr; @@ -67,7 +67,7 @@ void EDA_DRAW_FRAME::CreateServer( int service, bool local ) /* Function called on every client request. */ -void EDA_DRAW_FRAME::OnSockRequest( wxSocketEvent& evt ) +void KIWAY_PLAYER::OnSockRequest( wxSocketEvent& evt ) { size_t len; wxSocketBase* sock = evt.GetSocket(); @@ -99,7 +99,7 @@ void EDA_DRAW_FRAME::OnSockRequest( wxSocketEvent& evt ) /* Function called when a connection is requested by a client. */ -void EDA_DRAW_FRAME::OnSockRequestServer( wxSocketEvent& evt ) +void KIWAY_PLAYER::OnSockRequestServer( wxSocketEvent& evt ) { wxSocketBase* socket; wxSocketServer* server = (wxSocketServer*) evt.GetSocket(); diff --git a/common/eda_item.cpp b/common/eda_item.cpp index c1fb070186..fcfdc0e368 100644 --- a/common/eda_item.cpp +++ b/common/eda_item.cpp @@ -23,7 +23,6 @@ */ #include <algorithm> -#include <pybind11/pybind11.h> #include <bitmaps.h> #include <eda_item.h> diff --git a/common/kiway.cpp b/common/kiway.cpp index 2783c899cd..18a7a3baad 100644 --- a/common/kiway.cpp +++ b/common/kiway.cpp @@ -264,9 +264,9 @@ KIFACE* KIWAY::KiFACE( FACE_T aFaceId, bool doLoad ) } else { - KIFACE_GETTER_FUNC* getter = (KIFACE_GETTER_FUNC*) addr; + KIFACE_GETTER_FUNC* ki_getter = (KIFACE_GETTER_FUNC*) addr; - KIFACE* kiface = getter( &m_kiface_version[aFaceId], KIFACE_VERSION, m_program ); + KIFACE* kiface = ki_getter( &m_kiface_version[aFaceId], KIFACE_VERSION, m_program ); // KIFACE_GETTER_FUNC function comment (API) says the non-NULL is unconditional. wxASSERT_MSG( kiface, diff --git a/common/kiway_player.cpp b/common/kiway_player.cpp index fc1720b916..4889bd12f7 100644 --- a/common/kiway_player.cpp +++ b/common/kiway_player.cpp @@ -47,7 +47,8 @@ KIWAY_PLAYER::KIWAY_PLAYER( KIWAY* aKiway, wxWindow* aParent, FRAME_T aFrameType m_modal( false ), m_modal_loop( nullptr ), m_modal_resultant_parent( nullptr ), - m_modal_ret_val( false ) + m_modal_ret_val( false ), + m_socketServer( nullptr ) { } @@ -59,11 +60,11 @@ KIWAY_PLAYER::KIWAY_PLAYER( wxWindow* aParent, wxWindowID aId, const wxString& a m_modal( false ), m_modal_loop( nullptr ), m_modal_resultant_parent( nullptr ), - m_modal_ret_val( false ) + m_modal_ret_val( false ), + m_socketServer( nullptr ) { } - KIWAY_PLAYER::~KIWAY_PLAYER() throw() {} diff --git a/common/single_top.cpp b/common/single_top.cpp index a3d0af274c..30da289073 100644 --- a/common/single_top.cpp +++ b/common/single_top.cpp @@ -295,12 +295,12 @@ bool PGM_SINGLE_TOP::OnPgmInit() // i.e. they are single part link images so don't need to load a *.kiface. // Get the getter, it is statically linked into this binary image. - KIFACE_GETTER_FUNC* getter = &KIFACE_GETTER; + KIFACE_GETTER_FUNC* ki_getter = &KIFACE_GETTER; int kiface_version; // Get the KIFACE. - KIFACE* kiface = getter( &kiface_version, KIFACE_VERSION, this ); + KIFACE* kiface = ki_getter( &kiface_version, KIFACE_VERSION, this ); // Trick the KIWAY into thinking it loaded a KIFACE, by recording the KIFACE // in the KIWAY. It needs to be there for KIWAY::OnKiwayEnd() anyways. diff --git a/include/eda_base_frame.h b/include/eda_base_frame.h index 982e4d0269..9cf79dfe3f 100644 --- a/include/eda_base_frame.h +++ b/include/eda_base_frame.h @@ -106,6 +106,8 @@ public: const wxPoint& aPos, const wxSize& aSize, long aStyle, const wxString& aFrameName, KIWAY* aKiway ); + EDA_BASE_FRAME( FRAME_T aFrameType, KIWAY* aKiway ); + ~EDA_BASE_FRAME(); /** @@ -650,6 +652,11 @@ private: */ void windowClosing( wxCloseEvent& event ); + /** + * Collect common initialization functions used in all CTORs + */ + void commonInit( FRAME_T aFrameType ); + wxWindow* findQuasiModalDialog(); /** diff --git a/include/eda_dde.h b/include/eda_dde.h index e9a7a2ffda..365dade51e 100644 --- a/include/eda_dde.h +++ b/include/eda_dde.h @@ -2,7 +2,7 @@ * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2007-2014 Jean-Pierre Charras, jp.charras at wanadoo.fr - * Copyright (C) 1992-2014 KiCad Developers, see CHANGELOG.TXT for contributors. + * Copyright (C) 1992-2021 KiCad Developers, see CHANGELOG.TXT for contributors. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -42,6 +42,9 @@ ///< Eeschema listens on this port for commands from Pcbnew #define KICAD_SCH_PORT_SERVICE_NUMBER 4243 +///< Scripting window listens for commands for other apps +#define KICAD_PY_PORT_SERVICE_NUMBER 4244 + #define MSG_TO_PCB KICAD_PCB_PORT_SERVICE_NUMBER #define MSG_TO_SCH KICAD_SCH_PORT_SERVICE_NUMBER diff --git a/include/eda_draw_frame.h b/include/eda_draw_frame.h index d29e19e0fa..4bdbe0c18f 100644 --- a/include/eda_draw_frame.h +++ b/include/eda_draw_frame.h @@ -191,14 +191,6 @@ public: */ virtual BASE_SCREEN* GetScreen() const { return m_currentScreen; } - /** - * Execute a remote command sent via socket (to port KICAD_PCB_PORT_SERVICE_NUMBER, - * currently 4242). - * - * Subclasses should override to implement actual command handlers. - */ - virtual void ExecuteRemoteCommand( const char* cmdline ){} - void EraseMsgBox(); void ReCreateMenuBar() override { } @@ -346,11 +338,6 @@ public: */ virtual void DisplayGridMsg(); - /* interprocess communication */ - void CreateServer( int service, bool local = true ); - void OnSockRequest( wxSocketEvent& evt ); - void OnSockRequestServer( wxSocketEvent& evt ); - void LoadSettings( APP_SETTINGS_BASE* aCfg ) override; void SaveSettings( APP_SETTINGS_BASE* aCfg ) override; diff --git a/include/kiway_player.h b/include/kiway_player.h index bd3b1c3c6c..4b4b04a81a 100644 --- a/include/kiway_player.h +++ b/include/kiway_player.h @@ -32,7 +32,6 @@ #include <kiway_holder.h> #include <eda_base_frame.h> - class KIWAY; class PROJECT; struct KIFACE; @@ -43,6 +42,9 @@ class KIWAY_EXPRESS; #define WX_EVENT_LOOP wxGUIEventLoop class WX_EVENT_LOOP; +class wxSocketServer; +class wxSocketBase; + /** * A wxFrame capable of the OpenProjectFiles function, meaning it can load a portion of @@ -179,6 +181,20 @@ public: void DismissModal( bool aRetVal, const wxString& aResult = wxEmptyString ); + /* interprocess communication */ + void CreateServer( int service, bool local = true ); + void OnSockRequest( wxSocketEvent& evt ); + void OnSockRequestServer( wxSocketEvent& evt ); + + /** + * Execute a remote command sent via socket (to port KICAD_PCB_PORT_SERVICE_NUMBER, + * currently 4242). + * + * Subclasses should override to implement actual command handlers. + */ + virtual void ExecuteRemoteCommand( const char* cmdline ){} + + protected: /// event handler, routes to derivative specific virtual KiwayMailIn() @@ -198,6 +214,9 @@ protected: wxString m_modal_string; bool m_modal_ret_val; // true if a selection was made + wxSocketServer* m_socketServer; + std::vector<wxSocketBase*> m_sockets; ///< interprocess communication + #ifndef SWIG DECLARE_EVENT_TABLE() #endif diff --git a/include/pgm_base.h b/include/pgm_base.h index c99af07af7..e016d5c627 100644 --- a/include/pgm_base.h +++ b/include/pgm_base.h @@ -38,6 +38,7 @@ #include <wx/filename.h> #include <wx/gdicmn.h> +#include <pybind11/embed.h> class wxSingleInstanceChecker; class wxApp; @@ -329,6 +330,7 @@ protected: /// Flag to indicate if the environment variable overwrite warning dialog should be shown. bool m_show_env_var_dialog; + }; diff --git a/pcbnew/pcb_edit_frame.cpp b/pcbnew/pcb_edit_frame.cpp index 65102a8298..2166e33e21 100644 --- a/pcbnew/pcb_edit_frame.cpp +++ b/pcbnew/pcb_edit_frame.cpp @@ -1299,6 +1299,10 @@ void PCB_EDIT_FRAME::ScriptingConsoleEnableDisable() { frame = Kiway().Player( FRAME_PYTHON, true, this ); + // If we received an error in the CTOR due to Python-ness, don't crash + if( !frame ) + return; + if( !frame->IsVisible() ) frame->Show( true ); diff --git a/scripting/CMakeLists.txt b/scripting/CMakeLists.txt index d7d12585d9..f1a0576bfa 100644 --- a/scripting/CMakeLists.txt +++ b/scripting/CMakeLists.txt @@ -7,8 +7,6 @@ add_library( scripting STATIC ${KIPYTHON_SRCS} ) -add_subdirectory( pybind11 ) - target_link_libraries( scripting ${wxWidgets_LIBRARIES} # wxLogDebug, wxASSERT ${Boost_LIBRARIES} # Because of the OPT types @@ -32,6 +30,7 @@ target_include_directories( scripting PRIVATE # Setup the KIFACE add_library( scripting_kiface MODULE kicad_scripting_main.cpp + kipython_frame.cpp ${KIPYTHON_SRCS} ) @@ -39,7 +38,7 @@ set_source_files_properties( kicad_scripting_main.cpp PROPERTIES # The KIFACE is in kicad_scripting_main.cpp, export it: COMPILE_DEFINITIONS "BUILD_KIWAY_DLL;COMPILING_DLL" ) - + target_include_directories( scripting_kiface PRIVATE ${PROJECT_SOURCE_DIR}/bitmaps_png/include ${PROJECT_SOURCE_DIR}/include diff --git a/scripting/kicad_pyshell/__init__.py b/scripting/kicad_pyshell/__init__.py index 62831e1f76..813c02edb7 100644 --- a/scripting/kicad_pyshell/__init__.py +++ b/scripting/kicad_pyshell/__init__.py @@ -15,17 +15,16 @@ import wx import sys import os -from wx.py import crust, editor, version, dispatcher -from wx.py.editor import EditorNotebook +from wx.py import crust, version, dispatcher + +from .kicad_pyeditor import KiCadEditorNotebookFrame, KiCadEditorNotebook import pcbnew -INTRO = "KiCad - Python Shell" - - -class KiCadPyShell(editor.EditorNotebookFrame): - - """The KiCad Pythonshell based on wxPyShell""" +class KiCadPyShell(KiCadEditorNotebookFrame): + + def __init__(self, parent): + KiCadEditorNotebookFrame.__init__(self, parent) def _setup_startup(self): """Initialise the startup script.""" @@ -58,7 +57,7 @@ class KiCadPyShell(editor.EditorNotebookFrame): Called automatically by base class during init. """ - self.notebook = EditorNotebook(parent=self) + self.notebook = KiCadEditorNotebook(parent=self.parent) intro = 'Py %s' % version.VERSION import imp import builtins @@ -81,11 +80,6 @@ class KiCadPyShell(editor.EditorNotebookFrame): self.autoSaveHistory = False self.LoadSettings() - # in case of wxPhoenix we may need to create a wxApp first and store it - # to prevent removal by garbage collector - if 'phoenix' in wx.PlatformInfo: - self.theApp = wx.GetApp() or wx.App() - self.crust = crust.Crust(parent=self.notebook, intro=intro, locals=namespace, rootLabel="locals()", @@ -94,9 +88,9 @@ class KiCadPyShell(editor.EditorNotebookFrame): self.shell = self.crust.shell # Override the filling so that status messages go to the status bar. - self.crust.filling.tree.setStatusText = self.SetStatusText + self.crust.filling.tree.setStatusText = self.parent.SetStatusText # Override the shell so that status messages go to the status bar. - self.shell.setStatusText = self.SetStatusText + self.shell.setStatusText = self.parent.SetStatusText # Fix a problem with the sash shrinking to nothing. self.crust.filling.SetSashPosition(200) self.notebook.AddPage(page=self.crust, text='*Shell*', select=True) @@ -115,7 +109,7 @@ class KiCadPyShell(editor.EditorNotebookFrame): "wxPython Version: %s\n" % wx.VERSION_STRING + \ ("\t(%s)\n" % ", ".join(wx.PlatformInfo[1:])) - dialog = wx.MessageDialog(self, text, title, + dialog = wx.MessageDialog(self.parent, text, title, wx.OK | wx.ICON_INFORMATION) dialog.ShowModal() dialog.Destroy() @@ -127,7 +121,7 @@ class KiCadPyShell(editor.EditorNotebookFrame): def LoadSettings(self): """Load settings for the shell.""" if self.config is not None: - editor.EditorNotebookFrame.LoadSettings(self, self.config) + KiCadEditorNotebookFrame.LoadSettings(self.parent, self.config) self.autoSaveSettings = \ self.config.ReadBool('Options/AutoSaveSettings', False) self.execStartupScript = \ @@ -150,7 +144,7 @@ class KiCadPyShell(editor.EditorNotebookFrame): self.config.WriteBool('Options/AutoSaveSettings', self.autoSaveSettings) if self.autoSaveSettings or force: - editor.EditorNotebookFrame.SaveSettings(self, self.config) + KiCadEditorNotebookFrame.SaveSettings(self, self.config) self.config.WriteBool('Options/AutoSaveHistory', self.autoSaveHistory) @@ -209,7 +203,7 @@ class KiCadPyShell(editor.EditorNotebookFrame): d.Destroy() -def makePcbnewShellWindow(parent=None): +def makePcbnewShellWindow(parentid): """ Create a new Shell Window and return its handle. @@ -219,6 +213,6 @@ def makePcbnewShellWindow(parent=None): Returns: The handle to the new window. """ - pyshell = KiCadPyShell(parent, id=-1, title=INTRO) - pyshell.Show() - return pyshell + + parent = wx.FindWindowById( parentid ) + return KiCadPyShell(parent) diff --git a/scripting/kicad_pyshell/kicad_pyeditor.py b/scripting/kicad_pyshell/kicad_pyeditor.py new file mode 100644 index 0000000000..d61c02a82e --- /dev/null +++ b/scripting/kicad_pyshell/kicad_pyeditor.py @@ -0,0 +1,1059 @@ +''' +Provides the backend for a basic python editor in KiCad. + +This takes most code from PyShell/PyCrust but adapts it to the KiCad +environment where the Python doesn't create a frame but instead hooks +into the existing KIWAY_PLAYER + +Original PyCrust code used from +https://github.com/wxWidgets/Phoenix/tree/master/wx/py +''' + +import wx + +from wx.py import crust, version, dispatcher +from wx.py.editor import Editor +from wx.py.buffer import Buffer + +ID_NEW = wx.ID_NEW +ID_OPEN = wx.ID_OPEN +ID_REVERT = wx.ID_REVERT +ID_CLOSE = wx.ID_CLOSE +ID_SAVE = wx.ID_SAVE +ID_SAVEAS = wx.ID_SAVEAS +ID_PRINT = wx.ID_PRINT +ID_EXIT = wx.ID_EXIT +ID_UNDO = wx.ID_UNDO +ID_REDO = wx.ID_REDO +ID_CUT = wx.ID_CUT +ID_COPY = wx.ID_COPY +ID_PASTE = wx.ID_PASTE +ID_CLEAR = wx.ID_CLEAR +ID_SELECTALL = wx.ID_SELECTALL +ID_EMPTYBUFFER = wx.NewIdRef() +ID_ABOUT = wx.ID_ABOUT +ID_HELP = wx.NewIdRef() +ID_AUTOCOMP_SHOW = wx.NewIdRef() +ID_AUTOCOMP_MAGIC = wx.NewIdRef() +ID_AUTOCOMP_SINGLE = wx.NewIdRef() +ID_AUTOCOMP_DOUBLE = wx.NewIdRef() +ID_CALLTIPS_SHOW = wx.NewIdRef() +ID_CALLTIPS_INSERT = wx.NewIdRef() +ID_COPY_PLUS = wx.NewIdRef() +ID_NAMESPACE = wx.NewIdRef() +ID_PASTE_PLUS = wx.NewIdRef() +ID_WRAP = wx.NewIdRef() +ID_TOGGLE_MAXIMIZE = wx.NewIdRef() +ID_SHOW_LINENUMBERS = wx.NewIdRef() +ID_ENABLESHELLMODE = wx.NewIdRef() +ID_ENABLEAUTOSYMPY = wx.NewIdRef() +ID_AUTO_SAVESETTINGS = wx.NewIdRef() +ID_SAVEACOPY = wx.NewIdRef() +ID_SAVEHISTORY = wx.NewIdRef() +ID_SAVEHISTORYNOW = wx.NewIdRef() +ID_CLEARHISTORY = wx.NewIdRef() +ID_SAVESETTINGS = wx.NewIdRef() +ID_DELSETTINGSFILE = wx.NewIdRef() +ID_EDITSTARTUPSCRIPT = wx.NewIdRef() +ID_EXECSTARTUPSCRIPT = wx.NewIdRef() +ID_SHOWPYSLICESTUTORIAL = wx.NewIdRef() +ID_FIND = wx.ID_FIND +ID_FINDNEXT = wx.NewIdRef() +ID_FINDPREVIOUS = wx.NewIdRef() +ID_SHOWTOOLS = wx.NewIdRef() +ID_HIDEFOLDINGMARGIN = wx.NewIdRef() + +import pcbnew + +INTRO = "KiCad - Python Shell" + +class KiCadPyFrame(): + + def __init__(self, parent): + """Create a Frame instance.""" + + self.parent = parent + self.parent.CreateStatusBar() + self.parent.SetStatusText('Frame') + self.shellName='KiCad Python Editor' + self.__createMenus() + + self.iconized = False + self.findDlg = None + self.findData = wx.FindReplaceData() + self.findData.SetFlags(wx.FR_DOWN) + + self.parent.Bind(wx.EVT_CLOSE, self.OnClose) + self.parent.Bind(wx.EVT_ICONIZE, self.OnIconize) + + + def OnIconize(self, event): + """Event handler for Iconize.""" + self.iconized = event.Iconized() + + + def OnClose(self, event): + """Event handler for closing.""" + self.parent.Destroy() + + + def __createMenus(self): + # File Menu + m = self.fileMenu = wx.Menu() + m.Append(ID_NEW, '&New \tCtrl+N', + 'New file') + m.Append(ID_OPEN, '&Open... \tCtrl+O', + 'Open file') + m.AppendSeparator() + m.Append(ID_REVERT, '&Revert \tCtrl+R', + 'Revert to last saved version') + m.Append(ID_CLOSE, '&Close \tCtrl+W', + 'Close file') + m.AppendSeparator() + m.Append(ID_SAVE, '&Save... \tCtrl+S', + 'Save file') + m.Append(ID_SAVEAS, 'Save &As \tCtrl+Shift+S', + 'Save file with new name') + m.AppendSeparator() + m.Append(ID_PRINT, '&Print... \tCtrl+P', + 'Print file') + m.AppendSeparator() + m.Append(ID_NAMESPACE, '&Update Namespace \tCtrl+Shift+N', + 'Update namespace for autocompletion and calltips') + m.AppendSeparator() + m.Append(ID_EXIT, 'E&xit\tCtrl+Q', 'Exit Program') + + # Edit + m = self.editMenu = wx.Menu() + m.Append(ID_UNDO, '&Undo \tCtrl+Z', + 'Undo the last action') + m.Append(ID_REDO, '&Redo \tCtrl+Y', + 'Redo the last undone action') + m.AppendSeparator() + m.Append(ID_CUT, 'Cu&t \tCtrl+X', + 'Cut the selection') + m.Append(ID_COPY, '&Copy \tCtrl+C', + 'Copy the selection') + m.Append(ID_COPY_PLUS, 'Cop&y Plus \tCtrl+Shift+C', + 'Copy the selection - retaining prompts') + m.Append(ID_PASTE, '&Paste \tCtrl+V', 'Paste from clipboard') + m.Append(ID_PASTE_PLUS, 'Past&e Plus \tCtrl+Shift+V', + 'Paste and run commands') + m.AppendSeparator() + m.Append(ID_CLEAR, 'Cle&ar', + 'Delete the selection') + m.Append(ID_SELECTALL, 'Select A&ll \tCtrl+A', + 'Select all text') + m.AppendSeparator() + m.Append(ID_EMPTYBUFFER, 'E&mpty Buffer...', + 'Delete all the contents of the edit buffer') + m.Append(ID_FIND, '&Find Text... \tCtrl+F', + 'Search for text in the edit buffer') + m.Append(ID_FINDNEXT, 'Find &Next \tCtrl+G', + 'Find next instance of the search text') + m.Append(ID_FINDPREVIOUS, 'Find Pre&vious \tCtrl+Shift+G', + 'Find previous instance of the search text') + + # View + m = self.viewMenu = wx.Menu() + m.Append(ID_WRAP, '&Wrap Lines\tCtrl+Shift+W', + 'Wrap lines at right edge', wx.ITEM_CHECK) + m.Append(ID_SHOW_LINENUMBERS, '&Show Line Numbers\tCtrl+Shift+L', + 'Show Line Numbers', wx.ITEM_CHECK) + m.Append(ID_TOGGLE_MAXIMIZE, '&Toggle Maximize\tF11', + 'Maximize/Restore Application') + + # Options + m = self.optionsMenu = wx.Menu() + + self.historyMenu = wx.Menu() + self.historyMenu.Append(ID_SAVEHISTORY, '&Autosave History', + 'Automatically save history on close', wx.ITEM_CHECK) + self.historyMenu.Append(ID_SAVEHISTORYNOW, '&Save History Now', + 'Save history') + self.historyMenu.Append(ID_CLEARHISTORY, '&Clear History ', + 'Clear history') + m.AppendSubMenu(self.historyMenu, "&History", "History Options") + + self.startupMenu = wx.Menu() + self.startupMenu.Append(ID_EXECSTARTUPSCRIPT, + 'E&xecute Startup Script', + 'Execute Startup Script', wx.ITEM_CHECK) + self.startupMenu.Append(ID_EDITSTARTUPSCRIPT, + '&Edit Startup Script...', + 'Edit Startup Script') + m.AppendSubMenu(self.startupMenu, '&Startup', 'Startup Options') + + self.settingsMenu = wx.Menu() + self.settingsMenu.Append(ID_AUTO_SAVESETTINGS, + '&Auto Save Settings', + 'Automatically save settings on close', wx.ITEM_CHECK) + self.settingsMenu.Append(ID_SAVESETTINGS, + '&Save Settings', + 'Save settings now') + self.settingsMenu.Append(ID_DELSETTINGSFILE, + '&Revert to default', + 'Revert to the default settings') + m.AppendSubMenu(self.settingsMenu, '&Settings', 'Settings Options') + + m = self.helpMenu = wx.Menu() + m.Append(ID_HELP, '&Help\tF1', 'Help!') + m.AppendSeparator() + m.Append(ID_ABOUT, '&About...', 'About this program') + + b = self.menuBar = wx.MenuBar() + b.Append(self.fileMenu, '&File') + b.Append(self.editMenu, '&Edit') + b.Append(self.viewMenu, '&View') + b.Append(self.optionsMenu, '&Options') + b.Append(self.helpMenu, '&Help') + self.parent.SetMenuBar(b) + + self.parent.Bind(wx.EVT_MENU, self.OnFileNew, id=ID_NEW) + self.parent.Bind(wx.EVT_MENU, self.OnFileOpen, id=ID_OPEN) + self.parent.Bind(wx.EVT_MENU, self.OnFileRevert, id=ID_REVERT) + self.parent.Bind(wx.EVT_MENU, self.OnFileClose, id=ID_CLOSE) + self.parent.Bind(wx.EVT_MENU, self.OnFileSave, id=ID_SAVE) + self.parent.Bind(wx.EVT_MENU, self.OnFileSaveAs, id=ID_SAVEAS) + self.parent.Bind(wx.EVT_MENU, self.OnFileSaveACopy, id=ID_SAVEACOPY) + self.parent.Bind(wx.EVT_MENU, self.OnFileUpdateNamespace, id=ID_NAMESPACE) + self.parent.Bind(wx.EVT_MENU, self.OnFilePrint, id=ID_PRINT) + self.parent.Bind(wx.EVT_MENU, self.OnExit, id=ID_EXIT) + self.parent.Bind(wx.EVT_MENU, self.OnUndo, id=ID_UNDO) + self.parent.Bind(wx.EVT_MENU, self.OnRedo, id=ID_REDO) + self.parent.Bind(wx.EVT_MENU, self.OnCut, id=ID_CUT) + self.parent.Bind(wx.EVT_MENU, self.OnCopy, id=ID_COPY) + self.parent.Bind(wx.EVT_MENU, self.OnCopyPlus, id=ID_COPY_PLUS) + self.parent.Bind(wx.EVT_MENU, self.OnPaste, id=ID_PASTE) + self.parent.Bind(wx.EVT_MENU, self.OnPastePlus, id=ID_PASTE_PLUS) + self.parent.Bind(wx.EVT_MENU, self.OnClear, id=ID_CLEAR) + self.parent.Bind(wx.EVT_MENU, self.OnSelectAll, id=ID_SELECTALL) + self.parent.Bind(wx.EVT_MENU, self.OnEmptyBuffer, id=ID_EMPTYBUFFER) + self.parent.Bind(wx.EVT_MENU, self.OnAbout, id=ID_ABOUT) + self.parent.Bind(wx.EVT_MENU, self.OnHelp, id=ID_HELP) + self.parent.Bind(wx.EVT_MENU, self.OnAutoCompleteShow, id=ID_AUTOCOMP_SHOW) + self.parent.Bind(wx.EVT_MENU, self.OnAutoCompleteMagic, id=ID_AUTOCOMP_MAGIC) + self.parent.Bind(wx.EVT_MENU, self.OnAutoCompleteSingle, id=ID_AUTOCOMP_SINGLE) + self.parent.Bind(wx.EVT_MENU, self.OnAutoCompleteDouble, id=ID_AUTOCOMP_DOUBLE) + self.parent.Bind(wx.EVT_MENU, self.OnCallTipsShow, id=ID_CALLTIPS_SHOW) + self.parent.Bind(wx.EVT_MENU, self.OnCallTipsInsert, id=ID_CALLTIPS_INSERT) + self.parent.Bind(wx.EVT_MENU, self.OnWrap, id=ID_WRAP) + self.parent.Bind(wx.EVT_MENU, self.OnToggleMaximize, id=ID_TOGGLE_MAXIMIZE) + self.parent.Bind(wx.EVT_MENU, self.OnShowLineNumbers, id=ID_SHOW_LINENUMBERS) + self.parent.Bind(wx.EVT_MENU, self.OnEnableShellMode, id=ID_ENABLESHELLMODE) + self.parent.Bind(wx.EVT_MENU, self.OnEnableAutoSympy, id=ID_ENABLEAUTOSYMPY) + self.parent.Bind(wx.EVT_MENU, self.OnAutoSaveSettings, id=ID_AUTO_SAVESETTINGS) + self.parent.Bind(wx.EVT_MENU, self.OnSaveHistory, id=ID_SAVEHISTORY) + self.parent.Bind(wx.EVT_MENU, self.OnSaveHistoryNow, id=ID_SAVEHISTORYNOW) + self.parent.Bind(wx.EVT_MENU, self.OnClearHistory, id=ID_CLEARHISTORY) + self.parent.Bind(wx.EVT_MENU, self.OnSaveSettings, id=ID_SAVESETTINGS) + self.parent.Bind(wx.EVT_MENU, self.OnDelSettingsFile, id=ID_DELSETTINGSFILE) + self.parent.Bind(wx.EVT_MENU, self.OnEditStartupScript, id=ID_EDITSTARTUPSCRIPT) + self.parent.Bind(wx.EVT_MENU, self.OnExecStartupScript, id=ID_EXECSTARTUPSCRIPT) + self.parent.Bind(wx.EVT_MENU, self.OnShowPySlicesTutorial, id=ID_SHOWPYSLICESTUTORIAL) + self.parent.Bind(wx.EVT_MENU, self.OnFindText, id=ID_FIND) + self.parent.Bind(wx.EVT_MENU, self.OnFindNext, id=ID_FINDNEXT) + self.parent.Bind(wx.EVT_MENU, self.OnFindPrevious, id=ID_FINDPREVIOUS) + self.parent.Bind(wx.EVT_MENU, self.OnToggleTools, id=ID_SHOWTOOLS) + self.parent.Bind(wx.EVT_MENU, self.OnHideFoldingMargin, id=ID_HIDEFOLDINGMARGIN) + + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_NEW) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_OPEN) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_REVERT) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_CLOSE) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_SAVE) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_SAVEAS) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_NAMESPACE) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_PRINT) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_UNDO) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_REDO) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_CUT) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_COPY) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_COPY_PLUS) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_PASTE) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_PASTE_PLUS) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_CLEAR) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_SELECTALL) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_EMPTYBUFFER) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_AUTOCOMP_SHOW) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_AUTOCOMP_MAGIC) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_AUTOCOMP_SINGLE) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_AUTOCOMP_DOUBLE) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_CALLTIPS_SHOW) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_CALLTIPS_INSERT) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_WRAP) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_SHOW_LINENUMBERS) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_ENABLESHELLMODE) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_ENABLEAUTOSYMPY) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_AUTO_SAVESETTINGS) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_SAVESETTINGS) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_DELSETTINGSFILE) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_EXECSTARTUPSCRIPT) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_SHOWPYSLICESTUTORIAL) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_SAVEHISTORY) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_SAVEHISTORYNOW) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_CLEARHISTORY) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_EDITSTARTUPSCRIPT) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_FIND) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_FINDNEXT) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_FINDPREVIOUS) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_SHOWTOOLS) + self.parent.Bind(wx.EVT_UPDATE_UI, self.OnUpdateMenu, id=ID_HIDEFOLDINGMARGIN) + + self.parent.Bind(wx.EVT_ACTIVATE, self.OnActivate) + self.parent.Bind(wx.EVT_FIND, self.OnFindNext) + self.parent.Bind(wx.EVT_FIND_NEXT, self.OnFindNext) + self.parent.Bind(wx.EVT_FIND_CLOSE, self.OnFindClose) + + def OnShowLineNumbers(self, event): + win = wx.Window.FindFocus() + if hasattr(win, 'lineNumbers'): + win.lineNumbers = event.IsChecked() + win.setDisplayLineNumbers(win.lineNumbers) + + def OnFileNew(self, event): + self.bufferNew() + + def OnFileOpen(self, event): + self.bufferOpen() + + def OnFileRevert(self, event): + self.bufferRevert() + + def OnFileClose(self, event): + self.bufferClose() + + def OnFileSave(self, event): + self.bufferSave() + + def OnFileSaveAs(self, event): + self.bufferSaveAs() + + def OnFileSaveACopy(self, event): + self.bufferSaveACopy() + + def OnFileUpdateNamespace(self, event): + self.updateNamespace() + + def OnFilePrint(self, event): + self.bufferPrint() + + def OnExit(self, event): + self.parent.Close(False) + + def OnUndo(self, event): + win = wx.Window.FindFocus() + win.Undo() + + def OnRedo(self, event): + win = wx.Window.FindFocus() + win.Redo() + + def OnCut(self, event): + win = wx.Window.FindFocus() + win.Cut() + + def OnCopy(self, event): + win = wx.Window.FindFocus() + win.Copy() + + def OnCopyPlus(self, event): + win = wx.Window.FindFocus() + win.CopyWithPrompts() + + def OnPaste(self, event): + win = wx.Window.FindFocus() + win.Paste() + + def OnPastePlus(self, event): + win = wx.Window.FindFocus() + win.PasteAndRun() + + def OnClear(self, event): + win = wx.Window.FindFocus() + win.Clear() + + def OnEmptyBuffer(self, event): + win = wx.Window.FindFocus() + d = wx.MessageDialog(self, + "Are you sure you want to clear the edit buffer,\n" + "deleting all the text?", + "Empty Buffer", wx.OK | wx.CANCEL | wx.ICON_QUESTION) + answer = d.ShowModal() + d.Destroy() + if (answer == wx.ID_OK): + win.ClearAll() + if hasattr(win,'prompt'): + win.prompt() + + def OnSelectAll(self, event): + win = wx.Window.FindFocus() + win.SelectAll() + + def OnAbout(self, event): + """Display an About window.""" + title = 'About' + text = 'Your message here.' + dialog = wx.MessageDialog(self.parent, text, title, + wx.OK | wx.ICON_INFORMATION) + dialog.ShowModal() + dialog.Destroy() + + def OnHelp(self, event): + """Display a Help window.""" + title = 'Help' + text = "Type 'shell.help()' in the shell window." + dialog = wx.MessageDialog(self.parent, text, title, + wx.OK | wx.ICON_INFORMATION) + dialog.ShowModal() + dialog.Destroy() + + def OnAutoCompleteShow(self, event): + win = wx.Window.FindFocus() + win.autoComplete = event.IsChecked() + + def OnAutoCompleteMagic(self, event): + win = wx.Window.FindFocus() + win.autoCompleteIncludeMagic = event.IsChecked() + + def OnAutoCompleteSingle(self, event): + win = wx.Window.FindFocus() + win.autoCompleteIncludeSingle = event.IsChecked() + + def OnAutoCompleteDouble(self, event): + win = wx.Window.FindFocus() + win.autoCompleteIncludeDouble = event.IsChecked() + + def OnCallTipsShow(self, event): + win = wx.Window.FindFocus() + win.autoCallTip = event.IsChecked() + + def OnCallTipsInsert(self, event): + win = wx.Window.FindFocus() + win.callTipInsert = event.IsChecked() + + def OnWrap(self, event): + win = wx.Window.FindFocus() + win.SetWrapMode(event.IsChecked()) + wx.CallLater(1, self.shell.EnsureCaretVisible) + + def OnToggleMaximize(self, event): + self.parent.Maximize(not self.parent.IsMaximized()) + + def OnSaveHistory(self, event): + self.autoSaveHistory = event.IsChecked() + + def OnSaveHistoryNow(self, event): + self.SaveHistory() + + def OnClearHistory(self, event): + self.shell.clearHistory() + + def OnEnableShellMode(self, event): + self.enableShellMode = event.IsChecked() + + def OnEnableAutoSympy(self, event): + self.enableAutoSympy = event.IsChecked() + + def OnHideFoldingMargin(self, event): + self.hideFoldingMargin = event.IsChecked() + + def OnAutoSaveSettings(self, event): + self.autoSaveSettings = event.IsChecked() + + def OnSaveSettings(self, event): + self.DoSaveSettings() + + def OnDelSettingsFile(self, event): + if self.config is not None: + d = wx.MessageDialog( + self.parent, "Do you want to revert to the default settings?\n" + + "A restart is needed for the change to take effect", + "Warning", wx.OK | wx.CANCEL | wx.ICON_QUESTION) + answer = d.ShowModal() + d.Destroy() + if (answer == wx.ID_OK): + self.config.DeleteAll() + self.LoadSettings() + + + def OnEditStartupScript(self, event): + if hasattr(self, 'EditStartupScript'): + self.EditStartupScript() + + def OnExecStartupScript(self, event): + self.execStartupScript = event.IsChecked() + self.SaveSettings(force=True) + + def OnShowPySlicesTutorial(self,event): + self.showPySlicesTutorial = event.IsChecked() + self.SaveSettings(force=True) + + def OnFindText(self, event): + if self.findDlg is not None: + return + win = wx.Window.FindFocus() + if self.shellName == 'PyCrust': + self.findDlg = wx.FindReplaceDialog(win, self.findData, + "Find",wx.FR_NOWHOLEWORD) + else: + self.findDlg = wx.FindReplaceDialog(win, self.findData, + "Find & Replace", wx.FR_NOWHOLEWORD|wx.FR_REPLACEDIALOG) + self.findDlg.Show() + + def OnFindNext(self, event,backward=False): + if backward and (self.findData.GetFlags() & wx.FR_DOWN): + self.findData.SetFlags( self.findData.GetFlags() ^ wx.FR_DOWN ) + elif not backward and not (self.findData.GetFlags() & wx.FR_DOWN): + self.findData.SetFlags( self.findData.GetFlags() ^ wx.FR_DOWN ) + + if not self.findData.GetFindString(): + self.OnFindText(event) + return + if isinstance(event, wx.FindDialogEvent): + win = self.findDlg.GetParent() + else: + win = wx.Window.FindFocus() + win.DoFindNext(self.findData, self.findDlg) + if self.findDlg is not None: + self.OnFindClose(None) + + def OnFindPrevious(self, event): + self.OnFindNext(event,backward=True) + + def OnFindClose(self, event): + self.findDlg.Destroy() + self.findDlg = None + + def OnToggleTools(self, event): + self.ToggleTools() + + + def OnUpdateMenu(self, event): + """Update menu items based on current status and context.""" + win = wx.Window.FindFocus() + id = event.GetId() + event.Enable(True) + try: + if id == ID_NEW: + event.Enable(hasattr(self, 'bufferNew')) + elif id == ID_OPEN: + event.Enable(hasattr(self, 'bufferOpen')) + elif id == ID_REVERT: + event.Enable(hasattr(self, 'bufferRevert') + and self.hasBuffer()) + elif id == ID_CLOSE: + event.Enable(hasattr(self, 'bufferClose') + and self.hasBuffer()) + elif id == ID_SAVE: + event.Enable(hasattr(self, 'bufferSave') + and self.bufferHasChanged()) + elif id == ID_SAVEAS: + event.Enable(hasattr(self, 'bufferSaveAs') + and self.hasBuffer()) + elif id == ID_SAVEACOPY: + event.Enable(hasattr(self, 'bufferSaveACopy') + and self.hasBuffer()) + elif id == ID_NAMESPACE: + event.Enable(hasattr(self, 'updateNamespace') + and self.hasBuffer()) + elif id == ID_PRINT: + event.Enable(hasattr(self, 'bufferPrint') + and self.hasBuffer()) + elif id == ID_UNDO: + event.Enable(win.CanUndo()) + elif id == ID_REDO: + event.Enable(win.CanRedo()) + elif id == ID_CUT: + event.Enable(win.CanCut()) + elif id == ID_COPY: + event.Enable(win.CanCopy()) + elif id == ID_COPY_PLUS: + event.Enable(win.CanCopy() and hasattr(win, 'CopyWithPrompts')) + elif id == ID_PASTE: + event.Enable(win.CanPaste()) + elif id == ID_PASTE_PLUS: + event.Enable(win.CanPaste() and hasattr(win, 'PasteAndRun')) + elif id == ID_CLEAR: + event.Enable(win.CanCut()) + elif id == ID_SELECTALL: + event.Enable(hasattr(win, 'SelectAll')) + elif id == ID_EMPTYBUFFER: + event.Enable(hasattr(win, 'ClearAll') and not win.GetReadOnly()) + elif id == ID_AUTOCOMP_SHOW: + event.Check(win.autoComplete) + elif id == ID_AUTOCOMP_MAGIC: + event.Check(win.autoCompleteIncludeMagic) + elif id == ID_AUTOCOMP_SINGLE: + event.Check(win.autoCompleteIncludeSingle) + elif id == ID_AUTOCOMP_DOUBLE: + event.Check(win.autoCompleteIncludeDouble) + elif id == ID_CALLTIPS_SHOW: + event.Check(win.autoCallTip) + elif id == ID_CALLTIPS_INSERT: + event.Check(win.callTipInsert) + elif id == ID_WRAP: + event.Check(win.GetWrapMode()) + + elif id == ID_SHOW_LINENUMBERS: + event.Check(win.lineNumbers) + elif id == ID_ENABLESHELLMODE: + event.Check(self.enableShellMode) + event.Enable(self.config is not None) + elif id == ID_ENABLEAUTOSYMPY: + event.Check(self.enableAutoSympy) + event.Enable(self.config is not None) + elif id == ID_AUTO_SAVESETTINGS: + event.Check(self.autoSaveSettings) + event.Enable(self.config is not None) + elif id == ID_SAVESETTINGS: + event.Enable(self.config is not None and + hasattr(self, 'DoSaveSettings')) + elif id == ID_DELSETTINGSFILE: + event.Enable(self.config is not None) + + elif id == ID_EXECSTARTUPSCRIPT: + event.Check(self.execStartupScript) + event.Enable(self.config is not None) + + elif id == ID_SAVEHISTORY: + event.Check(self.autoSaveHistory) + event.Enable(self.dataDir is not None) + elif id == ID_SAVEHISTORYNOW: + event.Enable(self.dataDir is not None and + hasattr(self, 'SaveHistory')) + elif id == ID_CLEARHISTORY: + event.Enable(self.dataDir is not None) + + elif id == ID_EDITSTARTUPSCRIPT: + event.Enable(hasattr(self, 'EditStartupScript')) + event.Enable(self.dataDir is not None) + + elif id == ID_FIND: + event.Enable(hasattr(win, 'DoFindNext')) + elif id == ID_FINDNEXT: + event.Enable(hasattr(win, 'DoFindNext')) + elif id == ID_FINDPREVIOUS: + event.Enable(hasattr(win, 'DoFindNext')) + + elif id == ID_SHOWTOOLS: + event.Check(self.ToolsShown()) + + elif id == ID_HIDEFOLDINGMARGIN: + event.Check(self.hideFoldingMargin) + event.Enable(self.config is not None) + + else: + event.Enable(False) + except AttributeError: + # This menu option is not supported in the current context. + event.Enable(False) + + + def OnActivate(self, event): + """ + Event Handler for losing the focus of the Frame. Should close + Autocomplete listbox, if shown. + """ + if not event.GetActive(): + # If autocomplete active, cancel it. Otherwise, the + # autocomplete list will stay visible on top of the + # z-order after switching to another application + win = wx.Window.FindFocus() + if hasattr(win, 'AutoCompActive') and win.AutoCompActive(): + win.AutoCompCancel() + event.Skip() + + + + def LoadSettings(self, config): + """Called by derived classes to load settings specific to the Frame""" + pos = wx.Point(config.ReadInt('Window/PosX', -1), + config.ReadInt('Window/PosY', -1)) + + size = wx.Size(config.ReadInt('Window/Width', -1), + config.ReadInt('Window/Height', -1)) + + self.SetSize(size) + self.Move(pos) + + + def SaveSettings(self, config): + """Called by derived classes to save Frame settings to a wx.Config object""" + + # TODO: track position/size so we can save it even if the + # frame is maximized or iconized. + if not self.iconized and not self.IsMaximized(): + w, h = self.GetSize() + config.WriteInt('Window/Width', w) + config.WriteInt('Window/Height', h) + + px, py = self.GetPosition() + config.WriteInt('Window/PosX', px) + config.WriteInt('Window/PosY', py) + +class KiCadEditorFrame(KiCadPyFrame): + def __init__(self, parent=None, id=-1, title='KiCad Python'): + + """Create EditorFrame instance.""" + KiCadPyFrame.__init__(self, parent) + self.buffers = {} + self.buffer = None # Current buffer. + self.editor = None + self._defaultText = title + self._statusText = self._defaultText + self.parent.SetStatusText(self._statusText) + self.parent.Bind(wx.EVT_IDLE, self.OnIdle) + self._setup() + + def _setup(self): + """Setup prior to first buffer creation. + + Useful for subclasses.""" + pass + + def setEditor(self, editor): + self.editor = editor + self.buffer = self.editor.buffer + self.buffers[self.buffer.id] = self.buffer + + def OnClose(self, event): + """Event handler for closing.""" + for buffer in self.buffers.values(): + self.buffer = buffer + if buffer.hasChanged(): + cancel = self.bufferSuggestSave() + if cancel and event.CanVeto(): + event.Veto() + return + self.parent.Destroy() + + def OnIdle(self, event): + """Event handler for idle time.""" + self._updateStatus() + if hasattr(self, 'notebook'): + self._updateTabText() + self._updateTitle() + event.Skip() + + def _updateStatus(self): + """Show current status information.""" + if self.editor and hasattr(self.editor, 'getStatus'): + status = self.editor.getStatus() + text = 'File: %s | Line: %d | Column: %d' % status + else: + text = self._defaultText + if text != self._statusText: + self.parent.SetStatusText(text) + self._statusText = text + + def _updateTabText(self): + """Show current buffer information on notebook tab.""" +## suffix = ' **' +## notebook = self.notebook +## selection = notebook.GetSelection() +## if selection == -1: +## return +## text = notebook.GetPageText(selection) +## window = notebook.GetPage(selection) +## if window.editor and window.editor.buffer.hasChanged(): +## if text.endswith(suffix): +## pass +## else: +## notebook.SetPageText(selection, text + suffix) +## else: +## if text.endswith(suffix): +## notebook.SetPageText(selection, text[:len(suffix)]) + + def _updateTitle(self): + """Show current title information.""" + title = self.GetTitle() + if self.bufferHasChanged(): + if title.startswith('* '): + pass + else: + self.SetTitle('* ' + title) + else: + if title.startswith('* '): + self.SetTitle(title[2:]) + + def hasBuffer(self): + """Return True if there is a current buffer.""" + if self.buffer: + return True + else: + return False + + def bufferClose(self): + """Close buffer.""" + if self.bufferHasChanged(): + cancel = self.bufferSuggestSave() + if cancel: + return cancel + self.bufferDestroy() + cancel = False + return cancel + + def bufferCreate(self, filename=None): + """Create new buffer.""" + self.bufferDestroy() + buffer = Buffer() + self.panel = panel = wx.Panel(parent=self, id=-1) + panel.Bind (wx.EVT_ERASE_BACKGROUND, lambda x: x) + editor = Editor(parent=panel) + panel.editor = editor + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(editor.window, 1, wx.EXPAND) + panel.SetSizer(sizer) + panel.SetAutoLayout(True) + sizer.Layout() + buffer.addEditor(editor) + buffer.open(filename) + self.setEditor(editor) + self.editor.setFocus() + self.SendSizeEvent() + + + def bufferDestroy(self): + """Destroy the current buffer.""" + if self.buffer: + for editor in self.buffer.editors.values(): + editor.destroy() + self.editor = None + del self.buffers[self.buffer.id] + self.buffer = None + self.panel.Destroy() + + + def bufferHasChanged(self): + """Return True if buffer has changed since last save.""" + if self.buffer: + return self.buffer.hasChanged() + else: + return False + + def bufferNew(self): + """Create new buffer.""" + if self.bufferHasChanged(): + cancel = self.bufferSuggestSave() + if cancel: + return cancel + self.bufferCreate() + cancel = False + return cancel + + def bufferOpen(self): + """Open file in buffer.""" + if self.bufferHasChanged(): + cancel = self.bufferSuggestSave() + if cancel: + return cancel + filedir = '' + if self.buffer and self.buffer.doc.filedir: + filedir = self.buffer.doc.filedir + result = openSingle(directory=filedir) + if result.path: + self.bufferCreate(result.path) + cancel = False + return cancel + + def bufferSave(self): + """Save buffer to its file.""" + if self.buffer.doc.filepath: + self.buffer.save() + cancel = False + else: + cancel = self.bufferSaveAs() + return cancel + + def bufferSaveAs(self): + """Save buffer to a new filename.""" + if self.bufferHasChanged() and self.buffer.doc.filepath: + cancel = self.bufferSuggestSave() + if cancel: + return cancel + filedir = '' + if self.buffer and self.buffer.doc.filedir: + filedir = self.buffer.doc.filedir + result = saveSingle(directory=filedir) + if result.path: + self.buffer.saveAs(result.path) + cancel = False + else: + cancel = True + return cancel + + def bufferSuggestSave(self): + """Suggest saving changes. Return True if user selected Cancel.""" + result = messageDialog(parent=None, + message='%s has changed.\n' + 'Would you like to save it first' + '?' % self.buffer.name, + title='Save current file?') + if result.positive: + cancel = self.bufferSave() + else: + cancel = result.text == 'Cancel' + return cancel + + def updateNamespace(self): + """Update the buffer namespace for autocompletion and calltips.""" + if self.buffer.updateNamespace(): + self.SetStatusText('Namespace updated') + else: + self.SetStatusText('Error executing, unable to update namespace') + + +class KiCadEditorNotebookFrame(KiCadEditorFrame): + def __init__(self, parent): + """Create EditorNotebookFrame instance.""" + self.notebook = None + KiCadEditorFrame.__init__(self, parent) + if self.notebook: + dispatcher.connect(receiver=self._editorChange, + signal='EditorChange', sender=self.notebook) + + def _setup(self): + """Setup prior to first buffer creation. + + Called automatically by base class during init.""" + self.notebook = EditorNotebook(parent=self) + intro = 'Py %s' % version.VERSION + import imp + module = imp.new_module('__main__') + module.__dict__['__builtins__'] = __builtins__ + namespace = module.__dict__.copy() + self.crust = crust.Crust(parent=self.notebook, intro=intro, locals=namespace) + self.shell = self.crust.shell + # Override the filling so that status messages go to the status bar. + self.crust.filling.tree.setStatusText = self.SetStatusText + # Override the shell so that status messages go to the status bar. + self.shell.setStatusText = self.SetStatusText + # Fix a problem with the sash shrinking to nothing. + self.crust.filling.SetSashPosition(200) + self.notebook.AddPage(page=self.crust, text='*Shell*', select=True) + self.setEditor(self.crust.editor) + self.crust.editor.SetFocus() + + def _editorChange(self, editor): + """Editor change signal receiver.""" + if not self: + dispatcher.disconnect(receiver=self._editorChange, + signal='EditorChange', sender=self.notebook) + return + self.setEditor(editor) + + def _updateTitle(self): + """Show current title information.""" + pass + + + def bufferCreate(self, filename=None): + """Create new buffer.""" + buffer = Buffer() + panel = wx.Panel(parent=self.notebook, id=-1) + panel.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: x) + editor = Editor(parent=panel) + panel.editor = editor + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(editor.window, 1, wx.EXPAND) + panel.SetSizer(sizer) + panel.SetAutoLayout(True) + sizer.Layout() + buffer.addEditor(editor) + buffer.open(filename) + self.setEditor(editor) + self.notebook.AddPage(page=panel, text=self.buffer.name, select=True) + self.editor.setFocus() + + def bufferDestroy(self): + """Destroy the current buffer.""" + selection = self.notebook.GetSelection() +## print("Destroy Selection:", selection) + if selection > 0: # Don't destroy the PyCrust tab. + if self.buffer: + del self.buffers[self.buffer.id] + self.buffer = None # Do this before DeletePage(). + self.notebook.DeletePage(selection) + + def bufferNew(self): + """Create new buffer.""" + self.bufferCreate() + cancel = False + return cancel + + def bufferOpen(self): + """Open file in buffer.""" + filedir = '' + if self.buffer and self.buffer.doc.filedir: + filedir = self.buffer.doc.filedir + result = openMultiple(directory=filedir) + for path in result.paths: + self.bufferCreate(path) + cancel = False + return cancel + + +class KiCadEditorNotebook(wx.Notebook): + """A notebook containing a page for each editor.""" + + def __init__(self, parent): + """Create EditorNotebook instance.""" + wx.Notebook.__init__(self, parent, id=-1, style=wx.CLIP_CHILDREN) + self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGING, self.OnPageChanging, id=self.GetId()) + self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanged, id=self.GetId()) + self.Bind(wx.EVT_IDLE, self.OnIdle) + + def OnIdle(self, event): + """Event handler for idle time.""" + self._updateTabText() + event.Skip() + + def _updateTabText(self): + """Show current buffer display name on all but first tab.""" + size = 3 + changed = ' **' + unchanged = ' --' + selection = self.GetSelection() + if selection < 1: + return + text = self.GetPageText(selection) + window = self.GetPage(selection) + if not window.editor: + return + if text.endswith(changed) or text.endswith(unchanged): + name = text[:-size] + else: + name = text + if name != window.editor.buffer.name: + text = window.editor.buffer.name + if window.editor.buffer.hasChanged(): + if text.endswith(changed): + text = None + elif text.endswith(unchanged): + text = text[:-size] + changed + else: + text += changed + else: + if text.endswith(changed): + text = text[:-size] + unchanged + elif text.endswith(unchanged): + text = None + else: + text += unchanged + if text is not None: + self.SetPageText(selection, text) + self.Refresh() # Needed on Win98. + + def OnPageChanging(self, event): + """Page changing event handler.""" + event.Skip() + + def OnPageChanged(self, event): + """Page changed event handler.""" + new = event.GetSelection() + window = self.GetPage(new) + dispatcher.send(signal='EditorChange', sender=self, + editor=window.editor) + window.SetFocus() + event.Skip() diff --git a/scripting/kicad_scripting_main.cpp b/scripting/kicad_scripting_main.cpp index 07f0db13de..d37af5a65e 100644 --- a/scripting/kicad_scripting_main.cpp +++ b/scripting/kicad_scripting_main.cpp @@ -17,16 +17,16 @@ * with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <eda_dde.h> #include <kiface_i.h> #include <kiface_ids.h> +#include <kipython_frame.h> +#include <kipython_settings.h> #include <kiway.h> #include <pgm_base.h> +#include <python_scripting.h> #include <settings/settings_manager.h> -#include <kipython_settings.h> -#include <python_scripting.h> - -#include <sstream> //-----<KIFACE>----------------------------------------------------------------- @@ -41,84 +41,15 @@ static struct IFACE : public KIFACE_I wxWindow* CreateWindow( wxWindow* aParent, int aClassId, KIWAY* aKiway, int aCtlBits = 0 ) override { - InitSettings( new KIPYTHON_SETTINGS ); - Pgm().GetSettingsManager().RegisterSettings( KifaceSettings() ); + KIPYTHON_FRAME* frame = new KIPYTHON_FRAME( aKiway, aParent ); - - // passing window ids instead of pointers is because wxPython is not - // exposing the needed c++ apis to make that possible. - std::stringstream pcbnew_pyshell_one_step; - pcbnew_pyshell_one_step << "import kicad_pyshell\n"; - pcbnew_pyshell_one_step << "import wx\n"; - pcbnew_pyshell_one_step << "\n"; - - // parent is actually *PCB_EDIT_FRAME - if( aParent ) + if( Kiface().IsSingle() ) { - pcbnew_pyshell_one_step << "parent = wx.FindWindowById( " << aParent->GetId() << " )\n"; - pcbnew_pyshell_one_step << "newshell = kicad_pyshell.makePcbnewShellWindow( parent )\n"; - } - else - { - pcbnew_pyshell_one_step << "newshell = kicad_pyshell.makePcbnewShellWindow()\n"; + // only run this under single_top, not under a project manager. + frame->CreateServer( KICAD_PY_PORT_SERVICE_NUMBER ); } - pcbnew_pyshell_one_step << "newshell.SetName( \"KiCad Shell\" )\n"; - // return value goes into a "global". It's not actually global, but rather - // the dict that is passed to PyRun_String - pcbnew_pyshell_one_step << "retval = newshell.GetId()\n"; - - // As always, first grab the GIL - PyLOCK lock; - - // Now make a dictionary to serve as the global namespace when the code is - // executed. Put a reference to the builtins module in it. - - PyObject* globals = PyDict_New(); - PyObject* builtins = PyImport_ImportModule( "builtins" ); - - wxASSERT( builtins ); - - PyDict_SetItemString( globals, "__builtins__", builtins ); - Py_DECREF( builtins ); - - // Execute the code to make the makeWindow function we defined above - PyObject* result = PyRun_String( pcbnew_pyshell_one_step.str().c_str(), Py_file_input, - globals, globals ); - - // Was there an exception? - if( !result ) - { - PyErr_Print(); - return NULL; - } - - Py_DECREF( result ); - - result = PyDict_GetItemString( globals, "retval" ); - - if( !PyLong_Check( result ) ) - { - wxLogError( "creation of scripting window didn't return a number" ); - return NULL; - } - - const long windowId = PyLong_AsLong( result ); - - // It's important not to decref globals before extracting the window id. - // If you do it early, globals, and the retval int it contains, may/will be garbage collected. - // We do not need to decref result, because GetItemString returns a borrowed reference. - Py_DECREF( globals ); - - wxWindow* window = wxWindow::FindWindowById( windowId ); - - if( !window ) - { - wxLogError( "unable to find pyshell window with id %d", windowId ); - return NULL; - } - - return window; + return frame; } /** @@ -180,6 +111,8 @@ PGM_BASE* PgmOrNull() bool IFACE::OnKifaceStart( PGM_BASE* aProgram, int aCtlBits ) { + InitSettings( new KIPYTHON_SETTINGS ); + Pgm().GetSettingsManager().RegisterSettings( KifaceSettings() ); ScriptingSetup(); return start_common( aCtlBits ); diff --git a/scripting/kipython_frame.cpp b/scripting/kipython_frame.cpp new file mode 100644 index 0000000000..d344d07882 --- /dev/null +++ b/scripting/kipython_frame.cpp @@ -0,0 +1,91 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2021 KiCad Developers, see CHANGELOG.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include <frame_type.h> +#include <kipython_frame.h> +#include <kiway_player.h> +#include <python_scripting.h> + +#include <pybind11/embed.h> +#include <sstream> +#include <wx/wx.h> +#include <pybind11/embed.h> + +void KIPYTHON_FRAME::SetupPythonEditor() +{ + + // passing window ids instead of pointers is because wxPython is not + // exposing the needed c++ apis to make that possible. + std::stringstream pcbnew_pyshell_one_step; + pcbnew_pyshell_one_step << "import kicad_pyshell\n"; + pcbnew_pyshell_one_step << "newshell = kicad_pyshell.makePcbnewShellWindow( " << GetId() << " )\n"; + + // As always, first grab the GIL + PyLOCK lock; + + // Execute the code to make the makeWindow function we defined above + PyRun_SimpleString( pcbnew_pyshell_one_step.str().c_str() ); +} + + +void KIPYTHON_FRAME::redirectStdio() +{ + // This is a helpful little tidbit to help debugging and such. It + // redirects Python's stdout and stderr to a window that will popup + // only on demand when something is printed, like a traceback. + + PyLOCK lock; + + using namespace pybind11::literals; + int id = GetId(); + auto locals = pybind11::dict( "parent_id"_a= id ); + + pybind11::exec( R"( +import sys +import wx +output = wx.PyOnDemandOutputWindow() +sys.stderr = output +parent = wx.Window.FindWindowById( parent_id ) +output.SetParent( parent ) + )", pybind11::globals(), locals ); +} + +KIPYTHON_FRAME::KIPYTHON_FRAME( KIWAY* aKiway, wxWindow* aParent ) : + KIWAY_PLAYER( aKiway, aParent, FRAME_PYTHON, wxT( "KiPython" ), wxDefaultPosition, + wxDefaultSize, KICAD_DEFAULT_DRAWFRAME_STYLE, wxT( "KiPython" ) ) +{ + m_stdio = 0; + + CallAfter( [&](){ SetupPythonEditor(); } ); + + redirectStdio(); +} + + +KIPYTHON_FRAME::~KIPYTHON_FRAME() +{ + wxWindow* stdio_window = wxWindow::FindWindowById( m_stdio ); + + if( stdio_window ) + stdio_window->Close( true ); +} diff --git a/scripting/kipython_frame.h b/scripting/kipython_frame.h new file mode 100644 index 0000000000..4bb8fc8d67 --- /dev/null +++ b/scripting/kipython_frame.h @@ -0,0 +1,63 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2021 KiCad Developers, see CHANGELOG.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef SCRIPTING_KIPYTHON_FRAME_H_ +#define SCRIPTING_KIPYTHON_FRAME_H_ + +#include <kiway_player.h> + +class wxWindow; +class APP_SETTINGS_BASE; +class KIWAY_EXPRESS; + +class KIPYTHON_FRAME : public KIWAY_PLAYER +{ + + +public: + KIPYTHON_FRAME( KIWAY* aKiway, wxWindow* aParent ); + ~KIPYTHON_FRAME() override; + + void LoadSettings( APP_SETTINGS_BASE* aCfg ) override {} + void SaveSettings( APP_SETTINGS_BASE* aCfg ) override {} + + wxWindow* GetToolCanvas() const override { return nullptr;} + + void ExecuteRemoteCommand( const char* cmdline ) override {} + + void KiwayMailIn( KIWAY_EXPRESS& aEvent ) override {} + void ProjectChanged() override {} + + void SetupPythonEditor(); +private: + + void redirectStdio(); + bool canCloseWindow( wxCloseEvent& aCloseEvent ) override { return true; } + void doCloseWindow() override {} + + long m_stdio; +}; + + + +#endif /* SCRIPTING_KIPYTHON_FRAME_H_ */ diff --git a/scripting/python_scripting.cpp b/scripting/python_scripting.cpp index e99a0b5342..1cda0a60ea 100644 --- a/scripting/python_scripting.cpp +++ b/scripting/python_scripting.cpp @@ -54,17 +54,6 @@ extern "C" PyObject* PyInit__pcbnew( void ); -#define EXTRA_PYTHON_MODULES 10 // this is the number of python - // modules that we want to add into the list - - -/* python inittab that links module names to module init functions - * we will rebuild it to include the original python modules plus - * our own ones - */ - -struct _inittab* SwigImportInittab; -static int SwigNumModules = 0; /// True if the wxPython scripting layer was successfully loaded. static bool wxPythonLoaded = false; @@ -75,70 +64,6 @@ bool IsWxPythonLoaded() return wxPythonLoaded; } - -/** - * Add a name + initfuction to our SwigImportInittab - */ - -static void swigAddModule( const char* name, PyObject* (* initfunc)() ) -{ - SwigImportInittab[SwigNumModules].name = (char*) name; - SwigImportInittab[SwigNumModules].initfunc = initfunc; - SwigNumModules++; - SwigImportInittab[SwigNumModules].name = (char*) 0; - SwigImportInittab[SwigNumModules].initfunc = 0; -} - - -/** - * Add the builtin python modules - */ -static void swigAddBuiltin() -{ - int i = 0; - - /* discover the length of the pyimport inittab */ - while( PyImport_Inittab[i].name ) - i++; - - /* allocate memory for the python module table */ - SwigImportInittab = (struct _inittab*) malloc( - sizeof( struct _inittab ) * ( i + EXTRA_PYTHON_MODULES ) ); - - /* copy all pre-existing python modules into our newly created table */ - i = 0; - - while( PyImport_Inittab[i].name ) - { - swigAddModule( PyImport_Inittab[i].name, PyImport_Inittab[i].initfunc ); - i++; - } -} - - -/** - * Add the internal modules to the python scripting so they will be available to the scripts. - */ -static void swigAddModules() -{ - swigAddModule( "_pcbnew", PyInit__pcbnew ); - - // finally it seems better to include all in just one module - // but in case we needed to include any other modules, - // it must be done like this: - // swigAddModule( "_kicad", init_kicad ); -} - - -/** - * Switch the python module table to the Pcbnew built one. - */ -static void swigSwitchPythonBuiltin() -{ - PyImport_Inittab = SwigImportInittab; -} - - PyThreadState* g_PythonMainTState; @@ -245,9 +170,7 @@ bool InitPythonScripting( const char* aStockScriptingPath, const char* aUserScri int retv; char cmd[1024]; - swigAddBuiltin(); // add builtin functions - swigAddModules(); // add our own modules - swigSwitchPythonBuiltin(); // switch the python builtin modules to our new list + PyImport_AppendInittab( "_pcbnew", PyInit__pcbnew ); #ifdef _MSC_VER // Under vcpkg/msvc, we need to explicitly set the python home @@ -266,18 +189,16 @@ bool InitPythonScripting( const char* aStockScriptingPath, const char* aUserScri Py_SetPythonHome( pyHome.GetFullPath().c_str() ); } #endif +// +// Py_Initialize(); + pybind11::initialize_interpreter(); - Py_Initialize(); - PySys_SetArgv( Pgm().App().argc, Pgm().App().argv ); +// PySys_SetArgv( Pgm().App().argc, Pgm().App().argv ); #if PY_VERSION_HEX < 0x03070000 // PyEval_InitThreads() is called by Py_Initialize() starting with version 3.7 PyEval_InitThreads(); #endif // if PY_VERSION_HEX < 0x03070000 -#if defined( DEBUG ) - RedirectStdio(); -#endif - wxPythonLoaded = true; // Save the current Python thread state and release the @@ -374,7 +295,7 @@ static void RunPythonMethodWithReturnedString( const char* aMethodName, wxString void FinishPythonScripting() { PyEval_RestoreThread( g_PythonMainTState ); - Py_Finalize(); + pybind11::finalize_interpreter(); } @@ -426,25 +347,6 @@ void UpdatePythonEnvVar( const wxString& aVar, const wxString& aValue ) wxLogError( "Python error %d occurred running command:\n\n`%s`", retv, cmd ); } -void RedirectStdio() -{ - // This is a helpful little tidbit to help debugging and such. It - // redirects Python's stdout and stderr to a window that will popup - // only on demand when something is printed, like a traceback. - const char* python_redirect = - "import sys\n" - "import wx\n" - "output = wx.PyOnDemandOutputWindow()\n" - "sys.stderr = output\n"; - - PyLOCK lock; - - int retv = PyRun_SimpleString( python_redirect ); - - if( retv != 0 ) - wxLogError( "Python error %d occurred running command:\n\n`%s`", retv, python_redirect ); -} - wxString PyStringToWx( PyObject* aString ) {