diff --git a/src/lay/lay/macro_templates/drag_box_sample.lym b/src/lay/lay/macro_templates/drag_box_sample.lym index f747fded3..fbcea2c12 100644 --- a/src/lay/lay/macro_templates/drag_box_sample.lym +++ b/src/lay/lay/macro_templates/drag_box_sample.lym @@ -8,29 +8,39 @@ 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). +# (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 < 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 < 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 < 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 < 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 < 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 < t - 1e-10 + b = t - y + else + t = b + y + end + + if @box.left < 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 < 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 < 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 < 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 < 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 < RBA::Plugin end end + # clears all markers def _clear_marker - # clears all markers - [ @marker, @last_box ].each { |m| m && m._destroy } + [ @marker, @last_marker ].each { |marker| marker && 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 < 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 => 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 => 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 < 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 < 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 +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 index 17d2b4a7b..3eb35164b 100644 --- a/src/lay/lay/macro_templates/drag_box_sample_python.lym +++ b/src/lay/lay/macro_templates/drag_box_sample_python.lym @@ -8,15 +8,23 @@ 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). - +# (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 < t - 1e-10: + b = t - y + else: + t = b + y + + if self.box.left < 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()