klayout/src/lay/lay/macro_templates/drag_box_sample_python.lym

444 lines
12 KiB
XML

<?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). 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"
# 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)
"""
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)
# 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):
"""
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)
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):
"""
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_marker = None
self.box = None
self.start_point = None
self.view = view
self.color = None
self.width = 1
def configure(self, name, value):
"""
This method receives configuration callbacks
"""
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_marker ]:
if marker is not None:
marker._destroy()
self.marker = 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
"""
if self.marker is None:
self.marker = pya.Marker(self.view)
self._configure_marker()
self.marker.set(self.box)
def _configure_marker(self):
"""
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
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 _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):
"""
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 = self.focus_page()
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):
"""
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):
"""
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
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._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
# 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
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
# 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):
"""
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)
# Creates the singleton instance - as we register it,
# it is not garbage collected
DragBoxPluginFactory()
</text>
</klayout-macro>