Documentation Index
Fetch the complete documentation index at: https://mintlify.com/KiCad/kicad-source-mirror/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Action plugins allow you to extend KiCad’s PCB editor with custom interactive tools. They appear as menu items and optional toolbar buttons, providing a way to automate repetitive tasks or add new functionality.
Plugin Architecture
Action plugins are implemented through the ACTION_PLUGIN base class defined in /pcbnew/action_plugin.h:
class ACTION_PLUGIN
{
public:
virtual ~ACTION_PLUGIN();
// Required methods to implement
virtual wxString GetCategoryName() = 0;
virtual wxString GetName() = 0;
virtual wxString GetClassName() = 0;
virtual wxString GetDescription() = 0;
virtual bool GetShowToolbarButton() = 0;
virtual wxString GetIconFileName( bool aDark ) = 0;
virtual wxString GetPluginPath() = 0;
virtual void* GetObject() = 0;
virtual void Run() = 0;
// Registration
void register_action();
// UI elements
int m_actionMenuId;
int m_actionButtonId;
wxBitmap iconBitmap;
bool show_on_toolbar;
};
Creating an Action Plugin
Basic Structure
Create a Python file that inherits from ActionPlugin:
import pcbnew
import os
class MyActionPlugin(pcbnew.ActionPlugin):
def defaults(self):
"""Define plugin metadata"""
self.name = "My Custom Tool"
self.category = "Modify PCB"
self.description = "Does something useful with the PCB"
self.show_toolbar_button = True
self.icon_file_name = os.path.join(
os.path.dirname(__file__), 'icon.png'
)
self.dark_icon_file_name = os.path.join(
os.path.dirname(__file__), 'icon_dark.png'
)
def Run(self):
"""Execute when the plugin is invoked"""
board = pcbnew.GetBoard()
# Your plugin logic here
pcbnew.Refresh() # Update display
# Register the plugin
MyActionPlugin().register()
The defaults() method sets plugin properties:
- name: Display name in menu (required)
- category: Submenu grouping (required)
- description: Tooltip and help text (required)
- show_toolbar_button: Add to toolbar (default: False)
- icon_file_name: Path to icon (24x24 PNG recommended)
- dark_icon_file_name: Icon for dark theme (optional)
Real-World Examples
Example 1: Automatic Border Creator
Based on /demos/python_scripts_examples/action_menu_add_automatic_border.py:
from pcbnew import *
class AddAutomaticBorder(ActionPlugin):
"""
Automatically creates or updates PCB edges to include all elements
"""
def defaults(self):
self.name = "Add or update automatic PCB edges"
self.category = "Modify PCB"
self.description = "Automatically add or update edges on an existing PCB"
# Offset between elements and edge (2.54mm)
self.offset = FromMM(2.54)
# Snap to grid (2.54mm)
self.grid = FromMM(2.54)
def min(self, a, b):
"""Helper to find minimum, handling None values"""
if a is None:
return b
if b is None:
return a
return a if a < b else b
def max(self, a, b):
"""Helper to find maximum, handling None values"""
if a is None:
return b
if b is None:
return a
return a if a > b else b
def Run(self):
pcb = GetBoard()
min_x = min_y = max_x = max_y = None
# Find bounding box of all zones
for i in range(pcb.GetAreaCount()):
bbox = pcb.GetArea(i).GetBoundingBox()
min_x = self.min(min_x, bbox.GetX())
min_y = self.min(min_y, bbox.GetY())
max_x = self.max(max_x, bbox.GetX() + bbox.GetWidth())
max_y = self.max(max_y, bbox.GetY() + bbox.GetHeight())
# Include all tracks
for track in pcb.GetTracks():
min_x = self.min(min_x, track.GetStart().x)
min_y = self.min(min_y, track.GetStart().y)
max_x = self.max(max_x, track.GetEnd().x)
max_y = self.max(max_y, track.GetEnd().y)
# Include all footprints
for footprint in pcb.GetFootprints():
bbox = footprint.GetBoundingBox()
min_x = self.min(min_x, bbox.GetX())
min_y = self.min(min_y, bbox.GetY())
max_x = self.max(max_x, bbox.GetX() + bbox.GetWidth())
max_y = self.max(max_y, bbox.GetY() + bbox.GetHeight())
# Add offset
min_x -= self.offset
min_y -= self.offset
max_x += self.offset
max_y += self.offset
# Snap to grid
min_x = min_x - (min_x % self.grid)
min_y = min_y - (min_y % self.grid)
if (max_x % self.grid) != 0:
max_x = max_x - (max_x % self.grid) + self.grid
if (max_y % self.grid) != 0:
max_y = max_y - (max_y % self.grid) + self.grid
# Create edge shapes
self._create_edge(pcb, min_x, min_y, min_x, max_y) # West
self._create_edge(pcb, min_x, min_y, max_x, min_y) # North
self._create_edge(pcb, max_x, min_y, max_x, max_y) # East
self._create_edge(pcb, min_x, max_y, max_x, max_y) # South
def _create_edge(self, pcb, x1, y1, x2, y2):
"""Create or update an edge line"""
edge = PCB_SHAPE()
edge.SetLayer(Edge_Cuts)
edge.SetStart(wxPoint(x1, y1))
edge.SetEnd(wxPoint(x2, y2))
pcb.Add(edge)
AddAutomaticBorder().register()
Based on /demos/python_scripts_examples/action_plugin_test_undoredo.py:
import pcbnew
import random
class GenerateRandomContent(pcbnew.ActionPlugin):
def defaults(self):
self.name = "Generate Random Test Content"
self.category = "Test Tools"
self.description = "Generate random footprints and tracks for testing"
def Run(self):
self.pcb = pcbnew.GetBoard()
random.seed()
for i in range(10):
# Create random track segments
seg = pcbnew.PCB_SHAPE()
seg.SetLayer(random.choice([
pcbnew.Edge_Cuts,
pcbnew.Cmts_User,
pcbnew.Eco1_User
]))
seg.SetStart(pcbnew.VECTOR2I_MM(
random.randint(10, 100),
random.randint(10, 100)
))
seg.SetEnd(pcbnew.VECTOR2I_MM(
random.randint(10, 100),
random.randint(10, 100)
))
self.pcb.Add(seg)
# Create random tracks and vias
if i % 2 == 0:
t = pcbnew.PCB_TRACK(None)
else:
t = pcbnew.PCB_VIA(None)
t.SetViaType(pcbnew.VIATYPE_THROUGH)
t.SetDrill(pcbnew.FromMM(random.randint(1, 20) / 10.0))
t.SetStart(pcbnew.VECTOR2I_MM(
random.randint(100, 150),
random.randint(100, 150)
))
t.SetEnd(pcbnew.VECTOR2I_MM(
random.randint(100, 150),
random.randint(100, 150)
))
t.SetWidth(pcbnew.FromMM(random.randint(1, 15) / 10.0))
t.SetLayer(random.choice([pcbnew.F_Cu, pcbnew.B_Cu]))
self.pcb.Add(t)
# Create random connector footprints
self.create_fpc_footprint(random.randint(2, 40))
def create_fpc_footprint(self, pads):
"""Create an FPC connector footprint"""
footprint = pcbnew.FOOTPRINT(self.pcb)
footprint.SetReference(f"FPC{pads}")
footprint.Reference().SetPosition(pcbnew.VECTOR2I_MM(-1, -1))
self.pcb.Add(footprint)
# Create signal pads
pad_size = pcbnew.VECTOR2I_MM(0.25, 1.6)
for n in range(pads):
pad = pcbnew.PAD(footprint)
pad.SetSize(pad_size)
pad.SetShape(pcbnew.PAD_SHAPE_RECT)
pad.SetAttribute(pcbnew.PAD_ATTRIB_SMD)
pad.SetLayerSet(pad.SMDMask())
pad.SetPosition(pcbnew.VECTOR2I_MM(0.5 * n, 0))
pad.SetPadName(str(n + 1))
footprint.Add(pad)
# Create mounting pads
mount_size = pcbnew.VECTOR2I_MM(1.50, 2.0)
for i, x_offset in enumerate([-1.6, (pads-1)*0.5 + 1.6]):
pad = pcbnew.PAD(footprint)
pad.SetSize(mount_size)
pad.SetShape(pcbnew.PAD_SHAPE_RECT)
pad.SetAttribute(pcbnew.PAD_ATTRIB_SMD)
pad.SetLayerSet(pad.SMDMask())
pad.SetPosition(pcbnew.VECTOR2I_MM(x_offset, 1.3))
pad.SetPadName("0")
footprint.Add(pad)
# Add silkscreen
silk = pcbnew.PCB_SHAPE(footprint)
silk.SetStart(pcbnew.VECTOR2I_MM(-1, 0))
silk.SetEnd(pcbnew.VECTOR2I_MM(0, 0))
silk.SetWidth(pcbnew.FromMM(0.2))
silk.SetLayer(pcbnew.F_SilkS)
silk.SetShape(pcbnew.S_SEGMENT)
footprint.Add(silk)
# Random placement
footprint.SetPosition(pcbnew.VECTOR2I_MM(
random.randint(20, 200),
random.randint(20, 150)
))
GenerateRandomContent().register()
Example 3: Move Items Randomly
import pcbnew
import random
class MoveItemsRandomly(pcbnew.ActionPlugin):
def defaults(self):
self.name = "Move Elements Randomly"
self.category = "Test Undo/Redo"
self.description = "Move all board elements by random offsets"
def Run(self):
pcb = pcbnew.GetBoard()
# Move zones
for i in range(pcb.GetAreaCount()):
area = pcb.GetArea(i)
area.Move(pcbnew.VECTOR2I_MM(
random.randint(-20, 20),
random.randint(-20, 20)
))
# Move and randomly flip footprints
for footprint in pcb.GetFootprints():
footprint.Move(pcbnew.VECTOR2I_MM(
random.randint(-20, 20),
random.randint(-20, 20)
))
if random.randint(0, 10) > 5:
footprint.Flip(footprint.GetPosition(), True)
# Move tracks
for track in pcb.GetTracks():
track.Move(pcbnew.VECTOR2I_MM(
random.randint(-20, 20),
random.randint(-20, 20)
))
# Move drawings
for draw in pcb.GetDrawings():
draw.Move(pcbnew.VECTOR2I_MM(
random.randint(-20, 20),
random.randint(-20, 20)
))
MoveItemsRandomly().register()
Plugin Management
The ACTION_PLUGINS class manages all registered plugins:
class ACTION_PLUGINS
{
public:
static void register_action( ACTION_PLUGIN* aAction );
static bool deregister_object( void* aObject );
static ACTION_PLUGIN* GetAction( const wxString& aName );
static ACTION_PLUGIN* GetAction( int aIndex );
static ACTION_PLUGIN* GetActionByMenu( int aMenu );
static ACTION_PLUGIN* GetActionByButton( int aButton );
static ACTION_PLUGIN* GetActionByPath( const wxString& aPath );
static int GetActionsCount();
static bool IsActionRunning();
static void SetActionRunning( bool aRunning );
static void UnloadAll();
};
Plugin Installation
Directory Structure
Plugins are loaded from these directories (in order):
- Stock plugins:
<kicad>/scripting/plugins/
- User plugins:
~/.kicad/scripting/plugins/ (Linux/macOS)
- User plugins:
%APPDATA%/kicad/scripting/plugins/ (Windows)
- Third-party:
$KICAD_3RD_PARTY/plugins/
File Organization
plugins/
├── my_plugin.py # Simple single-file plugin
└── complex_plugin/ # Multi-file plugin
├── __init__.py # Must contain plugin class
├── helpers.py
├── icon.png
└── icon_dark.png
Loading Process
Plugins are automatically loaded on startup via LoadPlugins() in /scripting/kicadplugins.i:
LoadPlugins(
bundlepath="/usr/share/kicad/scripting",
userpath="~/.kicad/scripting",
thirdpartypath="$KICAD_3RD_PARTY"
)
Undo/Redo Support
Action plugins automatically support undo/redo when using proper methods:
def Run(self):
board = pcbnew.GetBoard()
# Use RemoveNative() for undo support
for footprint in board.GetFootprints():
board.RemoveNative(footprint)
# Changes are automatically tracked
# User can undo/redo after plugin execution
Icon Guidelines
- Size: 24x24 pixels (PNG format)
- Style: Monochrome or simple colors
- Transparency: Use alpha channel for toolbar integration
- Dark theme: Provide
dark_icon_file_name for dark mode
Debugging
Error Handling
import traceback
class MyPlugin(pcbnew.ActionPlugin):
def Run(self):
try:
# Your code here
board = pcbnew.GetBoard()
except Exception as e:
# Errors are shown in KiCad's error dialog
import wx
wx.MessageBox(
f"Error: {str(e)}\n\n{traceback.format_exc()}",
"Plugin Error",
wx.OK | wx.ICON_ERROR
)
Logging
Avoid using print() as it can cause IO exceptions. Use wx dialogs or write to files instead.
Best Practices
- Always call
Refresh(): Update the display after modifications
- Use
GetBoard(): Don’t cache the board object
- Handle errors gracefully: Wrap code in try/except blocks
- Validate inputs: Check that board elements exist before accessing
- Use proper units: Always convert with
FromMM() or FromMils()
- Test undo/redo: Ensure your plugin works with the undo system
- Avoid blocking operations: Long operations should show progress
Common Issues
Plugin Not Loading
Check PLUGIN_DIRECTORIES_SEARCH and NOT_LOADED_WIZARDS:
import pcbnew
print(pcbnew.GetWizardsSearchPaths())
print(pcbnew.GetUnLoadableWizards())
Ensure you:
- Called
.register() at module level
- Implemented all required methods
- Returned valid strings from metadata methods
See Also