Adding samples for new plugin features, doc update.

This commit is contained in:
Matthias Koefferlein 2025-08-30 14:53:46 +02:00
parent e396b6ec29
commit f1c16a0242
5 changed files with 513 additions and 1 deletions

View File

@ -976,7 +976,7 @@ marker.destroy</pre>
<p> <p>
The PluginFactory itself acts as a singleton per plugin class and provides not only the ability to create The PluginFactory itself acts as a singleton per plugin class and provides not only the ability to create
Plugin objects but also a couple of configuration options and a global handler for configuration and menu Plugin objects but also a couple of configuration options and a global handler for configuration and menu
events. The configuration includes: events. The PluginFactory provides:
</p> </p>
<ul> <ul>
@ -991,6 +991,10 @@ marker.destroy</pre>
After an option is configured, the individual Plugin objects and the PluginFactory receives "configure" calls After an option is configured, the individual Plugin objects and the PluginFactory receives "configure" calls
when a configuration option changes or for the initial configuration. when a configuration option changes or for the initial configuration.
</li> </li>
<li>Widgets: The plugin factory can provide widgets for the configuration dialog ('File/Setup') and the
editor options dock. Respective callbacks are <class_doc href="PluginFactory#create_config_pages"/>
and <class_doc href="PluginFactory#create_editor_options_pages"/>.
</li>
</ul> </ul>
<p> <p>
@ -1048,5 +1052,29 @@ marker.destroy</pre>
over the mouse in certain circumstances and is supposed to put the plugin into a "watching" instead of "dragging" state. over the mouse in certain circumstances and is supposed to put the plugin into a "watching" instead of "dragging" state.
</p> </p>
<p>
A plugin may also create markers for visual feedback and highlights. This can be done explicitly
using marker objects (<class_doc href="Marker"/>) or in a application-defined fashion by generating
mouse cursors. The API functions for this purpose are <class_doc href="Plugin#clear_mouse_cursors"/>,
<class_doc href="Plugin#add_mouse_cursor"/> and <class_doc href="Plugin#add_edge_marker"/>. These
functions provide cursors and highlights that match the visual effects of other plugins and
interface with the mouse tracking feature of the application.
</p>
<p>
Another service the Plugin class provides is snapping:
there exist a number of global configuration options which control snapping (grids, snapping to
objects, angle constraints). The plugin offers a number of snap functions that follow the
application's current configuration and implement snapping accordingly. These methods are
<class_doc href="Plugin#snap"/> and <class_doc href="Plugin#snap2"/>. While the first
method provides grid and angle snapping, the second also implements snapping to layout objects.
</p>
<p>
The "drag box" sample macro demonstrates many of these features.
The sample is available from the macro templates when you create a new
macro in the macro IDE.
</p>
</doc> </doc>

View File

@ -5,6 +5,8 @@
<file alias="new_macro.lym">macro_templates/new_macro.lym</file> <file alias="new_macro.lym">macro_templates/new_macro.lym</file>
<file alias="new_text_file.txt">macro_templates/new_text_file.txt</file> <file alias="new_text_file.txt">macro_templates/new_text_file.txt</file>
<file alias="new_ruby_file.rb">macro_templates/new_ruby_file.rb</file> <file alias="new_ruby_file.rb">macro_templates/new_ruby_file.rb</file>
<file alias="drag_box_sample.lym">macro_templates/drag_box_sample.lym</file>
<file alias="drag_box_sample_python.lym">macro_templates/drag_box_sample_python.lym</file>
<file alias="pcell.lym">macro_templates/pcell.lym</file> <file alias="pcell.lym">macro_templates/pcell.lym</file>
<file alias="pcell_sample.lym">macro_templates/pcell_sample.lym</file> <file alias="pcell_sample.lym">macro_templates/pcell_sample.lym</file>
<file alias="qt_designer.lym">macro_templates/qt_designer.lym</file> <file alias="qt_designer.lym">macro_templates/qt_designer.lym</file>

View File

