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>
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
events. The configuration includes:
events. The PluginFactory provides:
</p>
<ul>
@ -991,6 +991,10 @@ marker.destroy</pre>
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.
</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>
<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.
</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>

View File

@ -5,6 +5,8 @@
<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_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_sample.lym">macro_templates/pcell_sample.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_dialog.lym
qt_server.lym
drag_box_sample.lym
[pymacros]
# General group
@ -41,4 +42,5 @@ pcell_sample_python.lym
qt_designer_python.lym
qt_dialog_python.lym
qt_server_python.lym
drag_box_sample_python.lym