Updating samples to include focus pages

This commit is contained in:
Matthias Koefferlein 2025-09-04 19:24:49 +02:00
parent b9115fc0a2
commit b3123d385a
2 changed files with 474 additions and 85 deletions

View File

@ -8,29 +8,39 @@
<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).
# (color). These settings are managed through configuration
# options and their current state is persisted.
#
# The dimension of the box can be entered numerically
# while dragging the box. This feature is implemented
# through a modal "focus page", which opens when you
# press the Tab key during editing and when the keyboard
# focus is on the canvas.
#
# Register this macro as "autorun" to enable the plugin
# on startup.
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
# An option page providing a single entry box for configuring the line width
# This page communicates via configuration options. One advantage of this
# approach is that the values are persisted
class DragBoxEditorOptionsPage &lt; RBA::EditorOptionsPage
# Creates a new page with title "Options" and at position 1 (second from left)
def initialize
# Creates a new page with title "Options" and at
# position 1 (second from left)
super("Options", 1)
layout2 = RBA::QVBoxLayout::new(self)
@ -44,10 +54,15 @@ class DragBoxEditorOptionsPage &lt; RBA::EditorOptionsPage
layout.addWidget(@spin_box)
layout.addStretch(1)
layout2.addStretch(1)
# connect the spin box value change with the "edited" slot
# which will result in a call of "apply".
@spin_box.valueChanged = lambda { |x| self.edited }
end
# "setup" is called when the page needs to be populated with information -
# i.e. on first show.
def setup(dispatcher)
begin
@spin_box.setValue(dispatcher.get_config(CFG_WIDTH).to_i)
@ -56,23 +71,123 @@ class DragBoxEditorOptionsPage &lt; RBA::EditorOptionsPage
end
end
# "apply" is called when the page is requested to submit the entered
# values to the plugin. Usually this should be done via configuration
# events.
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
# A (modal) option page, also called a "focus page". This page is
# registered like an editor options page. It is brought to front
# when the user hits the "Tab" key during editing.
# In this case, this page uses "setup" and "apply" callbacks to
# set and fetch information. It also employs a handler named
# "update_box" to communicate changes between the client (the
# plugin) and the page.
#
# Attributes that the client needs to take care of are
# "self.box" (the current box), "self.pfix" (the start point)
# and "self.update_box".
class DragBoxFocusPage &lt; RBA::EditorOptionsPage
# Creates a new page with title "Options" and at
# position 1 (second from left)
attr_accessor :box
attr_accessor :pfix
attr_accessor :update_box
def initialize
super("Geometry", 2)
self.focus_page = true
self.modal_page = true
@box = RBA::DBox::new
@pfix = RBA::DPoint::new
layout = RBA::QGridLayout::new(self)
layout.setColumnStretch(1, 1)
label = RBA::QLabel::new("Width", self)
layout.addWidget(label, 0, 0, 1, 1)
@le_width = RBA::QLineEdit::new(self)
layout.addWidget(@le_width, 0, 1, 1, 1)
label = RBA::QLabel::new("Height", self)
layout.addWidget(label, 1, 0, 1, 1)
@le_height = RBA::QLineEdit::new(self)
layout.addWidget(@le_height, 1, 1, 1, 1)
layout.setRowStretch(2, 1)
end
# Is called when the page needs to be set up.
# We assume that the client has properly set up self.box
def setup(dispatcher)
@le_width.text = "%.12g" % @box.width
@le_height.text = "%.12g" % @box.height
end
# Apply is called when the dialog is accepted or the "Apply" button is pressed
# Usually this method is intended to submit configuration parameter changes,
# but we can use it for any other purpose as well.
# places the widget on a new section ("Drag Box")
# and "Configure" page
def apply(dispatcher)
# fetches the coordinates from the entry boxes
# throws an exception in case of an error
x = @le_width.text.to_f
y = @le_height.text.to_f
# prepares a new box with the given dimensions
# using the initial point ("pfix") and considering
# the drag direction
t = b = @pfix.y
l = r = @pfix.x
if @box.bottom &lt; t - 1e-10
b = t - y
else
t = b + y
end
if @box.left &lt; l - 1e-10
l = r - x
else
r = l + x
end
# issue the event (call the handler) to inform the plugin of this change
if @update_box
@update_box.call(RBA::DBox::new(l, b, r, t))
end
end
end
# The widget placed into the configuration page
# A configuration page with a single entry box to change
# the box color in RGB hex style.
# Configuration pages appear in the Setup dialog and can
# communicate only through configuration parameter updates.
class DragBoxConfigPage &lt; RBA::ConfigPage
# Initializes the page. Places it on a new section ("Drag Box") and "Configure" page
# and creates a single entry field.
def initialize
super("Drag Box|Configure")
# Qt user interface setup
layout = RBA::QHBoxLayout::new(self)
label = RBA::QLabel::new("Color (hex, rrggbb)", self)
layout.addWidget(label)
@ -82,22 +197,26 @@ class DragBoxConfigPage &lt; RBA::ConfigPage
end
# This method is called to request an update of the entry fields
def setup(dispatcher)
@line_edit.setText(dispatcher.get_config(CFG_COLOR))
end
# This method is called to request a transfer of the edited values
# to the configuration space.
def apply(dispatcher)
dispatcher.set_config(CFG_COLOR, @line_edit.text)
end
end
# The custom plugin implementation.
class DragBoxPlugin &lt; RBA::Plugin
def initialize(view)
super()
@marker = nil
@last_box = nil
@last_marker = nil
@box = nil
@start_point = nil
@view = view
@ -105,14 +224,14 @@ class DragBoxPlugin &lt; RBA::Plugin
@width = 1
end
# This method receives configuration callbacks
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
@color = nil
end
self._configure_marker
elsif name == CFG_WIDTH
@ -126,57 +245,97 @@ class DragBoxPlugin &lt; RBA::Plugin
end
end
# clears all markers
def _clear_marker
# clears all markers
[ @marker, @last_box ].each { |m| m &amp;&amp; m._destroy }
[ @marker, @last_marker ].each { |marker| marker &amp;&amp; marker._destroy }
@marker = nil
@last_box = nil
@last_marker = nil
end
# stops dragging the marker and copy to a static one
def _finish
if @last_marker
@last_marker._destroy
end
@last_marker = @marker
@marker = nil
# reset to idle
self.ungrab_mouse
RBA::MainWindow.instance.message("Box finished: " + @box.to_s, 10000)
end
# updates the marker with the current box
def _update_marker
# updates the marker with the current box
if !@marker
@marker = RBA::Marker::new(self.view)
if ! @marker
@marker = RBA::Marker::new(@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
# changes the marker's appearance
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
@marker.color = @color ? (@color | 0xff000000) : 0 # auto
end
end
# Updates the box with the given value and updates the marker.
# This method is bound to the focus page handler when needed.
def _update_box(box)
@box = box
self._update_marker
end
# overloaded callback: the focus page is requested
def focus_page_open(fp)
# stop unless dragging
if !@marker
return
end
# configure the focus page and show it:
# the page will call the handler of "update_box" to commit
# changes to the box
fp.box = @box
fp.pfix = @start_point
fp.update_box = lambda { |box| self._update_box(box) }
ret = fp.show
fp.update_box = nil
if ret == 1
# accepted: stop dragging now, we are done.
self._finish
end
return ret
end
# overloaded callback:
# plugin is activated - i.e. the mode is selected
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
# overloaded callback:
# plugin is deactivated - i.e. the mode is unselected
def deactivated
# plugin is deactivated - i.e. the mode is unselected
self._clear_marker
RBA::MainWindow.instance.message("", 0)
end
# overloaded callback:
# a mouse button was clicked
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
if ! @marker
p = self.snap2(p)
@box = RBA::DBox::new(p, p)
@start_point = p
@ -186,49 +345,55 @@ class DragBoxPlugin &lt; RBA::Plugin
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)
self._update_box(RBA::DBox::new(@start_point, p))
self._finish
end
# consume event
return true
else
return false
end
return false
end
# overloaded callback:
# the mouse was moved
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)
p = self.snap2(p, :visualize =&gt; true)
self.add_mouse_cursor(p)
else
self.clear_mouse_cursors
p = self.snap2(p, @start_point, true, self.ac_from_buttons(buttons), true)
p = self.snap2(p, @start_point, true, self.ac_from_buttons(buttons), :visualize =&gt; 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
# The purpose of this object is to create a plugin object
# and corresponding UI objects.
class DragBoxPluginFactory &lt; RBA::PluginFactory
def initialize
super()
super
self.has_tool_entry = true
# NOTE: it's a good practice to register configuration options
self.add_option(CFG_WIDTH, "1")
@ -236,23 +401,28 @@ class DragBoxPluginFactory &lt; RBA::PluginFactory
self.register(-1000, "drag_box", "Drag Box")
end
# Called to create the configuration pages
def create_config_pages
self.add_config_page(DragBoxConfigPage::new)
end
# Called to create the editor options pages
def create_editor_options_pages
self.add_editor_options_page(DragBoxEditorOptionsPage::new)
self.add_editor_options_page(DragBoxFocusPage::new)
end
# Creates the plugin
def create_plugin(manager, root, view)
return DragBoxPlugin::new(view)
end
end
# Create the singleton instance - as we register it,
end
# Creates the singleton instance - as we register it,
# it is not garbage collected
DragBoxPluginFactory::new
end</text>
end
</text>
</klayout-macro>

View File

@ -8,15 +8,23 @@
<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).
# (color). These settings are managed through configuration
# options and their current state is persisted.
#
# The dimension of the box can be entered numerically
# while dragging the box. This feature is implemented
# through a modal "focus page", which opens when you
# press the Tab key during editing and when the keyboard
# focus is on the canvas.
#
# Register this macro as "autorun" to enable the plugin
# on startup.
cfg_color = "drag-box-color"
cfg_width = "drag-box-width"
@ -24,11 +32,19 @@ cfg_width = "drag-box-width"
# The widget placed into the editor options dock
class DragBoxEditorOptionsPage(pya.EditorOptionsPage):
"""
An option page providing a single entry box for configuring the line width
This page communicates via configuration options. One advantage of this
approach is that the values are persisted
"""
def __init__(self):
# Creates a new page with title "Options" and at
# position 1 (second from left)
"""
Creates a new page with title "Options" and at position 1 (second from left)
"""
super(DragBoxEditorOptionsPage, self).__init__("Options", 1)
layout2 = pya.QVBoxLayout(self)
@ -43,25 +59,142 @@ class DragBoxEditorOptionsPage(pya.EditorOptionsPage):
layout.addStretch(1)
layout2.addStretch(1)
# connect the spin box value change with the "edited" slot
# which will result in a call of "apply".
self.spin_box.valueChanged = lambda x: self.edited()
def setup(self, dispatcher):
"""
"setup" is called when the page needs to be populated with information -
i.e. on first show.
"""
try:
self.spin_box.setValue(int(dispatcher.get_config(cfg_width)))
except:
self.spin_box.setValue(1)
def apply(self, dispatcher):
"""
"apply" is called when the page is requested to submit the entered
values to the plugin. Usually this should be done via configuration
events.
"""
dispatcher.set_config(cfg_width, str(self.spin_box.value))
# The modal dialog page that appears when "Tab" is pressed
class DragBoxFocusPage(pya.EditorOptionsPage):
"""
A (modal) option page, also called a "focus page". This page is
registered like an editor options page. It is brought to front
when the user hits the "Tab" key during editing.
In this case, this page uses "setup" and "apply" callbacks to
set and fetch information. It also employs a handler named
"update_box" to communicate changes between the client (the
plugin) and the page.
Attributes that the client needs to take care of are
"self.box" (the current box), "self.pfix" (the start point)
and "self.update_box".
"""
def __init__(self):
"""
Creates a new page with title "Options" and at
position 1 (second from left)
"""
super(DragBoxFocusPage, self).__init__("Geometry", 2)
self.focus_page = True
self.modal_page = True
self.box = pya.DBox()
self.pfix = pya.DPoint()
self.update_box = None
layout = pya.QGridLayout(self)
layout.setColumnStretch(1, 1)
label = pya.QLabel("Width", self)
layout.addWidget(label, 0, 0, 1, 1)
self.le_width = pya.QLineEdit(self)
layout.addWidget(self.le_width, 0, 1, 1, 1)
label = pya.QLabel("Height", self)
layout.addWidget(label, 1, 0, 1, 1)
self.le_height = pya.QLineEdit(self)
layout.addWidget(self.le_height, 1, 1, 1, 1)
layout.setRowStretch(2, 1)
def setup(self, dispatcher):
"""
Is called when the page needs to be set up.
We assume that the client has properly set up self.box
"""
self.le_width.text = "%.12g" % self.box.width()
self.le_height.text = "%.12g" % self.box.height()
def apply(self, dispatcher):
"""
Apply is called when the dialog is accepted or the "Apply" button is pressed
Usually this method is intended to submit configuration parameter changes,
but we can use it for any other purpose as well.
"""
# fetches the coordinates from the entry boxes
# throws an exception in case of an error
x = float(self.le_width.text)
y = float(self.le_height.text)
# prepares a new box with the given dimensions
# using the initial point ("pfix") and considering
# the drag direction
t = b = self.pfix.y
l = r = self.pfix.x
if self.box.bottom &lt; t - 1e-10:
b = t - y
else:
t = b + y
if self.box.left &lt; l - 1e-10:
l = r - x
else:
r = l + x
# issue the event (call the handler) to inform the plugin of this change
if self.update_box is not None:
self.update_box(pya.DBox(l, b, r, t))
# The widget placed into the configuration page
class DragBoxConfigPage(pya.ConfigPage):
"""
A configuration page with a single entry box to change
the box color in RGB hex style.
Configuration pages appear in the Setup dialog and can
communicate only through configuration parameter updates.
"""
def __init__(self):
# places the widget on a new section ("Drag Box")
# and "Configure" page
"""
Initializes the page. Places it on a new section ("Drag Box") and "Configure" page
and creates a single entry field.
"""
super(DragBoxConfigPage, self).__init__("Drag Box|Configure")
layout = pya.QHBoxLayout(self)
@ -72,17 +205,28 @@ class DragBoxConfigPage(pya.ConfigPage):
layout.addStretch(1)
def setup(self, dispatcher):
"""
This method is called to request an update of the entry fields
"""
self.line_edit.setText(dispatcher.get_config(cfg_color))
def apply(self, dispatcher):
"""
This method is called to request a transfer of the edited values
to the configuration space.
"""
dispatcher.set_config(cfg_color, self.line_edit.text)
class DragBoxPlugin(pya.Plugin):
"""
The custom plugin implementation.
"""
def __init__(self, view):
super(DragBoxPlugin, self).__init__()
self.marker = None
self.last_box = None
self.last_marker = None
self.box = None
self.start_point = None
self.view = view
@ -90,8 +234,9 @@ class DragBoxPlugin(pya.Plugin):
self.width = 1
def configure(self, name, value):
# receives configuration callbacks
needs_update = False
"""
This method receives configuration callbacks
"""
if name == cfg_color:
# configure marker color
try:
@ -111,29 +256,40 @@ class DragBoxPlugin(pya.Plugin):
self._configure_marker()
def _clear_marker(self):
# clears all markers
for marker in [ self.marker, self.last_box ]:
"""
clears all markers
"""
for marker in [ self.marker, self.last_marker ]:
if marker is not None:
marker._destroy()
self.marker = None
self.last_box = None
self.last_marker = None
def _finish(self):
"""
stops dragging the marker and copy to a static one
"""
if self.last_marker is not None:
self.last_marker._destroy()
self.last_marker = self.marker
self.marker = None
# reset to idle
self.ungrab_mouse()
pya.MainWindow.instance().message("Box finished: " + str(self.box), 10000)
def _update_marker(self):
# updates the marker with the current box
"""
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
"""
changes the marker's appearance
"""
if self.marker is not None:
self.marker.line_style = 2 # short-dashed
self.marker.vertex_size = 0 # no vertexes
@ -142,17 +298,64 @@ class DragBoxPlugin(pya.Plugin):
self.marker.color = self.color | 0xff000000
else:
self.marker.color = 0 # auto
def _update_box(self, box):
"""
Updates the box with the given value and updates the marker.
This method is bound to the focus page handler when needed.
"""
self.box = box
self._update_marker()
def focus_page_open(self, fp):
"""
overloaded callback: the focus page is requested
"""
# stop unless dragging
if self.marker is None:
return
# configure the focus page and show it:
# the page will call the handler of "update_box" to commit
# changes to the box
fp.box = self.box
fp.pfix = self.start_point
fp.update_box = self._update_box
ret = fp.show()
fp.update_box = None
if ret == 1:
# accepted: stop dragging now, we are done.
self._finish()
return ret
def activated(self):
# plugin is activated - i.e. the mode is selected
"""
overloaded callback:
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
"""
overloaded callback:
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):
"""
overloaded callback:
a mouse button was clicked
"""
if prio:
# first-level event: start a new box or
# stop dragging it and freeze the box
@ -166,13 +369,18 @@ class DragBoxPlugin(pya.Plugin):
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)
self._update_box(pya.DBox(self.start_point, p))
self._finish()
return True
return False
def mouse_moved_event(self, p, buttons, prio):
"""
overloaded callback:
the mouse was moved
"""
if prio:
# first-level event: if not dragging, provide a
# mouse cursor for tracking. If dragging, update
@ -191,13 +399,14 @@ class DragBoxPlugin(pya.Plugin):
# 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):
"""
Implements a "plugin factory".
The purpose of this object is to create a plugin object
and corresponding UI objects.
"""
def __init__(self):
super(DragBoxPluginFactory, self).__init__()
self.has_tool_entry = True
@ -207,15 +416,25 @@ class DragBoxPluginFactory(pya.PluginFactory):
self.register(-1000, "drag_box", "Drag Box")
def create_config_pages(self):
"""
Called to create the configuration pages
"""
self.add_config_page(DragBoxConfigPage())
def create_editor_options_pages(self):
"""
Called to create the editor options pages
"""
self.add_editor_options_page(DragBoxEditorOptionsPage())
self.add_editor_options_page(DragBoxFocusPage())
def create_plugin(self, manager, root, view):
"""
Creates the plugin
"""
return DragBoxPlugin(view)
# Create the singleton instance - as we register it,
# Creates the singleton instance - as we register it,
# it is not garbage collected
DragBoxPluginFactory()
</text>