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:
@@ -991,6 +991,10 @@ marker.destroy
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.
+ - Widgets: The plugin factory can provide widgets for the configuration dialog ('File/Setup') and the
+ editor options dock. Respective callbacks are
+ and .
+
@@ -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