1# -*- coding: utf-8 -*- 2"""KiCad Python Shell. 3 4This module provides the python shell for KiCad. 5 6KiCad starts the shell once, by calling makePcbnewShellWindow() the 7first time it is opened, subsequently the shell window is just hidden 8or shown, as per user requirements. 9 10IF makePcbnewShellWindow() is called again, a second/third shell window 11can be created. 12 13Note: 14**DO NOT** import pcbnew module if not called from Pcbnew: on msys2 it creates a serious issue: 15the python script import is broken because the pcbnew application is not running, and for instance 16Pgm() returns a nullptr and Kicad crashes when Pgm is invoked. 17 18""" 19 20import wx 21import sys 22import os 23 24from wx.py import crust, version, dispatcher 25 26from .kicad_pyeditor import KiCadEditorNotebookFrame 27from .kicad_pyeditor import KiCadEditorNotebook 28 29class KiCadPyShell(KiCadEditorNotebookFrame): 30 def __init__(self, parent): 31 # Search if a pcbnew frame is open, because import pcbnew can be made only if it exists. 32 # frame names are "SchematicFrame" and "PcbFrame" 33 # 34 # Note we must do this before the KiCadEditorNotebookFrame __init__ call because it ends up calling _setup back on us 35 frame = wx.FindWindowByName( "PcbFrame" ) 36 37 self.isPcbframe = frame is not None 38 39 KiCadEditorNotebookFrame.__init__(self, parent) 40 41 def _setup_startup(self): 42 if self.config_dir != "": 43 """Initialise the startup script.""" 44 # Create filename for startup script. 45 self.startup_file = os.path.join(self.config_dir, 46 "PyShell_pcbnew_startup.py") 47 self.execStartupScript = True 48 49 # Check if startup script exists 50 if not os.path.isfile(self.startup_file): 51 # Not, so try to create a default. 52 try: 53 default_startup = open(self.startup_file, 'w') 54 # provide the content for the default startup file. 55 default_startup.write( 56 "### DEFAULT STARTUP FILE FOR KiCad Python Shell\n" + 57 "# Enter any Python code you would like to execute when" + 58 " the PCBNEW python shell first runs.\n" + 59 "\n" + 60 "# For example, uncomment the following lines to import the current board\n" + 61 "\n" + 62 "# import pcbnew\n" + 63 "# import eeschema\n" + 64 "# board = pcbnew.GetBoard()\n" + 65 "# sch = eeschema.GetSchematic()\n") 66 default_startup.close() 67 except: 68 pass 69 else: 70 self.startup_file = "" 71 self.execStartupScript = False 72 73 def _setup(self): 74 """ 75 Setup prior to first buffer creation. 76 77 Called automatically by base class during init. 78 """ 79 self.notebook = KiCadEditorNotebook(parent=self.parent) 80 intro = 'Py %s' % version.VERSION 81 import types 82 import builtins 83 module = types.ModuleType('__main__') 84 module.__dict__['__builtins__'] = builtins 85 namespace = module.__dict__.copy() 86 87 ''' 88 Import pcbnew **only** if the board editor exists, to avoid strange behavior if not. 89 pcbnew.SETTINGS_MANAGER should be in fact called only if the python console is created 90 from the board editor, and if created from schematic editor, should use something like 91 eeschema.SETTINGS_MANAGER 92 ''' 93 if self.isPcbframe: 94 import pcbnew 95 self.config_dir = pcbnew.SETTINGS_MANAGER.GetUserSettingsPath() 96 else: 97 self.config_dir = "" 98 99 self.dataDir = self.config_dir 100 101 self._setup_startup() 102 self.history_file = os.path.join(self.config_dir, 103 "PyShell_pcbnew.history") 104 105 self.config = None 106 # self.config_file = os.path.join(self.config_dir, 107 # "PyShell_pcbnew.cfg") 108 # self.config = wx.FileConfig(localFilename=self.config_file) 109 # self.config.SetRecordDefaults(True) 110 111 self.autoSaveSettings = False 112 self.autoSaveHistory = False 113 self.LoadSettings() 114 115 self.crust = crust.Crust(parent=self.notebook, 116 intro=intro, locals=namespace, 117 rootLabel="locals()", 118 startupScript=self.startup_file, 119 execStartupScript=self.execStartupScript) 120 121 self.crust._CheckShouldSplit() 122 self.shell = self.crust.shell 123 # Override the filling so that status messages go to the status bar. 124 self.crust.filling.tree.setStatusText = self.parent.SetStatusText 125 # Override the shell so that status messages go to the status bar. 126 self.shell.setStatusText = self.parent.SetStatusText 127 # Fix a problem with the sash shrinking to nothing. 128 self.crust.filling.SetSashPosition(200) 129 self.notebook.AddPage(page=self.crust, text='*Shell*', select=True) 130 self.setEditor(self.crust.editor) 131 self.crust.editor.SetFocus() 132 133 self.LoadHistory() 134 135 def OnAbout(self, event): 136 """Display an About window.""" 137 title = 'About : KiCad - Python Shell' 138 text = "Enhanced Python Shell for KiCad\n\n" + \ 139 "KiCad Revision: %s\n" % "??.??" + \ 140 "Platform: %s\n" % sys.platform + \ 141 "Python Version: %s\n" % sys.version.split()[0] + \ 142 "wxPython Version: %s\n" % wx.VERSION_STRING + \ 143 ("\t(%s)\n" % ", ".join(wx.PlatformInfo[1:])) 144 145 dialog = wx.MessageDialog(self.parent, text, title, 146 wx.OK | wx.ICON_INFORMATION) 147 dialog.ShowModal() 148 dialog.Destroy() 149 150 def EditStartupScript(self): 151 """Open a Edit buffer of the startup script file.""" 152 self.bufferCreate(filename=self.startup_file) 153 154 def LoadSettings(self): 155 """Load settings for the shell.""" 156 if self.config is not None: 157 KiCadEditorNotebookFrame.LoadSettings(self.parent, self.config) 158 self.autoSaveSettings = \ 159 self.config.ReadBool('Options/AutoSaveSettings', False) 160 self.execStartupScript = \ 161 self.config.ReadBool('Options/ExecStartupScript', True) 162 self.autoSaveHistory = \ 163 self.config.ReadBool('Options/AutoSaveHistory', False) 164 self.hideFoldingMargin = \ 165 self.config.ReadBool('Options/HideFoldingMargin', True) 166 167 def SaveSettings(self, force=False): 168 """ 169 Save settings for the shell. 170 171 Arguments: 172 173 force -- False - Autosaving. True - Manual Saving. 174 """ 175 if self.config is not None: 176 # always save these 177 self.config.WriteBool('Options/AutoSaveSettings', 178 self.autoSaveSettings) 179 if self.autoSaveSettings or force: 180 KiCadEditorNotebookFrame.SaveSettings(self, self.config) 181 182 self.config.WriteBool('Options/AutoSaveHistory', 183 self.autoSaveHistory) 184 self.config.WriteBool('Options/ExecStartupScript', 185 self.execStartupScript) 186 self.config.WriteBool('Options/HideFoldingMargin', 187 self.hideFoldingMargin) 188 if self.autoSaveHistory: 189 self.SaveHistory() 190 191 def DoSaveSettings(self): 192 """Menu function to trigger saving the shells settings.""" 193 if self.config is not None: 194 self.SaveSettings(force=True) 195 self.config.Flush() 196 197 def SaveHistory(self): 198 """Save shell history to the shell history file.""" 199 if self.dataDir: 200 try: 201 name = self.history_file 202 f = file(name, 'w') 203 hist = [] 204 enc = wx.GetDefaultPyEncoding() 205 for h in self.shell.history: 206 if isinstance(h, unicode): 207 h = h.encode(enc) 208 hist.append(h) 209 hist = '\x00\n'.join(hist) 210 f.write(hist) 211 f.close() 212 except: 213 d = wx.MessageDialog(self, "Error saving history file.", 214 "Error", wx.ICON_EXCLAMATION | wx.OK) 215 d.ShowModal() 216 d.Destroy() 217 raise 218 219 def LoadHistory(self): 220 """Load shell history from the shell history file.""" 221 if self.dataDir: 222 name = self.history_file 223 if os.path.exists(name): 224 try: 225 f = file(name, 'U') 226 hist = f.read() 227 f.close() 228 self.shell.history = hist.split('\x00\n') 229 dispatcher.send(signal="Shell.loadHistory", 230 history=self.shell.history) 231 except: 232 d = wx.MessageDialog(self, 233 "Error loading history file!", 234 "Error", wx.ICON_EXCLAMATION | wx.OK) 235 d.ShowModal() 236 d.Destroy() 237 238 239def makePcbnewShellWindow(parentid): 240 """ 241 Create a new Shell Window and return its handle. 242 243 Arguments: 244 parent -- The parent window to attach to. 245 246 Returns: 247 The handle to the new window. 248 """ 249 250 parent = wx.FindWindowById( parentid ) 251 252 frmname = parent.GetName() 253 return KiCadPyShell(parent) 254