@ -0,0 +1,258 @@
<?xml version="1.0" encoding="utf-8"?>
<klayout-macro>
<description>A plugin sample\nThis sample provides a box drawing feature and demonstrates UI components and snapping</description>
<format>general</format>
<autorun>true</autorun>
<autorun-early>false</autorun-early>
<show-in-menu>false</show-in-menu>
<shortcut></shortcut>
<interpreter>ruby</interpreter>
<text># Sample plugin
#
# This plugin implements a box that can be drawn by
# clicking at the first and then at the second point.
# There is one box which is replacing the previous one.
# Line color and line width of the box can be configured
# by editor options (line width) or configuration pages
# (color).
module DragBox
# Register this macro as "autorun" to enable the plugin
CFG_COLOR = "drag-box-color"
CFG_WIDTH = "drag-box-width"
# The widget placed into the editor options dock
class DragBoxEditorOptionsPage &lt; RBA::EditorOptionsPage
def initialize
# Creates a new page with title "Options" and at
# position 1 (second from left)
super("Options", 1)
layout2 = RBA::QVBoxLayout::new(self)
layout = RBA::QHBoxLayout::new(self)
layout2.addLayout(layout)
label = RBA::QLabel::new("Line width", self)
layout.addWidget(label)
@spin_box = RBA::QSpinBox::new(self)
@spin_box.setMinimum(1)
@spin_box.setMaximum(16)
layout.addWidget(@spin_box)
layout.addStretch(1)
layout2.addStretch(1)
@spin_box.valueChanged = lambda { |x| self.edited }
end
def setup(dispatcher)
begin
@spin_box.setValue(dispatcher.get_config(CFG_WIDTH).to_i)
rescue
@spin_box.setValue(1)
end
end
def apply(dispatcher)
dispatcher.set_config(CFG_WIDTH, @spin_box.value.to_s)
end
end
# The widget placed into the configuration page
class DragBoxConfigPage &lt; RBA::ConfigPage
def initialize
# places the widget on a new section ("Drag Box")
# and "Configure" page
super("Drag Box|Configure")
# Qt user interfact setup
layout = RBA::QHBoxLayout::new(self)
label = RBA::QLabel::new("Color (hex, rrggbb)", self)
layout.addWidget(label)
@line_edit = RBA::QLineEdit::new(self)
layout.addWidget(@line_edit)
layout.addStretch(1)
end
def setup(dispatcher)
@line_edit.setText(dispatcher.get_config(CFG_COLOR))
end
def apply(dispatcher)
dispatcher.set_config(CFG_COLOR, @line_edit.text)
end
end
class DragBoxPlugin &lt; RBA::Plugin
def initialize(view)
super()
@marker = nil
@last_box = nil
@box = nil
@start_point = nil
@view = view
@color = nil
@width = 1
end
def configure(name, value)
# receives configuration callbacks
if name == CFG_COLOR
# configure marker color
begin
@color = value != "" ? value.to_i(16) : nil
rescue
self.color = nil
end
self._configure_marker
elsif name == CFG_WIDTH
# configure marker line width
begin
@width = value.to_i
rescue
@width = nil
end
self._configure_marker
end
end
def _clear_marker
# clears all markers
[ @marker, @last_box ].each { |m| m &amp;&amp; m._destroy }
@marker = nil
@last_box = nil
end
def _update_marker
# updates the marker with the current box
if !@marker
@marker = RBA::Marker::new(self.view)
self._configure_marker
end
@marker.set(@box)
end
def freeze_marker
# stop dragging the marker and copy to a static one
if @last_box
@last_box._destroy()
end
@last_box = @marker
@marker = nil
end
def _configure_marker
# change the marker's appearance
if @marker
@marker.line_style = 2 # short-dashed
@marker.vertex_size = 0 # no vertexes
@marker.line_width = @width
@marker.color = @color ? (@color | 0xff000000) : 0
end
end
def activated
# plugin is activated - i.e. the mode is selected
RBA::MainWindow.instance.message("Click on point to start dragging a box", 10000)
end
def deactivated
# plugin is deactivated - i.e. the mode is unselected
self._clear_marker
RBA::MainWindow.instance.message("", 0)
end
def mouse_click_event(p, buttons, prio)
if prio
# first-level event: start a new box or
# stop dragging it and freeze the box
if !@marker
p = self.snap2(p)
@box = RBA::DBox::new(p, p)
@start_point = p
self._clear_marker
self._update_marker
self.grab_mouse
RBA::MainWindow.instance.message("Drag the box and click again", 10000)
else
p = self.snap2(p, @start_point, true, self.ac_from_buttons(buttons))
self.freeze_marker
self.ungrab_mouse
RBA::MainWindow.instance.message("Box finished: " + @box.to_s, 10000)
end
# consume event
return true
else
return false
end
end
def mouse_moved_event(p, buttons, prio)
if prio
# first-level event: if not dragging, provide a
# mouse cursor for tracking. If dragging, update
# the box and provide a mouse cursor.
if !@marker
self.clear_mouse_cursors
p = self.snap2(p, true)
self.add_mouse_cursor(p)
else
self.clear_mouse_cursors
p = self.snap2(p, @start_point, true, self.ac_from_buttons(buttons), true)
self.add_mouse_cursor(p)
@box = RBA::DBox::new(@start_point, p)
self._update_marker
end
end
# NOTE: we must not digest this event (i.e. return true)
# to allow the mouse tracker to receive the events as well
return false
end
end
# Implements a "plugin factory".
# Purpose of this object is to create a plugin object
# and corresponding UI objects.
class DragBoxPluginFactory &lt; RBA::PluginFactory
def initialize
super()
self.has_tool_entry = true
# NOTE: it's a good practice to register configuration options
self.add_option(CFG_WIDTH, "1")
self.add_option(CFG_COLOR, "")
self.register(-1000, "drag_box", "Drag Box")
end
def create_config_pages
self.add_config_page(DragBoxConfigPage::new)
end
def create_editor_options_pages
self.add_editor_options_page(DragBoxEditorOptionsPage::new)
end
def create_plugin(manager, root, view)
return DragBoxPlugin::new(view)
end
end
# Create the singleton instance - as we register it,
# it is not garbage collected
DragBoxPluginFactory::new
end</text>
</klayout-macro>

