From c3d3a7a4c8c03ee32e0dfe60525b73d6cd831103 Mon Sep 17 00:00:00 2001 From: Strontium Date: Mon, 21 Dec 2015 09:55:31 -0500 Subject: [PATCH] Scripting: replace PyCrust shell with enhanced PyAlaMode shell. --- pcbnew/CMakeLists.txt | 6 + pcbnew/pcbframe.cpp | 7 +- pcbnew/pcbnew.cpp | 32 ++- pcbnew/scripting/kicad_pyshell/__init__.py | 223 +++++++++++++++++++++ scripting/kicadplugins.i | 44 +++- scripting/python_scripting.cpp | 21 +- scripting/python_scripting.h | 4 +- 7 files changed, 294 insertions(+), 43 deletions(-) create mode 100644 pcbnew/scripting/kicad_pyshell/__init__.py diff --git a/pcbnew/CMakeLists.txt b/pcbnew/CMakeLists.txt index 23538d29e0..b5bc9101f5 100644 --- a/pcbnew/CMakeLists.txt +++ b/pcbnew/CMakeLists.txt @@ -679,6 +679,12 @@ if( KICAD_SCRIPTING ) DESTINATION ${KICAD_DATA}/scripting/plugins FILE_PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ ) + + # scripting python shell + install( DIRECTORY ${PROJECT_SOURCE_DIR}/pcbnew/scripting/kicad_pyshell/ + DESTINATION ${KICAD_DATA}/scripting/kicad_pyshell + FILE_PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ + ) endif() if( KICAD_SCRIPTING_MODULES ) diff --git a/pcbnew/pcbframe.cpp b/pcbnew/pcbframe.cpp index 4eee8235c8..0d641e5c02 100644 --- a/pcbnew/pcbframe.cpp +++ b/pcbnew/pcbframe.cpp @@ -70,8 +70,6 @@ #include #include -#include - #if defined(KICAD_SCRIPTING) || defined(KICAD_SCRIPTING_WXPYTHON) #include #endif @@ -984,9 +982,6 @@ void PCB_EDIT_FRAME::UpdateTitle() } -wxSize PYTHON_CONSOLE_FRAME::m_frameSize; ///< The size of the PYTHON_CONSOLE_FRAME frame, stored during a session -wxPoint PYTHON_CONSOLE_FRAME::m_framePos; ///< The position ofPYTHON_CONSOLE_FRAME the frame, stored during a session - void PCB_EDIT_FRAME::ScriptingConsoleEnableDisable( wxCommandEvent& aEvent ) { @@ -994,7 +989,7 @@ void PCB_EDIT_FRAME::ScriptingConsoleEnableDisable( wxCommandEvent& aEvent ) bool pythonPanelShown = true; if( pythonPanelFrame == NULL ) - pythonPanelFrame = new PYTHON_CONSOLE_FRAME( this, pythonConsoleNameId() ); + pythonPanelFrame = CreatePythonShellWindow( this, pythonConsoleNameId() ); else pythonPanelShown = ! pythonPanelFrame->IsShown(); diff --git a/pcbnew/pcbnew.cpp b/pcbnew/pcbnew.cpp index 0abbd00038..2277c654d7 100644 --- a/pcbnew/pcbnew.cpp +++ b/pcbnew/pcbnew.cpp @@ -241,36 +241,26 @@ static bool scriptingSetup() } } - // TODO: make this path definable by the user, and set more than one path - // (and remove the fixed paths from /scripting/kicadplugins.i) - - // wizard plugins are stored in kicad/bin/plugins. - // so add this path to python scripting default search paths + // wizard plugins are stored in ../share/kicad/scripting/plugins. + // so add the base scripting path to python scripting default search paths // which are ( [KICAD_PATH] is an environment variable to define) + // [KICAD_PATH]/scripting // [KICAD_PATH]/scripting/plugins // Add this default search path: - path_frag = Pgm().GetExecutablePath() + wxT( "../share/kicad/scripting/plugins" ); + path_frag = Pgm().GetExecutablePath() + wxT( "../share/kicad/scripting" ); #elif defined( __WXMAC__ ) - // TODO: - // For scripting currently only the bundle scripting path and the path - // defined by $(KICAD_PATH)/scripting/plugins is defined. - // These paths are defined here and in kicadplugins.i - // In future, probably more paths are of interest: - // * User folder (~/Library/Application Support/kicad/scripting/plugins) - // => GetOSXKicadUserDataDir() + wxT( "/scripting/plugins" ); - // * Machine folder (/Library/Application Support/kicad/scripting/plugins) - // => GetOSXKicadMachineDataDir() + wxT( "/scripting/plugins" ); // This path is given to LoadPlugins() from kicadplugins.i, which - // only supports one path. Only use bundle scripting path for now. - path_frag = GetOSXKicadDataDir() + wxT( "/scripting/plugins" ); + // only supports one path, the bundle scripting path for now. + // All other paths are determined by the pcbnew.py initialisation code + path_frag = GetOSXKicadDataDir() + wxT( "/scripting" ); // Add default paths to PYTHONPATH wxString pypath; - // Bundle scripting folder (/Contents/SharedSupport/scripting/plugins) - pypath += GetOSXKicadDataDir() + wxT( "/scripting/plugins" ); + // Bundle scripting folder (/Contents/SharedSupport/scripting) + pypath += GetOSXKicadDataDir() + wxT( "/scripting" ); // $(KICAD_PATH)/scripting/plugins is always added in kicadplugins.i if( wxGetenv("KICAD_PATH") != NULL ) @@ -303,9 +293,11 @@ static bool scriptingSetup() wxSetEnv( wxT( "PYTHONPATH" ), pypath ); // Add this default search path: - path_frag = Pgm().GetExecutablePath() + wxT( "../share/kicad/scripting/plugins" ); + path_frag = Pgm().GetExecutablePath() + wxT( "../share/kicad/scripting" ); #endif + // path_frag is the path to the bundled scripts and plugins, all other paths are + // determined by the python pcbnew.py initialisation code. if( !pcbnewInitPythonScripting( TO_UTF8( path_frag ) ) ) { wxLogError( wxT( "pcbnewInitPythonScripting() failed." ) ); diff --git a/pcbnew/scripting/kicad_pyshell/__init__.py b/pcbnew/scripting/kicad_pyshell/__init__.py new file mode 100644 index 0000000000..7da0005784 --- /dev/null +++ b/pcbnew/scripting/kicad_pyshell/__init__.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +"""KiCad Python Shell. + +This module provides the python shell for KiCad. + +Currently the shell is only available inside PCBNEW. + +PCBNEW starts the shell once, by calling makePcbnewShellWindow() the +first time it is opened, subsequently the shell window is just hidden +or shown, as per user requirements. + +IF makePcbnewShellWindow() is called again, a second/third shell window +can be created. PCBNEW does not do this, but a user may call this from +the first shell if they require. + +""" +import wx +import sys +import os + +from wx.py import crust, editor, version, dispatcher +from wx.py.editor import EditorNotebook + +import pcbnew + +INTRO = "KiCAD:PCBNEW - Python Shell - PyAlaMode %s" % version.VERSION + + +class PcbnewPyShell(editor.EditorNotebookFrame): + + """The Pythonshell of PCBNEW.""" + + def _setup_startup(self): + """Initialise the startup script.""" + # Create filename for startup script. + self.startup_file = os.path.join(self.config_dir, + "PyShell_pcbnew_startup.py") + self.execStartupScript = True + + # Check if startup script exists + if not os.path.isfile(self.startup_file): + # Not, so create a default. + default_startup = open(self.startup_file, 'w') + # provide the content for the default startup file. + default_startup.write( + "### DEFAULT STARTUP FILE FOR KiCad:PCBNEW Python Shell\n" + + "# Enter any Python code you would like to execute when" + + " the PCBNEW python shell first runs.\n" + + "\n" + + "# Eg:\n" + + "\n" + + "# import pcbnew\n" + + "# board = pcbnew.GetBoard()\n") + default_startup.close() + + 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__') + import __builtin__ + module.__dict__['__builtins__'] = __builtin__ + namespace = module.__dict__.copy() + + self.config_dir = pcbnew.GetKicadConfigPath() + self.dataDir = self.config_dir + + self._setup_startup() + self.history_file = os.path.join(self.config_dir, + "PyShell_pcbnew.history") + + self.config_file = os.path.join(self.config_dir, + "PyShell_pcbnew.cfg") + self.config = wx.FileConfig(localFilename=self.config_file) + self.config.SetRecordDefaults(True) + self.autoSaveSettings = False + self.autoSaveHistory = False + self.LoadSettings() + + self.crust = crust.Crust(parent=self.notebook, + intro=intro, locals=namespace, + rootLabel="locals()", + startupScript=self.startup_file, + execStartupScript=self.execStartupScript) + + 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() + + self.LoadHistory() + + def OnAbout(self, event): + """Display an About window.""" + title = 'About : KiCad:PCBNEW - Python Shell' + text = "Enahnced Python Shell for KiCad:PCBNEW\n\n" + \ + "This KiCad Python Shell is based on wxPython PyAlaMode.\n\n" + \ + "see: http://wiki.wxpython.org/PyAlaMode\n\n" + \ + "KiCad Revision: %s\n" % "??.??" + \ + "PyAlaMode Revision : %s\n" % version.VERSION + \ + "Platform: %s\n" % sys.platform + \ + "Python Version: %s\n" % sys.version.split()[0] + \ + "wxPython Version: %s\n" % wx.VERSION_STRING + \ + ("\t(%s)\n" % ", ".join(wx.PlatformInfo[1:])) + + dialog = wx.MessageDialog(self, text, title, + wx.OK | wx.ICON_INFORMATION) + dialog.ShowModal() + dialog.Destroy() + + def EditStartupScript(self): + """Open a Edit buffer of the startup script file.""" + self.bufferCreate(filename=self.startup_file) + + def LoadSettings(self): + """Load settings for the shell.""" + if self.config is not None: + editor.EditorNotebookFrame.LoadSettings(self, self.config) + self.autoSaveSettings = \ + self.config.ReadBool('Options/AutoSaveSettings', False) + self.execStartupScript = \ + self.config.ReadBool('Options/ExecStartupScript', True) + self.autoSaveHistory = \ + self.config.ReadBool('Options/AutoSaveHistory', False) + self.hideFoldingMargin = \ + self.config.ReadBool('Options/HideFoldingMargin', True) + + def SaveSettings(self, force=False): + """ + Save settings for the shell. + + Arguments: + + force -- False - Autosaving. True - Manual Saving. + """ + if self.config is not None: + # always save these + self.config.WriteBool('Options/AutoSaveSettings', + self.autoSaveSettings) + if self.autoSaveSettings or force: + editor.EditorNotebookFrame.SaveSettings(self, self.config) + + self.config.WriteBool('Options/AutoSaveHistory', + self.autoSaveHistory) + self.config.WriteBool('Options/ExecStartupScript', + self.execStartupScript) + self.config.WriteBool('Options/HideFoldingMargin', + self.hideFoldingMargin) + if self.autoSaveHistory: + self.SaveHistory() + + def DoSaveSettings(self): + """Menu function to trigger saving the shells settings.""" + if self.config is not None: + self.SaveSettings(force=True) + self.config.Flush() + + def SaveHistory(self): + """Save shell history to the shell history file.""" + if self.dataDir: + try: + name = self.history_file + f = file(name, 'w') + hist = [] + enc = wx.GetDefaultPyEncoding() + for h in self.shell.history: + if isinstance(h, unicode): + h = h.encode(enc) + hist.append(h) + hist = '\x00\n'.join(hist) + f.write(hist) + f.close() + except: + d = wx.MessageDialog(self, "Error saving history file.", + "Error", wx.ICON_EXCLAMATION | wx.OK) + d.ShowModal() + d.Destroy() + raise + + def LoadHistory(self): + """Load shell history from the shell history file.""" + if self.dataDir: + name = self.history_file + if os.path.exists(name): + try: + f = file(name, 'U') + hist = f.read() + f.close() + self.shell.history = hist.split('\x00\n') + dispatcher.send(signal="Shell.loadHistory", + history=self.shell.history) + except: + d = wx.MessageDialog(self, + "Error loading history file!", + "Error", wx.ICON_EXCLAMATION | wx.OK) + d.ShowModal() + d.Destroy() + + +def makePcbnewShellWindow(parent=None): + """ + Create a new Shell Window and return its handle. + + Arguments: + parent -- The parent window to attach to. + + Returns: + The handle to the new window. + """ + pyshell = PcbnewPyShell(parent, id=-1, title=INTRO) + pyshell.Show() + return pyshell diff --git a/scripting/kicadplugins.i b/scripting/kicadplugins.i index 9a310faa59..918896d15d 100644 --- a/scripting/kicadplugins.i +++ b/scripting/kicadplugins.i @@ -78,21 +78,59 @@ def ReloadPlugins(): ReloadPlugin(k) -def LoadPlugins(plugpath): +def LoadPlugins(bundlepath=None): + """ + Initialise Scripting/Plugin python environment and load plugins. + + Arguments: + scriptpath -- The path to the bundled scripts. + The bunbled Plugins are relative to this path, in the + "plugins" subdirectory. + + NOTE: These are all of the possible "default" search paths for kicad + python scripts. These paths will ONLY be added to the python + search path ONLY IF they already exist. + + The Scripts bundled with the KiCad installation: + / + /plugins/ + + The Scripts relative to the KiCad search path environment variable: + [KICAD_PATH]/scripting/ + [KICAD_PATH]/scripting/plugins/ + + The Scripts relative to the KiCad Users configuration: + /scripting/ + /scripting/plugins/ + + And on Linux ONLY, extra paths relative to the users home directory: + ~/.kicad_plugins/ + ~/.kicad/scripting/ + ~/.kicad/scripting/plugins/ + """ import os import sys + import pcbnew kicad_path = os.environ.get('KICAD_PATH') + config_path = pcbnew.GetKicadConfigPath() plugin_directories=[] - if plugpath: - plugin_directories.append(plugpath) + if bundlepath: + plugin_directories.append(bundlepath) + plugin_directories.append(os.path.join(bundlepath, 'plugins')) if kicad_path: + plugin_directories.append(os.path.join(kicad_path, 'scripting')) plugin_directories.append(os.path.join(kicad_path, 'scripting', 'plugins')) + if config_path: + plugin_directories.append(os.path.join(config_path, 'scripting')) + plugin_directories.append(os.path.join(config_path, 'scripting', 'plugins')) + if sys.platform.startswith('linux'): plugin_directories.append(os.environ['HOME']+'/.kicad_plugins/') + plugin_directories.append(os.environ['HOME']+'/.kicad/scripting/') plugin_directories.append(os.environ['HOME']+'/.kicad/scripting/plugins/') for plugins_dir in plugin_directories: diff --git a/scripting/python_scripting.cpp b/scripting/python_scripting.cpp index c74abf5ed5..f2b3d22de0 100644 --- a/scripting/python_scripting.cpp +++ b/scripting/python_scripting.cpp @@ -136,7 +136,7 @@ static void swigSwitchPythonBuiltin() PyThreadState* g_PythonMainTState; -bool pcbnewInitPythonScripting( const char * aUserPluginsPath ) +bool pcbnewInitPythonScripting( const char * aUserScriptingPath ) { swigAddBuiltin(); // add builtin functions swigAddModules(); // add our own modules @@ -191,7 +191,7 @@ bool pcbnewInitPythonScripting( const char * aUserPluginsPath ) snprintf( cmd, sizeof(cmd), "import sys, traceback\n" "sys.path.append(\".\")\n" "import pcbnew\n" - "pcbnew.LoadPlugins(\"%s\")", aUserPluginsPath ); + "pcbnew.LoadPlugins(\"%s\")", aUserScriptingPath ); PyRun_SimpleString( cmd ); } @@ -227,20 +227,15 @@ void RedirectStdio() } -wxWindow* CreatePythonShellWindow( wxWindow* parent ) +wxWindow* CreatePythonShellWindow( wxWindow* parent, const wxString& aFramenameId ) { - const char* pycrust_panel = - "import wx\n" - "from wx.py import shell, version\n" - "\n" - "intro = \"PyCrust %s - KiCAD Python Shell\" % version.VERSION\n" + const char* pcbnew_pyshell = + "import kicad_pyshell\n" "\n" "def makeWindow(parent):\n" - " pycrust = shell.Shell(parent, -1, introText=intro)\n" - " return pycrust\n" + " return kicad_pyshell.makePcbnewShellWindow(parent)\n" "\n"; - wxWindow* window = NULL; PyObject* result; @@ -257,7 +252,7 @@ wxWindow* CreatePythonShellWindow( wxWindow* parent ) Py_DECREF( builtins ); // Execute the code to make the makeWindow function we defined above - result = PyRun_String( pycrust_panel, Py_file_input, globals, globals ); + result = PyRun_String( pcbnew_pyshell, Py_file_input, globals, globals ); // Was there an exception? if( !result ) @@ -297,6 +292,8 @@ wxWindow* CreatePythonShellWindow( wxWindow* parent ) wxASSERT_MSG( success, _T( "Returned object was not a wxWindow!" ) ); Py_DECREF( result ); + + window->SetName( aFramenameId ); } // Release the python objects we still have diff --git a/scripting/python_scripting.h b/scripting/python_scripting.h index 8d0ae174fe..ca25de435d 100644 --- a/scripting/python_scripting.h +++ b/scripting/python_scripting.h @@ -24,14 +24,14 @@ * Initializes the Python engine inside pcbnew */ -bool pcbnewInitPythonScripting( const char * aUserPluginsPath ); +bool pcbnewInitPythonScripting( const char * aUserScriptingPath ); void pcbnewFinishPythonScripting(); #ifdef KICAD_SCRIPTING_WXPYTHON void RedirectStdio(); -wxWindow* CreatePythonShellWindow( wxWindow* parent ); +wxWindow* CreatePythonShellWindow( wxWindow* parent, const wxString& aFramenameId ); class PyLOCK {