Skip to content

Instantly share code, notes, and snippets.

@hanya
Last active December 15, 2015 16:38
Show Gist options
  • Save hanya/8708d3ae57961064e912 to your computer and use it in GitHub Desktop.
Save hanya/8708d3ae57961064e912 to your computer and use it in GitHub Desktop.
Macro executor dialog for KiCAD Pcbnew

Macro executor for KiCAD Pcbnew

This macro provides the dialog which allows you to execute your Python macros easily.

How to use

Put the macros.py file in the plugin directory let the file is automatically loaded.

KICAD_PATH/scripting/plugins
# the following places can be used on Linux
~/.kicad_plugins
~/.kicad/scripting/plugins
  1. Start KiCAD program.
  2. Choose Preferences - Configure Paths entry in the main menu of the KiCAD window.
  3. Add Name: "_KIMACROS", Path: path to the directory you want to store your Python macros, such as "/mnt/hd/misc/kicadtools".
  4. If you want to show the macros dialog when Pcbnew is started, add Name: "_KIMACROSAUTOSTART", Path: "1". If Path is one of "1, True, true", the macros dialog is not shown.

If you want to show the macros dialog through the Python console, input like the following:

import macros; macros.run()

Problem

The macros dialog will not be closed even you close Pcbnew or KiCAD program. There is no way to know the closing of the KiCAD program on 4.0, it seems. I have tried to add weakref on pcbnew or board instances, but it was not the solution.

# -*- coding: utf-8 -*-
import wx
import wx.xrc
ID_DIRECTORY = 1000
ID_RELOAD = 1001
ID_EXECUTE = 1002
class _MacroDialogBase ( wx.Dialog ):
def __init__( self, parent ):
wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"Macros - Pcbnew",
pos = wx.DefaultPosition, size = wx.Size( 200,250 ), style = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER )
self.SetSizeHintsSz( wx.Size( 200,250 ), wx.DefaultSize )
bSizer3 = wx.BoxSizer( wx.VERTICAL )
bSizer3.SetMinSize( wx.Size( 200,250 ) )
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 )
# Connect Events
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)
class _MacroDialog( _MacroDialogBase ):
""" 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 ):
_MacroDialogBase.__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())
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 run():
""" Start macros dialog.
Call this function in the main menu - Tools - Scripting Console as follows:
import macros
macros.run()
"""
d = _MacroDialog(None)
d.Show()
def register():
""" Register this as plug-in. """
# If _KIMACROSAUTOSTART is defined in the main menu - Preferences - Configure Paths,
# and the value match is one of "1", "True" or "true", the macro dialog is automatically shown
# at start up of the Pcbnew window.
value = os.environ.get("_KIMACROSAUTOSTART", "0")
if value in ("1", "True", "true"):
return run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment