From f1c16a02426b8e9507b901bb57d46f7e9e53491d Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sat, 30 Aug 2025 14:53:46 +0200 Subject: [PATCH] Adding samples for new plugin features, doc update. --- src/doc/doc/programming/application_api.xml | 30 +- src/lay/lay/layMacroTemplates.qrc | 2 + .../lay/macro_templates/drag_box_sample.lym | 258 ++++++++++++++++++ .../drag_box_sample_python.lym | 222 +++++++++++++++ src/lay/lay/macro_templates/index.txt | 2 + 5 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 src/lay/lay/macro_templates/drag_box_sample.lym create mode 100644 src/lay/lay/macro_templates/drag_box_sample_python.lym diff --git a/src/doc/doc/programming/application_api.xml b/src/doc/doc/programming/application_api.xml index 836178481..d4fcf1ebe 100644 --- a/src/doc/doc/programming/application_api.xml +++ b/src/doc/doc/programming/application_api.xml @@ -976,7 +976,7 @@ marker.destroy

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:

@@ -1048,5 +1052,29 @@ marker.destroy over the mouse in certain circumstances and is supposed to put the plugin into a "watching" instead of "dragging" state.

+

+ A plugin may also create markers for visual feedback and highlights. This can be done explicitly + using marker objects () or in a application-defined fashion by generating + mouse cursors. The API functions for this purpose are , + and . These + functions provide cursors and highlights that match the visual effects of other plugins and + interface with the mouse tracking feature of the application. +

+ +

+ 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 + and . While the first + method provides grid and angle snapping, the second also implements snapping to layout objects. +

+ +

+ 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. +

+ diff --git a/src/lay/lay/layMacroTemplates.qrc b/src/lay/lay/layMacroTemplates.qrc index 79bdbc8ad..cae26b9ed 100644 --- a/src/lay/lay/layMacroTemplates.qrc +++ b/src/lay/lay/layMacroTemplates.qrc @@ -5,6 +5,8 @@ macro_templates/new_macro.lym macro_templates/new_text_file.txt macro_templates/new_ruby_file.rb + macro_templates/drag_box_sample.lym + macro_templates/drag_box_sample_python.lym macro_templates/pcell.lym macro_templates/pcell_sample.lym macro_templates/qt_designer.lym diff --git a/src/lay/lay/macro_templates/drag_box_sample.lym b/src/lay/lay/macro_templates/drag_box_sample.lym new file mode 100644 index 000000000..7d29c2be1 --- /dev/null +++ b/src/lay/lay/macro_templates/drag_box_sample.lym @@ -0,0 +1,258 @@ + + + A plugin sample\nThis sample provides a box drawing feature and demonstrates UI components and snapping + general + true + false + false + + ruby + # 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 < 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 < 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 < 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 && 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 < 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 + diff --git a/src/lay/lay/macro_templates/drag_box_sample_python.lym b/src/lay/lay/macro_templates/drag_box_sample_python.lym new file mode 100644 index 000000000..17d2b4a7b --- /dev/null +++ b/src/lay/lay/macro_templates/drag_box_sample_python.lym @@ -0,0 +1,222 @@ + + + A plugin sample\nThis sample provides a box drawing feature and demonstrates UI components and snapping + general + true + false + false + + python + # 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() + + diff --git a/src/lay/lay/macro_templates/index.txt b/src/lay/lay/macro_templates/index.txt index a446546d7..ae40bdd3d 100644 --- a/src/lay/lay/macro_templates/index.txt +++ b/src/lay/lay/macro_templates/index.txt @@ -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