I have created a plugin triggered by hotkeys, using instructions here:
However, when I try to undo the action, I get Incomplete undo/redo operation: some items not found.
This isn’t an issue when I use a button to trigger the action, but the button doesn’t appear under the Hotkeys Preferences. Nor does there appear to be a way to produce two buttons from the same plugin.
Is there really no access to the UNDO_REDO list in Python? Here’s an example of what I’m trying to accomplish:
You can have two plugins in the same script though.
No, not directly.
The way plugins and undo/redo interact is like a transaction. When kicad invokes a plugin it saves current state in undo system before starting plugin code. Then after the plugin Run() function completes it compares the board state to previous one and puts all changes into the undo action.
So in theory you can use this to have 2 plugins, one as an orchestrator and another as doing bits of work that you want to have undo/redo control over. It’s important that the orchestrator then returns control from Run() immediately so that kicad can run another plugin. Orchestrator can have a window floating around with a button or listening to keyboard and invoking the other plugin.
Putting two plugins in the same file is easy enough, but I’m confused about how to invoke the plugin properly. Simply executing the Run() method doesn’t seem to be sufficient. Is it in the actionPlugin superclass?
I hope that hotkey support for plugins is coming in KiCAD 8.
You need to have kicad invoke it, not call plugin’s method directly. For that you need to send wx event to the pcb editor window. First post you linked has some code that finds the window, now you either need to send it a keyboard event (if you have bound your plugin to a shortcut programmatically) or find the top toolbar and send it appropriate command event with id of the plugin button.
Once you get the pcbnew window you can FindWindowById(ID_H_TOOLBAR)
For example this code used to add the plugin button in kicad 5 before it was natively supported.
Note that nowadays top toolbar is regenerated so you should not store the id like I did in the link above, that works only for old kicad. Instead find the toolbar and button every time you need to invoke it.
Alternative option is to find the window menu and iterate through it’s items to find “tools” then “external plugins” then your plugin. This method will not rely on user keeping your plugin button on the toolbar (they can hide it in preferences).
To iterate through child widgets of an element use native wx.Window methods https://docs.wxpython.org/wx.Window.html#wx.Window.GetChildren
and use their properties to find the right one (title, label, whatever fits).
Ok. I found the toolbar but the only child seems to be the wx.BitmapComboBox for selecting the active layer.
Where are all the buttons??
I did verify that I have the right object, by way of the Center() and Disable() methods.
I played around a bit, you should be able to piece things together from this:
w = [w for w in wx.GetTopLevelWindows() if "pcb editor" in w.GetTitle().lower()][0]
tb = [t for t in w.Children if t.GetId() == pcbnew.ID_H_TOOLBAR][0]
tb.ToolCount
# prints 49
t = tb.FindToolByIndex(48)
t.ShortHelp
# prints 'Update test plugin'
w.QueueEvent(wx.CommandEvent(wx.wxEVT_TOOL, id=t.Id))
# runs plugin
I see what you’re doing, but I can’t replicate the results.
w = [w for w in wx.GetTopLevelWindows() if "pcb editor" in w.GetTitle().lower()][0]
tb = [t for t in w.Children if t.GetId() == pcbnew.ID_H_TOOLBAR][0]
tb.ToolCount
# AttributeError: 'Control' object has no attribute 'ToolCount'
tb
# prints <wx._core.Control object at 0x7fd21916db40>
tb.ClassName
# prints 'wxAuiToolBar'
wx.aui.AuiToolBar(tb)
# Creates a child of the toolbar, rather than typecasting as I hoped
typing.cast(wx.aui.AuiToolBar,tb)
# Returns the same tb object without changing the type
So even though the object claims to be an AuiToolBar, I don’t know how to access it as one.
I’m sorry I’m not very good with Python type casting. It seems to be one of those things Python is supposed to take care of without intervention.
I have same version on windows and my snippet works perfectly in kicad’s scripting console. Did you try it there?
Also maybe you installed some weird plugins or pip packages that mess with wxpython, try it on a clean kicad installation.
Yes, I did try it on Windows with the same result.
Maybe it has to do with the imports? I only used two:
import wx
import pcbnew
EDIT:
Yes, that’s the issue. I need to also import wx.aui in order to see the correct classes.
Something with the REPL optimization was preventing this from working after I had already tried. I had to restart KiCAD and do the correct imports the first time.
Isn’t it redundant to make a button to queue a button press?
It seems like I should be able to update the acceleratortable using the tool ID of the plugin directly.
However, I can’t seem to trigger any of the toolbar buttons, though I can make the hotkey button trigger itself. The tool IDs are global, right?
Here’s my test code
import pcbnew
import wx.aui
import wx
import os
class MyTool(pcbnew.ActionPlugin):
def defaults(self):
self.name = "MyTool"
self.category = "testing"
self.description = "testing hotkeys"
self.show_toolbar_button = True# Optional, defaults to False
def Run(self):
print("Hurray! Button was pressed")
MyTool().register() # Instantiate and register to Pcbnew
def findPcbnewWindow():
"""Find the window for the PCBNEW application."""
windows = wx.GetTopLevelWindows()
pcbnew = [w for w in windows if "PCB Editor" in w.GetTitle()]
if len(pcbnew) != 1:
raise Exception("Cannot find pcbnew window from title matching!")
return pcbnew[0]
def FindToolBar(barid = pcbnew.ID_H_TOOLBAR):
bar = [a for a in findPcbnewWindow().GetChildren() if a.GetId() == barid]
if len(bar) != 1:
raise Exception("Cannot find toolbar panel from ID matching")
return bar[0]
def FindToolId(tool):
bar = FindToolBar()
tools = [bar.FindToolByIndex(i) for i in range(bar.ToolCount)]
if isinstance(tool,str):
name = tool
else:
name = tool.name
tid = [t.GetId() for t in tools if t.ShortHelp == name]
if len(tid) != 1:
raise Exception("Cannot find desired tool")
return tid[0]
mainFrame = findPcbnewWindow()
hotkey1= wx.NewId()
hotkey2= wx.NewId()
toolbtn = FindToolId(MyTool())
def btn_press(id):
mainFrame.QueueEvent(wx.CommandEvent(wx.wxEVT_TOOL, id=id))
def cb1(context):
print("hotkey1")
btn_press(hotkey2)
def cb2(context):
print("hotkey2")
btn_press(toolbtn)
accel_tbl = wx.AcceleratorTable([(wx.ACCEL_SHIFT, ord('J'), hotkey1)
,(wx.ACCEL_SHIFT, ord('K'), hotkey2)
])
mainFrame.Bind(wx.EVT_TOOL, cb1, id=hotkey1)
mainFrame.Bind(wx.EVT_TOOL, cb2, id=hotkey2)
mainFrame.SetAcceleratorTable(accel_tbl)
print("Starting Hotkey test plugin")
The toolbar with plugin buttons is recreated dynamically on various triggers so the tool IDs are not stable. Also you can’t access it in the plugin init code because toolbar is likely not yet populated when init runs.