View File

@ -0,0 +1,222 @@
<?xml version="1.0" encoding="utf-8"?>
<klayout-macro>
<description>A plugin sample\nThis sample provides a box drawing feature and demonstrates UI components and snapping</description>
<format>general</format>
<autorun>true</autorun>
<autorun-early>false</autorun-early>
<show-in-menu>false</show-in-menu>
<shortcut></shortcut>
<interpreter>python</interpreter>
<text># Sample plugin
#
# This plugin implements a box that can be drawn by
# clicking at the first and then at the second point.
# There is one box which is replacing the previous one.
# Line color and line width of the box can be configured
# by editor options (line width) or configuration pages
# (color).
# Register this macro as "autorun" to enable the plugin
cfg_color = "drag-box-color"
cfg_width = "drag-box-width"
# The widget placed into the editor options dock
class DragBoxEditorOptionsPage(pya.EditorOptionsPage):
def __init__(self):
# Creates a new page with title "Options" and at
# position 1 (second from left)
super(DragBoxEditorOptionsPage, self).__init__("Options", 1)
layout2 = pya.QVBoxLayout(self)
layout = pya.QHBoxLayout(self)
layout2.addLayout(layout)
label = pya.QLabel("Line width", self)
layout.addWidget(label)
self.spin_box = pya.QSpinBox(self)
self.spin_box.setMinimum(1)
self.spin_box.setMaximum(16)
layout.addWidget(self.spin_box)
layout.addStretch(1)
layout2.addStretch(1)
self.spin_box.valueChanged = lambda x: self.edited()
def setup(self, dispatcher):
try:
self.spin_box.setValue(int(dispatcher.get_config(cfg_width)))
except:
self.spin_box.setValue(1)
def apply(self, dispatcher):
dispatcher.set_config(cfg_width, str(self.spin_box.value))
# The widget placed into the configuration page
class DragBoxConfigPage(pya.ConfigPage):
def __init__(self):
# places the widget on a new section ("Drag Box")
# and "Configure" page
super(DragBoxConfigPage, self).__init__("Drag Box|Configure")
layout = pya.QHBoxLayout(self)
label = pya.QLabel("Color (hex, rrggbb)", self)
layout.addWidget(label)
self.line_edit = pya.QLineEdit(self)
layout.addWidget(self.line_edit)
layout.addStretch(1)
def setup(self, dispatcher):
self.line_edit.setText(dispatcher.get_config(cfg_color))
def apply(self, dispatcher):
dispatcher.set_config(cfg_color, self.line_edit.text)
class DragBoxPlugin(pya.Plugin):
def __init__(self, view):
super(DragBoxPlugin, self).__init__()
self.marker = None
self.last_box = None
self.box = None
self.start_point = None
self.view = view
self.color = None
self.width = 1
def configure(self, name, value):
# receives configuration callbacks
needs_update = False
if name == cfg_color:
# configure marker color
try:
if value != "":
self.color = int(value, 16)
else:
self.color = None
except:
self.color = None
self._configure_marker()
elif name == cfg_width:
# configure marker line width
try:
self.width = int(value)
except:
self.width = None
self._configure_marker()
def _clear_marker(self):
# clears all markers
for marker in [ self.marker, self.last_box ]:
if marker is not None:
marker._destroy()
self.marker = None
self.last_box = None
def _update_marker(self):
# updates the marker with the current box
if self.marker is None:
self.marker = pya.Marker(self.view)
self._configure_marker()
self.marker.set(self.box)
def freeze_marker(self):
# stop dragging the marker and copy to a static one
if self.last_box is not None:
self.last_box._destroy()
self.last_box = self.marker
self.marker = None
def _configure_marker(self):
# change the marker's appearance
if self.marker is not None:
self.marker.line_style = 2 # short-dashed
self.marker.vertex_size = 0 # no vertexes
self.marker.line_width = self.width
if self.color is not None:
self.marker.color = self.color | 0xff000000
else:
self.marker.color = 0 # auto
def activated(self):
# plugin is activated - i.e. the mode is selected
pya.MainWindow.instance().message("Click on point to start dragging a box", 10000)
def deactivated(self):
# plugin is deactivated - i.e. the mode is unselected
self._clear_marker()
pya.MainWindow.instance().message("", 0)
def mouse_click_event(self, p, buttons, prio):
if prio:
# first-level event: start a new box or
# stop dragging it and freeze the box
if self.marker is None:
p = self.snap2(p)
self.box = pya.DBox(p, p)
self.start_point = p
self._clear_marker()
self._update_marker()
self.grab_mouse()
pya.MainWindow.instance().message("Drag the box and click again", 10000)
else:
p = self.snap2(p, self.start_point, True, self.ac_from_buttons(buttons))
self.freeze_marker()
self.ungrab_mouse()
pya.MainWindow.instance().message("Box finished: " + str(self.box), 10000)
return True
return False
def mouse_moved_event(self, p, buttons, prio):
if prio:
# first-level event: if not dragging, provide a
# mouse cursor for tracking. If dragging, update
# the box and provide a mouse cursor.
if self.marker is None:
self.clear_mouse_cursors()
p = self.snap2(p, visualize=True)
self.add_mouse_cursor(p)
else:
self.clear_mouse_cursors()
p = self.snap2(p, self.start_point, True, self.ac_from_buttons(buttons), visualize=True)
self.add_mouse_cursor(p)
self.box = pya.DBox(self.start_point, p)
self._update_marker()
# NOTE: we must not digest this event (i.e. return True)
# to allow the mouse tracker to receive the events as well
return False
# Implements a "plugin factory".
# Purpose of this object is to create a plugin object
# and corresponding UI objects.
class DragBoxPluginFactory(pya.PluginFactory):
def __init__(self):
super(DragBoxPluginFactory, self).__init__()
self.has_tool_entry = True
# NOTE: it's a good practice to register configuration options
self.add_option(cfg_width, "1")
self.add_option(cfg_color, "")
self.register(-1000, "drag_box", "Drag Box")
def create_config_pages(self):
self.add_config_page(DragBoxConfigPage())
def create_editor_options_pages(self):
self.add_editor_options_page(DragBoxEditorOptionsPage())
def create_plugin(self, manager, root, view):
return DragBoxPlugin(view)
# Create the singleton instance - as we register it,
# it is not garbage collected
DragBoxPluginFactory()
</text>
</klayout-macro>

View File

@ -23,6 +23,7 @@ pcell_sample.lym
qt_designer.lym qt_designer.lym
qt_dialog.lym qt_dialog.lym
qt_server.lym qt_server.lym
drag_box_sample.lym
[pymacros] [pymacros]
# General group # General group
@ -41,4 +42,5 @@ pcell_sample_python.lym
qt_designer_python.lym qt_designer_python.lym
qt_dialog_python.lym qt_dialog_python.lym
qt_server_python.lym qt_server_python.lym
drag_box_sample_python.lym