Skip to content

Instantly share code, notes, and snippets.

@hanya
Last active December 29, 2016 16:12
Show Gist options
  • Save hanya/08591a20a6c76bf9215f8175bd46681b to your computer and use it in GitHub Desktop.
Save hanya/08591a20a6c76bf9215f8175bd46681b to your computer and use it in GitHub Desktop.
KiCAD macro to add macros executor window to PyShell of Pcbnew

KiCAD macro to add macros executor window to PyShell of Pcbnew

This macro works on Python Shell provided by KiCAD 4.1 or later. 4.0.X can not be supported.

Python Shell is pretty good to execute your line of code provided by Pcbnew. But it is hard to execute long macro many times in the interactive mode. This macro provides way to execute your macros from files stored on your specified directory.

Prepare macros in selected directory and specify it in _KIMACROS variable on Preferences - Configure Paths dialog. Copy content of pyshell_hack.py file into your PyShell_pcbnew_startup.py file which can be found through PyShell - Options - Startup - Edit Startup Script... entry.

####
import wx
ID_DIRECTORY, ID_RELOAD, ID_EXECUTE = 1000, 1001, 1002
class _MacroWindowBase(wx.Window):
# size = wx.Size( 200,250 ),
def __init__(self, parent):
wx.Window.__init__ ( self, parent, id = wx.ID_ANY,
pos = wx.DefaultPosition, style = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER )
bSizer3 = wx.BoxSizer( wx.VERTICAL )
self.m_toolBar1 = wx.ToolBar( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TB_HORIZONTAL )
self.m_tool1 = self.m_toolBar1.AddLabelTool( ID_DIRECTORY, u"Change Directory", wx.ArtProvider.GetBitmap( wx.ART_FILE_OPEN, ), wx.NullBitmap, wx.ITEM_NORMAL, u"Change Directory", wx.EmptyString, None )
self.m_tool2 = self.m_toolBar1.AddLabelTool( ID_RELOAD, u"Reload", wx.ArtProvider.GetBitmap(wx.ART_GO_BACK,), wx.NullBitmap, wx.ITEM_NORMAL, u"Reload", wx.EmptyString, None )
self.m_toolBar1.AddSeparator()
self.m_tool3 = self.m_toolBar1.AddLabelTool( ID_EXECUTE, u"Execute", wx.ArtProvider.GetBitmap( wx.ART_EXECUTABLE_FILE, ), wx.NullBitmap, wx.ITEM_NORMAL, u"Execute", wx.EmptyString, None )
self.m_toolBar1.Realize()
bSizer3.Add( self.m_toolBar1, 0, wx.EXPAND, 5 )
m_MacrosListBoxChoices = []
self.m_MacrosListBox = wx.ListBox( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_MacrosListBoxChoices, wx.LB_SINGLE )
self.m_MacrosListBox.SetMinSize( wx.Size( 180,150 ) )
bSizer3.Add( self.m_MacrosListBox, 1, wx.ALL|wx.EXPAND, 5 )
self.SetSizer( bSizer3 )
self.Layout()
self.Centre( wx.BOTH )
self.Bind( wx.EVT_CLOSE, self.OnClose )
self.Bind( wx.EVT_TOOL, self.OnToolClicked, id = self.m_tool1.GetId() )
self.Bind( wx.EVT_TOOL, self.OnToolClicked, id = self.m_tool2.GetId() )
self.Bind( wx.EVT_TOOL, self.OnToolClicked, id = self.m_tool3.GetId() )
self.m_MacrosListBox.Bind( wx.EVT_KEY_DOWN, self.OnKeyDownMacrosListBox )
self.m_MacrosListBox.Bind( wx.EVT_LEFT_DCLICK, self.OnLeftDClickMacrosListBox )
def __del__( self ):
pass
# Virtual event handlers, overide them in your derived class
def OnClose( self, event ):
event.Skip()
def OnToolClicked( self, event ):
event.Skip()
def OnKeyDownMacrosListBox( self, event ):
event.Skip()
def OnLeftDClickMacrosListBox( self, event ):
event.Skip()
import os
import os.path
import sys
import traceback
import json
import weakref
import threading
try:
from UserDict import UserDict
except:
from collections import UserDict
class MacroSettings:
""" Macro settings manager. """
class Settings(UserDict):
def __init__(self, parent, key, initialdata={}):
UserDict.__init__(self, initialdata=None)
self.parent = parent
self.key = key
self.data = initialdata
def _save(self):
self.parent.save()
def __init__(self, settings_path):
self.settings_path = settings_path
self.settings = None
self.load()
def request_settings(self, key):
""" Request to obtain the configuration specified by unique key.
@param key unique key to your macro
@return dict like object that holds settings, which has
save function to store new value.
"""
settings = self.settings.get(key, None)
if settings is None:
settings = {}
self.settings[key] = settings
return self.__class__.Settings(self, key, settings)
def load(self):
if os.path.exists(self.settings_path):
try:
with open(self.settings_path) as f:
self.settings = json.load(f)
return
except:
pass
self.settings = {}
def save(self):
# ToDo needs lock among Pcbnew instances
try:
with open(self.settings_path, "w") as f:
json.dump(self.settings, f)
except Exception as e:
print(e)
import wx.py.buffer
class DummyEditor:
""" Dummy editor to provide buffer access to notebook. """
def __init__(self):
self.buffer = wx.py.buffer.Buffer()
class _MacroWindow(_MacroWindowBase):
""" Provides easy way to execute macros from the specified directory.
The path to the directory which your macros stored in can be set
by _KIMACROS variable in the main menu - Preferences - Configure Paths.
"""
ENV_NAME = "kicad_script"
SETTINGS_FILE_NAME = "macro_settings.json"
SETTINGS_KEY = "macros.py_settings_key"
POSITION_NAME = "position"
macros_path = os.environ.get("_KIMACROS", "")
settings_path = os.path.join(macros_path, SETTINGS_FILE_NAME)
# the same with the macros because of the plugin directory might be readonly
def __init__(self, parent):
_MacroWindowBase.__init__(self, parent)
self.macro_settings = MacroSettings(self.__class__.settings_path)
self.settings = self.macro_settings.request_settings(self.__class__.SETTINGS_KEY)
self.macros = None
self.modules = {}
self.set_macros_path(self.macros_path)
self.reload_macros()
self.SetPosition(self.load_initial_position())
self.editor = DummyEditor() # to avoid error on OnPageChanged
def load_initial_position(self):
""" Load last selected position in the macros list. """
key = self.__class__.POSITION_NAME
if not key in self.settings:
self.settings.update({key: (-1, -1)})
return self.settings[key]
def save_current_position(self):
""" Save last selected position in the list. """
try:
position = self.GetPosition()
self.settings[self.__class__.POSITION_NAME] = (position.x, position.y)
self.settings._save()
except Exception as e:
print(e)
def set_macros_path(self, path):
""" Set macro path. """
self.__class__.macros_path = path
if not path in sys.path:
sys.path.append(path)
def reload_macros(self):
""" Reload macro list.
Macro file has .py file extension and its name does not start with _.
"""
# ToDo support sub-directories
self.macros = []
self._clear_macros_listbox()
if not os.path.exists(self.macros_path):
return
for item in sorted(os.listdir(self.macros_path)):
if not item.startswith("_") and item.endswith(".py"):
self.macros.append(item)
self._macros_listbox_insert_items(self.macros, 0)
def exec_file(self, path):
""" Execute specified file as a macro.
@param path path to the macro file to execute
"""
try:
with open(path) as f:
exec(compile(f.read(), path, "exec"),
{"__file__": path,
"__name__": self.__class__.ENV_NAME,
"_request_settings": self.macro_settings.request_settings})
except Exception:
self.show_error(traceback.format_exc())
def execute_from_listbox(self):
""" Execute selected macro in the list. """
n = self._macros_listbox_get_selection()
if n != wx.NOT_FOUND:
path = os.path.join(self.macros_path, self.macros[n])
if os.path.exists(path):
self.exec_file(path)
else:
self.show_error("{} not found.".format(path))
def _clear_macros_listbox(self):
self.m_MacrosListBox.Clear()
def _macros_listbox_insert_items(self, items, pos):
self.m_MacrosListBox.InsertItems(items, pos)
def _macros_listbox_get_selection(self):
return self.m_MacrosListBox.GetSelection()
def show_error(self, message, caption="Error"):
wx.MessageBox(message, caption=caption, style=wx.ICON_ERROR)
# Handlers for MacroWindow events.
def OnClose( self, event ):
self.save_current_position()
self.Destroy()
def OnToolClicked( self, event ):
id = event.GetId()
if id == ID_EXECUTE:
self.execute_from_listbox()
elif id == ID_RELOAD:
self.reload_macros()
elif id == ID_DIRECTORY:
try:
dir_path = wx.DirSelector("Macro directory", self.macros_path)
if dir_path:
self.set_macros_path(dir_path)
self.reload_macros()
except Exception as e:
wx.MessageBox(str(e))
def OnKeyDownMacrosListBox( self, event ):
if event.GetKeyCode() == wx.WXK_SPACE:#wx.WXK_RETURN:
self.execute_from_listbox()
else:
event.Skip()
def OnLeftDClickMacrosListBox( self, event ):
self.execute_from_listbox()
def deregister(self):
self.OnClose(None)
def hack_shell():
""" Hacks PyShell for KiCAD - Pcbnew. """
import inspect
current_frame = inspect.currentframe()
outer_frames = inspect.getouterframes(current_frame)
try:
outer = None
for outer in outer_frames:
if outer[1].endswith("kicad_pyshell/__init__.py") and \
outer[3] == "_setup":
shell = inspect.getargvalues(outer[0]).locals["self"]
try:
shell.notebook.AddPage(
_MacroWindow(parent=shell.notebook), text="Macros", select=True)
except Exception as e:
print(e)
finally:
del shell
break
except Exception as e:
print(e)
finally:
del outer
del outer_frames
del current_frame
hack_shell()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment