class Box(Component): color = ColorTrait("white") border_color = ColorTrait("black") border_size = border_size_trait def _draw_mainlayer(self, gc, view_bounds=None, mode="default"): "Draw the box background in a specified graphics context" # Set up all the control variables for quick access: bs = self.border_size bsd = bs + bs bsh = bs / 2.0 x, y = self.position dx, dy = self.bounds with gc: # Fill the background region (if required); color = self.color_ if color is not transparent_color: gc.set_fill_color(color) gc.draw_rect((x + bs, y + bs, dx - bsd, dy - bsd), FILL) # Draw the border (if required): if bs > 0: border_color = self.border_color_ if border_color is not transparent_color: gc.set_stroke_color(border_color) gc.set_line_width(bs) gc.draw_rect((x + bsh, y + bsh, dx - bs, dy - bs), STROKE) return
class Legend(HasTraits): measured_color = ColorTrait('purple') loaded_color = ColorTrait('green') def draw(self, component, gc): r = 6 gc.set_font(str_to_font('modern 10')) with gc: gc.translate_ctm(component.x + 20, component.y2 - 50) # monitor with gc: gc.set_line_width(1) gc.set_fill_color((0, 0, 0)) gc.arc(0, 30, r, 0, 360) x = 0 y = 30 gc.move_to(x, y - r) gc.line_to(x, y + r) gc.stroke_path() gc.move_to(x - r, y) gc.line_to(x + r, y) gc.stroke_path() gc.set_text_position(10, 26) gc.show_text('Monitor') # irradiated with gc: gc.set_line_width(1) gc.set_fill_color(self.loaded_color_) gc.arc(0, 16, r, 0, 360) gc.fill_path() gc.set_text_position(10, 12) gc.show_text('Irradiated') # measured with gc: gc.set_line_width(1) gc.set_fill_color(self.loaded_color_) gc.arc(0, 0, r, 0, 360) gc.fill_path() with gc: gc.set_line_width(2) gc.set_stroke_color(self.measured_color_) gc.arc(0, 0, r + 1, 0, 360) gc.stroke_path() gc.set_text_position(10, -5) gc.show_text('Measured')
class LatestOverlay(AbstractOverlay): data_position = None color = ColorTrait('transparent') # The color of the outline to draw around the marker. outline_color = ColorTrait('orange') def overlay(self, other_component, gc, view_bounds=None, mode="normal"): with gc: pts = self.component.map_screen(self.data_position) render_markers(gc, pts, 'circle', 5, self.color_, 2, self.outline_color_)
class LimitOverlay(AbstractOverlay): tool = LimitsTool label = Instance(XYPlotLabel) color = ColorTrait('red') label_manual_color = ColorTrait('tomato') label_bgcolor = ColorTrait('lightblue') def _label_default(self): return XYPlotLabel( component=self.component, bgcolor=self.label_bgcolor, # border_width = LabelDelegate border_color='black', # border_visible = LabelDelegate border_visible=True) def overlay(self, other_component, gc, view_bounds=None, mode="normal"): tool = self.tool y, y2 = other_component.y, other_component.y2 x, x2 = other_component.x, other_component.x2 if tool.ruler_pos: a, b = tool.ruler_pos if tool.orientation == 'x': x, x2 = a, a self.label.sx = x self.label.sy = b + 10 else: y, y2 = b, b self.label.sx = a + 20 self.label.sy = y with gc: gc.set_stroke_color(self.color_) gc.set_line_width(2) gc.lines([(x, y), (x2, y2)]) gc.stroke_path() if tool.entered_value: self.label.bgcolor = self.label_manual_color v = tool.entered_value else: self.label.bgcolor = self.label_bgcolor v = floatfmt(tool.ruler_data_pos) self.label.text = '{}: {}'.format(tool.orientation.upper(), v) self.label.overlay(other_component, gc, view_bounds=None, mode="normal")
class ColorStop(HasStrictTraits): """ A point on a gradient with a fixed color. """ #: The position of the color stop in the gradient, between 0.0 and 1.0. offset = Range(0.0, 1.0, update=True) #: The color at the color stop. color = ColorTrait("transparent", update=True) #: A trait which fires when the color stop is updated. updated = Event() def to_array(self): """ Return an array which represents the color stop. This is the raw form of the color stop required by Kiva. Returns ------- stop_array : numpy array Return an array of (offset, r, b, b, a). """ return array([self.offset] + list(self.color_)) @observe("+update") def observe_update_traits(self, event): self.updated = True
class ComponentEditor(BasicEditorFactory): """ wxPython editor factory for Enable components. """ #--------------------------------------------------------------------------- # Trait definitions: #--------------------------------------------------------------------------- # The class used to create all editor styles (overrides BasicEditorFactory). klass = _ComponentEditor # The background color for the window bgcolor = ColorTrait('sys_window') # The default size of the Window wrapping this Enable component size = Tuple((400, 400)) # Convenience function for accessing the width width = Property # Convenience function for accessing the width height = Property def _get_width(self): return self.size[0] def _set_width(self, width): self.size = (width, self.size[1]) def _get_height(self): return self.size[1] def _set_height(self, height): self.size = (self.size[0], height)
class Annotater(Component): color = ColorTrait((0.0, 0.0, 0.0, 0.2)) style = Trait("rectangular", TraitPrefixList(["rectangular", 'freehand'])) annotation = Event traits_view = View(Group('<component>', id='component'), Group('<links>', id='links'), Group('color', 'style', id='annotater', style='custom')) #--------------------------------------------------------------------------- # Mouse event handlers #--------------------------------------------------------------------------- def _left_down_changed(self, event): event.handled = True self.window.mouse_owner = self self._cur_x, self._cur_y = event.x, event.y self._start_x, self._start_y = event.x, event.y return def _left_up_changed(self, event): event.handled = True self.window.mouse_owner = None if self.xy_in_bounds(event): self.annotation = (min(self._start_x, event.x), min(self._start_y, event.y), abs(self._start_x - event.x), abs(self._start_y - event.y)) self._start_x = self._start_y = self._cur_x = self._cur_y = None self.redraw() return def _mouse_move_changed(self, event): event.handled = True if self._start_x is not None: x = max(min(event.x, self.right - 1.0), self.x) y = max(min(event.y, self.top - 1.0), self.y) if (x != self._cur_x) or (y != self._cur_y): self._cur_x, self._cur_y = x, y self.redraw() return #--------------------------------------------------------------------------- # "Component" interface #--------------------------------------------------------------------------- def _draw(self, gc): "Draw the contents of the control" if self._start_x is not None: with gc: gc.set_fill_color(self.color_) gc.begin_path() gc.rect(min(self._start_x, self._cur_x), min(self._start_y, self._cur_y), abs(self._start_x - self._cur_x), abs(self._start_y - self._cur_y)) gc.fill_path() return
class TextFieldStyle(HasTraits): """ This class holds style settings for rendering an EnableTextField. fixme: See docstring on EnableBoxStyle """ # The color of the text text_color = ColorTrait((0, 0, 0, 1.0)) # The font for the text (must be monospaced!) font = KivaFont("Courier 12") # The color of highlighted text highlight_color = ColorTrait((0.65, 0, 0, 1.0)) # The background color of highlighted items highlight_bgcolor = ColorTrait("lightgray") # The font for flagged text (must be monospaced!) highlight_font = KivaFont("Courier 14 bold") # The number of pixels between each line line_spacing = Int(3) # Space to offset text from the widget's border text_offset = Int(5) # Cursor properties cursor_color = ColorTrait((0, 0, 0, 1)) cursor_width = Int(2) # Drawing properties border_visible = Bool(False) border_color = ColorTrait((0, 0, 0, 1)) bgcolor = ColorTrait((1, 1, 1, 1))
class ANumericItem(HasPrivateTraits): """ Defines an abstract base class for accessing an item in a NumericContext. """ #--------------------------------------------------------------------------- # Trait definitions: #--------------------------------------------------------------------------- # The context containing this item: context = Instance(ANumericContext) # The list of groups the item belongs to: groups = List # Name of the context trait for this item (implemented in each subclass): #name = Property # The current value of the associated numeric array (implemented in each # subclass): #data = Property # Value to be substituted in reduction filters when 'use_value' is True: value = Any(nan) #-- View related --------------------------------------------------------------- # User interface label: label = Str # String formatting rule: format = Str('%.3f') # Foreground color (intended use: text color, plot line color): foreground_color = ColorTrait('black') # Background color (intended use: text background color, plot fill color): background_color = ColorTrait('white')
class SelectorOverlay(AbstractOverlay): tool = Any color = ColorTrait('green') def overlay(self, other_component, gc, view_bounds=None, mode="normal"): if self.tool.event_state == 'select': with gc: w, h = self.component.bounds x, y = self.component.x, self.component.y gc.set_stroke_color(self.color_) gc.set_line_width(4) gc.rect(x, y, w, h) gc.stroke_path()
class ColorBrush(Brush): """ A simple brush that paints a solid color. """ #: The color to brush the region with. color = ColorTrait("transparent", update=True) def set_brush(self, gc): """ Apply the brush settings to a graphics context. This sets the fill color of the GC to the specified color. Parameters ---------- gc : graphics context The graphics context to use the brush with. """ gc.set_fill_color(self.color_)
class GuideOverlay(AbstractOverlay): """ draws a horizontal or vertical line at the specified value """ orientation = Enum('h', 'v') value = Float color = ColorTrait("red") line_style = LineStyle('dash') line_width = Float(1) display_value = False label = Instance(Label, ()) def overlay(self, component, gc, view_bounds=None, mode='normal'): with gc: gc.clip_to_rect(component.x, component.y, component.width, component.height) with gc: gc.set_line_dash(self.line_style_) gc.set_line_width(self.line_width) gc.set_stroke_color(self.color_) gc.begin_path() if self.orientation == 'h': x1 = component.x x2 = component.x2 y1 = y2 = component.value_mapper.map_screen(self.value) else: y1 = component.y y2 = component.y2 x1 = x2 = component.index_mapper.map_screen(self.value) gc.move_to(x1, y1) gc.line_to(x2, y2) gc.stroke_path() if self.display_value: with gc: l = self.label l.text = '{:0.5f}'.format(self.value) l.position = self.label_position l.draw(gc)
class SelectableBox(Box): """ A box that can be selected and renders in a different color """ selected = Bool selected_color = ColorTrait('green') def select(self, selected): self.selected = selected def _selected_changed(self): self.request_redraw() def _draw_mainlayer(self, gc, view_bounds=None, mode="default"): "Draw the box background in a specified graphics context" # Set up all the control variables for quick access: bs = self.border_size bsd = bs + bs bsh = bs / 2.0 x, y = self.position dx, dy = self.bounds with gc: if self.selected: color = self.selected_color_ else: color = self.color_ if color is not transparent_color: gc.set_fill_color(color) gc.draw_rect((x + bs, y + bs, dx - bsd, dy - bsd), FILL) # Draw the border (if required): if bs > 0: border_color = self.border_color_ if border_color is not transparent_color: gc.set_stroke_color(border_color) gc.set_line_width(bs) gc.draw_rect((x + bsh, y + bsh, dx - bs, dy - bs), STROKE) return
class GuideOverlay(AbstractOverlay): """ draws a horizontal or vertical line at the specified value """ orientation = Enum('h', 'v') value = Float color = ColorTrait("red") line_style = LineStyle('dash') line_width = Float(1) def overlay(self, component, gc, view_bounds=None, mode='normal'): """ """ gc.save_state() gc.clip_to_rect(self.component.x, self.component.y, self.component.width, self.component.height) gc.set_line_dash(self.line_style_) gc.set_line_width(self.line_width) gc.set_stroke_color(self.color_) gc.begin_path() if self.orientation == 'h': x1 = self.component.x x2 = self.component.x2 y1 = y2 = self.component.value_mapper.map_screen(self.value) else: y1 = self.component.y y2 = self.component.y2 x1 = x2 = self.component.index_mapper.map_screen(self.value) gc.move_to(x1, y1) gc.line_to(x2, y2) gc.stroke_path() gc.restore_state()
class ComponentEditor(BasicEditorFactory): """ TraitsUI editor factory for Enable components. """ # ------------------------------------------------------------------------- # Trait definitions: # ------------------------------------------------------------------------- # The class used to create all editor styles (overrides BasicEditorFactory) klass = _ComponentEditor #: The background color for the window bgcolor = ColorTrait("sys_window") #: When available, use HiDPI for GraphicsContext rasterization. high_resolution = Bool(True) #: The default size of the Window wrapping this Enable component size = Tuple((400, 400)) #: Convenience function for accessing the width width = Property #: Convenience function for accessing the width height = Property def _get_width(self): return self.size[0] def _set_width(self, width): self.size = (width, self.size[1]) def _get_height(self): return self.size[1] def _set_height(self, height): self.size = (self.size[0], height)
class tcPlot(BarPlot): """custom plot to draw the timechart probably not very 'chacotic' We draw the chart as a whole """ # The text of the axis title. title = Trait('', Str, Unicode) #May want to add PlotLabel option # The font of the title. title_font = KivaFont('modern 9') # The font of the title. title_font_large = KivaFont('modern 15') # The font of the title. title_font_huge = KivaFont('modern 20') # The spacing between the axis line and the title title_spacing = Trait('auto', 'auto', Float) # The color of the title. title_color = ColorTrait("black") not_on_screen = List on_screen = List options = TimeChartOptions() range_tools = RangeSelectionTools() redraw_timer = None def invalidate(self): self.invalidate_draw() self.request_redraw() def immediate_invalidate(self): self.invalidate_draw() self.request_redraw_delayed() def request_redraw_delayed(self): self.redraw_timer.Stop() BarPlot.request_redraw(self) def request_redraw(self): if self.redraw_timer == None: self.redraw_timer = timer.Timer(30, self.request_redraw_delayed) self.redraw_timer.Start() def auto_zoom_y(self): if self.value_range.high != self.max_y + 1 or self.value_range.low != self.min_y: self.value_range.high = self.max_y + 1 self.value_range.low = self.min_y self.invalidate_draw() self.request_redraw() def _gather_timechart_points(self, start_ts, end_ts, y, step): low_i = searchsorted(end_ts, self.index_mapper.range.low) high_i = searchsorted(start_ts, self.index_mapper.range.high) if low_i == high_i: return array([]) start_ts = start_ts[low_i:high_i] end_ts = end_ts[low_i:high_i] points = column_stack( (start_ts, end_ts, zeros(high_i - low_i) + (y + step), ones(high_i - low_i) + (y - step), array(list(range(low_i, high_i))))) return points def _draw_label(self, gc, label, text, x, y): label.text = text l_w, l_h = label.get_width_height(gc) offset = array((x, y - l_h / 2)) gc.translate_ctm(*offset) label.draw(gc) gc.translate_ctm(*(-offset)) return l_w, l_h def _draw_timechart(self, gc, tc, label, base_y): bar_middle_y = self.first_bar_y + (base_y + .5) * self.bar_height points = self._gather_timechart_points(tc.start_ts, tc.end_ts, base_y, .2) overview = None if self.options.use_overview: if points.size > 500: overview = tc.get_overview_ts(self.overview_threshold) points = self._gather_timechart_points(overview[0], overview[1], base_y, .2) if self.options.remove_pids_not_on_screen and points.size == 0: return 0 if bar_middle_y + self.bar_height < self.y or bar_middle_y - self.bar_height > self.y + self.height: return 1 #quickly decide we are not on the screen self._draw_bg(gc, base_y, tc.bg_color) # we are too short in height, dont display all the labels if self.last_label >= bar_middle_y: # draw label l_w, l_h = self._draw_label(gc, label, tc.name, self.x, bar_middle_y) self.last_label = bar_middle_y - 8 else: l_w, l_h = 0, 0 if points.size != 0: # draw the middle line from end of label to end of screen if l_w != 0: # we did not draw label because too short on space gc.set_alpha(0.2) gc.move_to(self.x + l_w, bar_middle_y) gc.line_to(self.x + self.width, bar_middle_y) gc.draw_path() gc.set_alpha(0.5) # map the bars start and stop locations into screen space lower_left_pts = self.map_screen(points[:, (0, 2)]) upper_right_pts = self.map_screen(points[:, (1, 3)]) bounds = upper_right_pts - lower_left_pts if overview: # critical path, we only draw unicolor rects #calculate the mean color #print points.size gc.set_fill_color(get_aggcolor_by_id(get_color_id("overview"))) gc.set_alpha(.9) rects = column_stack((lower_left_pts, bounds)) gc.rects(rects) gc.draw_path() else: # lets display them more nicely rects = column_stack((lower_left_pts, bounds, points[:, (4)])) last_t = -1 gc.save_state() for x, y, sx, sy, i in rects: t = tc.types[int(i)] if last_t != t: # only draw when we change color. agg will then simplify the path # note that a path only can only have one color in agg. gc.draw_path() gc.set_fill_color(get_aggcolor_by_id(int(t))) last_t = t gc.rect(x, y, sx, sy) # draw last path gc.draw_path() if tc.has_comments: for x, y, sx, sy, i in rects: if sx < 8: # not worth calculatig text size continue label.text = tc.get_comment(i) l_w, l_h = label.get_width_height(gc) if l_w < sx: offset = array( (x, y + self.bar_height * .6 / 2 - l_h / 2)) gc.translate_ctm(*offset) label.draw(gc) gc.translate_ctm(*(-offset)) if tc.max_latency > 0: # emphase events where max_latency is reached ts = tc.max_latency_ts if ts.size > 0: points = self._gather_timechart_points(ts, ts, base_y, 0) if points.size > 0: # map the bars start and stop locations into screen space gc.set_alpha(1) lower_left_pts = self.map_screen(points[:, (0, 2)]) upper_right_pts = self.map_screen(points[:, (1, 3)]) bounds = upper_right_pts - lower_left_pts rects = column_stack((lower_left_pts, bounds)) gc.rects(rects) gc.draw_path() #print('gc.draw_path() ', __file__) #assert True, 'draw_path' return 1 def _draw_freqchart(self, gc, tc, label, y): self._draw_bg(gc, y, tc.bg_color) low_i = searchsorted(tc.start_ts, self.index_mapper.range.low) high_i = searchsorted(tc.start_ts, self.index_mapper.range.high) if low_i > 0: low_i -= 1 if high_i < len(tc.start_ts): high_i += 1 if low_i >= high_i - 1: return array([]) start_ts = tc.start_ts[low_i:high_i - 1] end_ts = tc.start_ts[low_i + 1:high_i] values = (tc.types[low_i:high_i - 1] / (float(tc.max_types))) + y starts = column_stack((start_ts, values)) ends = column_stack((end_ts, values)) starts = self.map_screen(starts) ends = self.map_screen(ends) gc.begin_path() gc.line_set(starts, ends) gc.stroke_path() for i in range(len(starts)): x1, y1 = starts[i] x2, y2 = ends[i] sx = x2 - x1 if sx > 8: label.text = str(tc.types[low_i + i]) l_w, l_h = label.get_width_height(gc) if l_w < sx: if x1 < 0: x1 = 0 offset = array((x1, y1)) gc.translate_ctm(*offset) label.draw(gc) gc.translate_ctm(*(-offset)) def _draw_wake_ups(self, gc, processes_y): low_i = searchsorted(self.proj.wake_events['time'], self.index_mapper.range.low) high_i = searchsorted(self.proj.wake_events['time'], self.index_mapper.range.high) gc.set_stroke_color((0, 0, 0, .6)) for i in range(low_i, high_i): waker, wakee, ts = self.proj.wake_events[i] if wakee in processes_y and waker in processes_y: y1 = processes_y[wakee] y2 = processes_y[waker] x, y = self.map_screen(array((ts, y1))) gc.move_to(x, y) y2 = processes_y[waker] x, y = self.map_screen(array((ts, y2))) gc.line_to(x, y) x, y = self.map_screen(array((ts, (y1 + y2) / 2))) if y1 > y2: y += 5 dy = -5 else: y -= 5 dy = +5 gc.move_to(x, y) gc.line_to(x - 3, y + dy) gc.move_to(x, y) gc.line_to(x + 3, y + dy) gc.draw_path() def _draw_bg(self, gc, y, color): gc.set_alpha(1) gc.set_line_width(0) gc.set_fill_color(color) this_bar_y = self.map_screen(array((0, y)))[1] gc.rect(self.x, this_bar_y, self.width, self.bar_height) gc.draw_path() gc.set_line_width(self.line_width) gc.set_alpha(0.5) def _draw_plot(self, gc, view_bounds=None, mode="normal"): gc.save_state() gc.clip_to_rect(self.x, self.y, self.width, self.height) gc.set_antialias(1) gc.set_stroke_color(self.line_color_) gc.set_line_width(self.line_width) self.first_bar_y = self.map_screen(array((0, 0)))[1] self.last_label = self.height + self.y self.bar_height = self.map_screen(array((0, 1)))[1] - self.first_bar_y self.max_y = y = self.proj.num_cpu * 2 + self.proj.num_process - 1 if self.bar_height > 15: font = self.title_font_large else: font = self.title_font label = Label(text="", font=font, color=self.title_color, rotate_angle=0) # we unmap four pixels on screen, and find the nearest greater power of two # this by rounding the log2, and then exponentiate again # as the overview data is cached, this avoids using too much memory four_pixels = self.index_mapper.map_data(array((0, 4))) if len(four_pixels) == 1: self.overview_threshold = 1 << int( log(1 + int(four_pixels[0] - four_pixels[0]), 2)) else: self.overview_threshold = 1 << int( log(1 + int(four_pixels[1] - four_pixels[0]), 2)) for i in range(len(self.proj.c_states)): tc = self.proj.c_states[i] if self.options.show_c_states: self._draw_timechart(gc, tc, label, y) y -= 1 tc = self.proj.p_states[i] if self.options.show_p_states: self._draw_freqchart(gc, tc, label, y) y -= 1 processes_y = {0xffffffffffffffff: y + 1} not_on_screen = [] on_screen = [] for tc in self.proj.processes: if tc.show == False: continue processes_y[(tc.comm, tc.pid)] = y + .5 if self._draw_timechart( gc, tc, label, y) or not self.options.remove_pids_not_on_screen: y -= 1 on_screen.append(tc) else: not_on_screen.append(tc) self.not_on_screen = not_on_screen self.on_screen = on_screen if self.options.show_wake_events: self._draw_wake_ups(gc, processes_y) message = "" if self.proj.filename == "dummy": message = "please load a trace file in the 'file' menu" elif len(self.proj.processes) == 0: message = "no processes??! is your trace empty?" if message: label.text = message label.font = self.title_font_huge gc.translate_ctm(100, (self.y + self.height) / 2) label.draw(gc) gc.restore_state() self.min_y = y if self.options.auto_zoom_y: self.options.auto_zoom_timer.Start() def _on_hide_others(self): for i in self.not_on_screen: i.show = False self.invalidate_draw() self.request_redraw() def _on_hide_onscreen(self): for i in self.on_screen: i.show = False self.invalidate_draw() self.request_redraw()
class Polygon(Component): """ A filled polygon component. """ #-------------------------------------------------------------------------- # Trait definitions. #-------------------------------------------------------------------------- # The background color of this polygon. background_color = ColorTrait("white") # The color of the border of this polygon. border_color = ColorTrait("black") # The dash pattern to use for this polygon. border_dash = Any # The thickness of the border of this polygon. border_size = Trait(1, border_size_trait) # Event fired when the polygon is "complete". complete = Event # The rule to use to determine the inside of the polygon. inside_rule = Trait('winding', { 'winding': FILL_STROKE, 'oddeven': EOF_FILL_STROKE }) # The points that make up this polygon. model = Instance(PolygonModel, ()) # Convenience property to access the model's points. points = Property # The color of each vertex. vertex_color = ColorTrait("black") # The size of each vertex. vertex_size = Float(3.0) traits_view = View( Group('<component>', id='component'), Group('<links>', id='links'), Group('background_color', '_', 'border_color', '_', 'border_size', id='Box', style='custom')) colorchip_map = {'color': 'color', 'alt_color': 'border_color'} #-------------------------------------------------------------------------- # Traits property accessors #-------------------------------------------------------------------------- def _get_points(self): return self.model.points #-------------------------------------------------------------------------- # 'Polygon' interface #-------------------------------------------------------------------------- def reset(self): "Reset the polygon to the initial state" self.model.reset() self.event_state = 'normal' return #-------------------------------------------------------------------------- # 'Component' interface #-------------------------------------------------------------------------- def _draw_mainlayer(self, gc, view_bounds=None, mode="normal"): "Draw the component in the specified graphics context" self._draw_closed(gc) return #-------------------------------------------------------------------------- # Protected interface #-------------------------------------------------------------------------- def _is_in(self, point): """ Test if the point (an x, y tuple) is within this polygonal region. To perform the test, we use the winding number inclusion algorithm, referenced in the comp.graphics.algorithms FAQ (http://www.faqs.org/faqs/graphics/algorithms-faq/) and described in detail here: http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm """ point_array = array((point, )) vertices = array(self.model.points) winding = self.inside_rule == 'winding' result = points_in_polygon(point_array, vertices, winding) return result[0] #-------------------------------------------------------------------------- # Private interface #-------------------------------------------------------------------------- def _draw_closed(self, gc): "Draw this polygon as a closed polygon" if len(self.model.points) > 2: # Set the drawing parameters. gc.set_fill_color(self.background_color_) gc.set_stroke_color(self.border_color_) gc.set_line_width(self.border_size) gc.set_line_dash(self.border_dash) # Draw the path. gc.begin_path() gc.move_to(self.model.points[0][0] - self.x, self.model.points[0][1] + self.y) offset_points = [(x - self.x, y + self.y) for x, y in self.model.points] gc.lines(offset_points) gc.close_path() gc.draw_path(self.inside_rule_) # Draw the vertices. self._draw_vertices(gc) return def _draw_open(self, gc): "Draw this polygon as an open polygon" if len(self.model.points) > 2: # Set the drawing parameters. gc.set_fill_color(self.background_color_) gc.set_stroke_color(self.border_color_) gc.set_line_width(self.border_size) gc.set_line_dash(self.border_dash) # Draw the path. gc.begin_path() gc.move_to(self.model.points[0][0] - self.x, self.model.points[0][1] + self.y) offset_points = [(x - self.x, y + self.y) for x, y in self.model.points] gc.lines(offset_points) gc.draw_path(self.inside_rule_) # Draw the vertices. self._draw_vertices(gc) return def _draw_vertices(self, gc): "Draw the vertices of the polygon." gc.set_fill_color(self.vertex_color_) gc.set_line_dash(None) offset = self.vertex_size / 2.0 offset_points = [(x + self.x, y + self.y) for x, y in self.model.points] if hasattr(gc, 'draw_path_at_points'): path = gc.get_empty_path() path.rect(-offset, -offset, self.vertex_size, self.vertex_size) gc.draw_path_at_points(offset_points, path, FILL_STROKE) else: for x, y in offset_points: gc.draw_rect((x - offset, y - offset, self.vertex_size, self.vertex_size), FILL) return
class ANumericFilter ( HasPrivateTraits ): """ Defines an abstract base class for all NumericContext filters. """ #--------------------------------------------------------------------------- # Trait definitions: #--------------------------------------------------------------------------- # Name of the filter (implemented as a Property in each subclass): # name = Property # Event fired when filter is modified: updated = Event # Is the filter enabled or disabled? enabled = Bool( True, event = 'modified' ) # Should the value, isbit and color traits be used? use_value = Bool( False, event = 'modified' ) # Value to scale filter results by: value = Int( 1, event = 'modified' ) # Is the value a bit number? is_bit = Bool( False, event = 'modified' ) #-- View related --------------------------------------------------------------- # Foreground color: foreground_color = ColorTrait( 'black', event = 'modified' ) # Background color: background_color = ColorTrait( 'white', event = 'modified' ) #--------------------------------------------------------------------------- # Handles the 'modified' pseudo-event being fired: #--------------------------------------------------------------------------- def _modified_changed ( self ): """ Handles the 'modified' pseudo-event being fired. """ self.updated = True #--------------------------------------------------------------------------- # Evaluates the result of the filter for the specified context: #--------------------------------------------------------------------------- def __call__ ( self, context ): """ Evaluates the result of the filter for the specified context. """ if self.enabled: result = self.evaluate( context ) if (not self.use_value) or (result is None): return result result = not_equal( result, 0 ) scale = self.value if self.is_bit: scale = (1 << self.value) if scale == 1: return result return result * scale return None #--------------------------------------------------------------------------- # Sets the 'updated' trait True if the filter is affected by a change # to any of the 'names' values in 'context'. Also returns whether the # 'updated' trait was set or not: #--------------------------------------------------------------------------- def context_has_changed ( self, context, names ): """ Handles an update to the specified names within a specified context. """ if self.context_changed( context, names ): self.updated = True return True return False #-- Overriddable Methods --------------------------------------------------- #--------------------------------------------------------------------------- # Evaluates the result of the filter for the specified context: # # This method should be overridden by subclasses. #--------------------------------------------------------------------------- def evaluate ( self, context ): """ Evaluates the result of the filter for the specified context. """ return None #--------------------------------------------------------------------------- # Returns whether an update to a context affects the filter. It should # return True if a change to any of the 'name' values in the specified # 'context' affects the filter, and False otherwise. # # This method can be optionally overridden by a subclass if the filter # it defines has dependencies on the contents of a context. #--------------------------------------------------------------------------- def context_changed ( self, context, names ): """ Handles an update to the specified names within a specified context. """ return False #--------------------------------------------------------------------------- # Returns the string representation of the filter: # # This method can be optionally overridden by subclasses if they wish to # return a different string representation of the filter. #--------------------------------------------------------------------------- def __str__ ( self ): """ Returns the string representation of the filter. """ return self.name
class ViewportZoomTool(AbstractOverlay, ToolHistoryMixin, BaseZoomTool): """ Selects a range along the index or value axis. The user left-click-drags to select a region to zoom in. Certain keyboard keys are mapped to performing zoom actions as well. Implements a basic "zoom stack" so the user move go backwards and forwards through previous zoom regions. """ # The selection mode: # # range: # Select a range across a single index or value axis. # box: # Perform a "box" selection on two axes. tool_mode = Enum("range", "box") #Enum("box", "range") # Is the tool always "on"? If True, left-clicking always initiates # a zoom operation; if False, the user must press a key to enter zoom mode. always_on = Bool(False) #------------------------------------------------------------------------- # Zoom control #------------------------------------------------------------------------- # The axis to which the selection made by this tool is perpendicular. This # only applies in 'range' mode. axis = Enum("x", "y") #------------------------------------------------------------------------- # Interaction control #------------------------------------------------------------------------- # Enable the mousewheel for zooming? enable_wheel = Bool(True) # The mouse button that initiates the drag. drag_button = Enum("left", "right") # Conversion ratio from wheel steps to zoom factors. wheel_zoom_step = Float(.25) # The key press to enter zoom mode, if **always_on** is False. Has no effect # if **always_on** is True. enter_zoom_key = Instance(KeySpec, args=("z", )) # The key press to leave zoom mode, if **always_on** is False. Has no effect # if **always_on** is True. exit_zoom_key = Instance(KeySpec, args=("z", )) # Disable the tool after the zoom is completed? disable_on_complete = Bool(True) # The minimum amount of screen space the user must select in order for # the tool to actually take effect. minimum_screen_delta = Int(10) # The most that this tool will zoom in on the target. Since zoom is the # ratio of the original bounds to the new bounds, a max_zoom value of 2.0 # would make the tool stop once it had zoomed into a region half the size # of the original bounds. max_zoom = Float(inf) # The most that this tool will zoom out from the target. For example, # a min_zoom of 0.2 would prevent the tool from showing a view zoomed # out more than 5 times from the original bounds. min_zoom = Float(-inf) #------------------------------------------------------------------------- # Appearance properties (for Box mode) #------------------------------------------------------------------------- # The pointer to use when drawing a zoom box. pointer = "magnifier" # The color of the selection box. color = ColorTrait("lightskyblue") # The alpha value to apply to **color** when filling in the selection # region. Because it is almost certainly useless to have an opaque zoom # rectangle, but it's also extremely useful to be able to use the normal # named colors from Enable, this attribute allows the specification of a # separate alpha value that replaces the alpha value of **color** at draw # time. alpha = Trait(0.4, None, Float) # The color of the outside selection rectangle. border_color = ColorTrait("dodgerblue") # The thickness of selection rectangle border. border_size = Int(1) # The possible event states of this zoom tool. event_state = Enum("normal", "selecting") #------------------------------------------------------------------------ # Key mappings #------------------------------------------------------------------------ # The key that cancels the zoom and resets the view to the original defaults. cancel_zoom_key = Instance(KeySpec, args=("Esc", )) #------------------------------------------------------------------------ # Private traits #------------------------------------------------------------------------ # If **always_on** is False, this attribute indicates whether the tool # is currently enabled. _enabled = Bool(False) # the original numerical screen ranges _orig_position = Trait(None, List, Float) _orig_bounds = Trait(None, List, Float) # The (x,y) screen point where the mouse went down. _screen_start = Trait(None, None, Tuple) # The (x,,y) screen point of the last seen mouse move event. _screen_end = Trait(None, None, Tuple) def __init__(self, component=None, *args, **kw): # Support AbstractController-style constructors so that this can be # handed in the component it will be overlaying in the constructor # without using kwargs. self.component = component super(ViewportZoomTool, self).__init__(*args, **kw) self._reset_state_to_current() if self.tool_mode == "range": i = self._get_range_index() self._orig_position = self.component.view_position[i] self._orig_bounds = self.component.view_bounds[i] else: self._orig_position = self.component.view_position self._orig_bounds = self.component.view_bounds return def enable(self, event=None): """ Provides a programmatic way to enable this tool, if **always_on** is False. Calling this method has the same effect as if the user pressed the **enter_zoom_key**. """ if self.component.active_tool != self: self.component.active_tool = self self._enabled = True if event and event.window: event.window.set_pointer(self.pointer) return def disable(self, event=None): """ Provides a programmatic way to enable this tool, if **always_on** is False. Calling this method has the same effect as if the user pressed the **exit_zoom_key**. """ self.reset() self._enabled = False if self.component.active_tool == self: self.component.active_tool = None if event and event.window: event.window.set_pointer("arrow") return def reset(self, event=None): """ Resets the tool to normal state, with no start or end position. """ self.event_state = "normal" self._screen_start = None self._screen_end = None def deactivate(self, component): """ Called when this is no longer the active tool. """ # Required as part of the AbstractController interface. return self.disable() def normal_left_down(self, event): """ Handles the left mouse button being pressed while the tool is in the 'normal' state. If the tool is enabled or always on, it starts selecting. """ if self.always_on or self._enabled: # we need to make sure that there isn't another active tool that we will # interfere with. if self.drag_button == "left": self._start_select(event) return def normal_right_down(self, event): """ Handles the right mouse button being pressed while the tool is in the 'normal' state. If the tool is enabled or always on, it starts selecting. """ if self.always_on or self._enabled: if self.drag_button == "right": self._start_select(event) return def normal_mouse_wheel(self, event): """ Handles the mouse wheel being used when the tool is in the 'normal' state. Scrolling the wheel "up" zooms in; scrolling it "down" zooms out. self.component is the viewport self.component.component is the canvas """ if self.enable_wheel and event.mouse_wheel != 0: position = self.component.view_position scale = self.component.zoom transformed_x = event.x / scale + position[0] transformed_y = event.y / scale + position[1] # Calculate zoom if event.mouse_wheel < 0: zoom = 1.0 / (1.0 + 0.5 * self.wheel_zoom_step) new_zoom = self.component.zoom * zoom elif event.mouse_wheel > 0: zoom = 1.0 + 0.5 * self.wheel_zoom_step new_zoom = self.component.zoom * zoom if new_zoom < self.min_zoom: new_zoom = self.min_zoom zoom = new_zoom / self.component.zoom elif new_zoom > self.max_zoom: new_zoom = self.max_zoom zoom = new_zoom / self.component.zoom self.component.zoom = new_zoom x_pos = transformed_x - (transformed_x - position[0]) / zoom y_pos = transformed_y - (transformed_y - position[1]) / zoom self.component.set(view_position=[x_pos, y_pos], trait_change_notify=False) bounds = self.component.view_bounds self.component.view_bounds = [bounds[0] / zoom, bounds[1] / zoom] event.handled = True self.component.request_redraw() return def _component_changed(self): self._reset_state_to_current() return #------------------------------------------------------------------------ # Implementation of PlotComponent interface #------------------------------------------------------------------------ def _activate(self): """ Called by PlotComponent to set this as the active tool. """ self.enable() #------------------------------------------------------------------------ # implementations of abstract methods on ToolHistoryMixin #------------------------------------------------------------------------ def _reset_state_to_current(self): """ Clears the tool history, and sets the current state to be the first state in the history. """ if self.tool_mode == "range": i = self._get_range_index() self._reset_state((self.component.view_position[i], self.component.view_bounds[i])) else: self._reset_state( (self.component.view_position, self.component.view_bounds))
class ErrorEnvelopeOverlay(AbstractOverlay): _cache_valid = False _screen_cache_valid = False upper = Array lower = Array use_downsampling = False line_color = black_color_trait region_color = ColorTrait((0.5, 0.5, 0.5, 0.5)) use_region = False xs = None def invalidate(self): self._cache_valid = False self._screen_cache_valid = False def _gather_points(self): if not self._cache_valid: index = self.component.index value = self.component.value if not index or not value: return xs = self.xs if xs is None: xs = index.get_data() ls = self.lower us = self.upper self._cached_data_pts_u = [array((xs, us)).T] self._cached_data_pts_l = [array((xs, ls)).T] self._cache_valid = True return def get_screen_points(self): self._gather_points() if self.use_downsampling: return self._downsample() else: return (self.component.map_screen(self._cached_data_pts_u), self.component.map_screen(self._cached_data_pts_l)) def overlay(self, other_component, gc, view_bounds=None, mode="normal"): with gc: gc.clip_to_rect(0, 0, other_component.width, other_component.height) upts, lpts = self.get_screen_points() if self.use_region: upts = upts[0] lpts = lpts[0] uxs, uys = upts.T lxs, lys = lpts.T x1 = x4 = uxs.min() x2 = x3 = uxs.max() y1 = uys[0] y2 = uys[-1] y3 = lys[-1] y4 = lys[0] gc.move_to(x1, y1) gc.line_to(x2, y2) gc.line_to(x3, y3) gc.line_to(x4, y4) # gc.set_fill_color((1, 0.4, 0.1, 0.5)) gc.set_fill_color(self.region_color_) gc.fill_path() else: gc.set_line_dash((5, 5)) gc.set_stroke_color(self.line_color_) self._render_line(gc, upts) self._render_line(gc, lpts) def _render_line(self, gc, points): for ary in points: if len(ary) > 0: gc.begin_path() gc.lines(ary) gc.stroke_path() def _downsample(self): if not self._screen_cache_valid: self._cached_screen_pts_u = self.component.map_screen( self._cached_data_pts_u)[0] self._cached_screen_pts_l = self.component.map_screen( self._cached_data_pts_l)[0] self._screen_cache_valid = True for pt_arrays in (self._cached_screen_pts_l, self._cached_screen_pts_u): r, c = pt_arrays.shape # some boneheaded short-circuits m = self.component.index_mapper total_numpoints = r * c if (total_numpoints < 400) or (total_numpoints < m.high_pos - m.low_pos): return [self._cached_screen_pts_l ], [self._cached_screen_pts_u] # the new point array and a counter of how many actual points we've added # to it new_arrays = [] for pts in pt_arrays: new_pts = zeros(pts.shape, "d") numpoints = 1 new_pts[0] = pts[0] last_x, last_y = pts[0] for x, y in pts[1:]: if (x - last_x)**2 + (y - last_y)**2 > 2: new_pts[numpoints] = (x, y) last_x = x last_y = y numpoints += 1 new_arrays.append(new_pts[:numpoints]) return [self._cached_screen_pts_l], [self._cached_screen_pts_u]
class Line(Component): """A line segment component""" # Event fired when the points are no longer updating. # PZW: there seems to be a missing defn here; investigate. # An event to indicate that the point list has changed updated = Event # The color of the line. line_color = ColorTrait("black") # The dash pattern for the line. line_dash = Any # The width of the line. line_width = border_size_trait(1) # The points that make up this polygon. points = List # List of Tuples # The color of each vertex. vertex_color = ColorTrait("black") # The size of each vertex. vertex_size = Float(3.0) # Whether to draw the path closed, with a line back to the first point close_path = Bool(True) # ------------------------------------------------------------------------- # 'Line' interface # ------------------------------------------------------------------------- def reset(self): "Reset the polygon to the initial state" self.points = [] self.event_state = "normal" self.updated = self # ------------------------------------------------------------------------- # 'Component' interface # ------------------------------------------------------------------------- def _draw_mainlayer(self, gc, view_bounds=None, mode="default"): "Draw this line in the specified graphics context" if len(self.points) > 1: with gc: # Set the drawing parameters. gc.set_stroke_color(self.line_color_) gc.set_line_dash(self.line_dash) gc.set_line_width(self.line_width) # Draw the path as lines. gc.begin_path() offset_points = [(x, y) for x, y in self.points] offset_points = resize(array(offset_points), (len(self.points), 2)) gc.lines(offset_points) if self.close_path: gc.close_path() gc.draw_path(STROKE) if len(self.points) > 0: with gc: # Draw the vertices. self._draw_points(gc) # ------------------------------------------------------------------------- # Private interface # ------------------------------------------------------------------------- def _draw_points(self, gc): "Draw the points of the line" # Shortcut out if we would draw transparently. if self.vertex_color_[3] != 0: with gc: gc.set_fill_color(self.vertex_color_) gc.set_line_dash(None) offset_points = [(x, y) for x, y in self.points] offset_points = resize(array(offset_points), (len(self.points), 2)) offset = self.vertex_size / 2.0 if hasattr(gc, "draw_path_at_points"): path = gc.get_empty_path() path.rect(-offset, -offset, self.vertex_size, self.vertex_size) gc.draw_path_at_points(offset_points, path, FILL_STROKE) else: for x, y in offset_points: gc.draw_rect( ( x - offset, y - offset, self.vertex_size, self.vertex_size, ), FILL, )
class Annotater(Component): color = ColorTrait((0.0, 0.0, 0.0, 0.2)) style = PrefixList(["rectangular", "freehand"], default_value="rectangular") annotation = Event traits_view = View( Group("<component>", id="component"), Group("<links>", id="links"), Group("color", "style", id="annotater", style="custom"), ) # ------------------------------------------------------------------------- # Mouse event handlers # ------------------------------------------------------------------------- def _left_down_changed(self, event): event.handled = True self.window.mouse_owner = self self._cur_x, self._cur_y = event.x, event.y self._start_x, self._start_y = event.x, event.y def _left_up_changed(self, event): event.handled = True self.window.mouse_owner = None if self.xy_in_bounds(event): self.annotation = ( min(self._start_x, event.x), min(self._start_y, event.y), abs(self._start_x - event.x), abs(self._start_y - event.y), ) self._start_x = self._start_y = self._cur_x = self._cur_y = None self.redraw() def _mouse_move_changed(self, event): event.handled = True if self._start_x is not None: x = max(min(event.x, self.right - 1.0), self.x) y = max(min(event.y, self.top - 1.0), self.y) if (x != self._cur_x) or (y != self._cur_y): self._cur_x, self._cur_y = x, y self.redraw() # ------------------------------------------------------------------------- # "Component" interface # ------------------------------------------------------------------------- def _draw(self, gc): "Draw the contents of the control" if self._start_x is not None: with gc: gc.set_fill_color(self.color_) gc.begin_path() gc.rect( min(self._start_x, self._cur_x), min(self._start_y, self._cur_y), abs(self._start_x - self._cur_x), abs(self._start_y - self._cur_y), ) gc.fill_path() return
class EditField(Component): """ A simplified single-line editable text entry field for Enable. """ #--------------------------------------------------------------------- # Traits #--------------------------------------------------------------------- # The text on display - with appropriate line breaks text = Property(depends_on=['_text_changed']) # The list of characters in this text field _text = List() # Even that fires when the text changes _text_changed = Event # The current index into the list of characters # This corresponds to the user's position index = Int(0) # Should we draw the cursor in the box? This only occurs # when someone is in edit mode. _draw_cursor = Bool(False) # The font we're using - must be monospaced font = KivaFont("Courier 12") # Toggle normal/highlighted color of the text text_color = RGBAColor((0, 0, 0, 1.0)) highlight_color = RGBAColor((.65, 0, 0, 1.0)) # Toggle normal/highlighted background color bgcolor = RGBAColor((1.0, 1.0, 1.0, 1.0)) highlight_bgcolor = ColorTrait("lightgray") # The text offset offset = Int(3) # The object to use to measure text extents metrics = Any # Events that get fired on certain key pressed events accept = Event cancel = Event # Can we edit this text field? can_edit = Bool(True) #--------------------------------------------------------------------- # Public methods #--------------------------------------------------------------------- def __init__(self, *args, **kwargs): super(EditField, self).__init__(*args, **kwargs) if self.metrics is None: self.metrics = font_metrics_provider() # If no bounds have been set, make sure it is wide enough to # display the text if self.height == 0 and self.width == 0 and len(self._text) > 0: self.update_bounds() def update_bounds(self): w, h = self.metrics.get_text_extent(self.text)[2:4] self.width = w + 2 * self.offset self.height = h + 2 * self.offset #--------------------------------------------------------------------- # Interactor interface #--------------------------------------------------------------------- def normal_left_dclick(self, event): # If we can't edit, just return. if not self.can_edit: return event.window.set_pointer('ibeam') self.event_state = 'edit' self._acquire_focus(event.window) event.handled = True self.request_redraw() def edit_left_up(self, event): self.event_state = "normal" event.handled = True self.request_redraw() def normal_character(self, event): # handle normal text entry char = event.character old_len = len(self._text) self._text.insert(self.index, char) self.index += 1 self._text_changed = True if old_len != len(self._text): self.update_bounds() event.handled = True self.invalidate_draw() self.request_redraw() def normal_key_pressed(self, event): char = event.character old_len = len(self._text) #Normal characters if len(char) == 1: # leave unhandled, and let character event be generated return #Deletion elif char == "Backspace": if self.index > 0: del self._text[self.index - 1] self.index -= 1 self._text_changed = True elif char == "Delete": if self.index < len(self._text): del self._text[self.index] self._text_changed = True #Cursor Movement elif char == "Left": if self.index > 0: self.index -= 1 elif event.character == "Right": if self.index < len(self._text): self.index += 1 elif event.character == "Home": self.index = 0 elif event.character == "End": self.index = len(self._text) elif event.character == "Enter": self.accept = event elif char == "Escape": self.cancel = event if old_len != len(self._text): self.update_bounds() event.handled = True self.invalidate_draw() self.request_redraw() #--------------------------------------------------------------------- # Component interface #--------------------------------------------------------------------- def _draw_mainlayer(self, gc, view_bounds, mode="default"): gc.save_state() self.set_font(gc) x = self.x + self.offset y = self.y gc.show_text_at_point(self.text, x, y) if self._draw_cursor: x += self.metrics.get_text_extent(self.text[:self.index])[2] y2 = self.y2 - self.offset gc.set_line_width(2) gc.set_stroke_color((0, 0, 0, 1.0)) gc.begin_path() gc.move_to(x, y) gc.line_to(x, y2) gc.stroke_path() gc.restore_state() def set_font(self, gc): gc.set_font(self.font) gc.set_fill_color(self.text_color) #--------------------------------------------------------------------- # TextField interface #--------------------------------------------------------------------- def _acquire_focus(self, window): self._draw_cursor = True self.border_visible = True window.focus_owner = self window.on_trait_change(self._focus_owner_changed, "focus_owner") self.request_redraw() def _focus_owner_changed(self, obj, name, old, new): if old == self and new != self: obj.on_trait_change(self._focus_owner_changed, "focus_owner", remove=True) self._draw_cursor = False self.border_visible = False self.request_redraw() #--------------------------------------------------------------------- # Property get/set and Trait event handlers #--------------------------------------------------------------------- def _get_text(self): """ Return the text to be displayed in the text field. """ return "".join(self._text) def _set_text(self, val): """ Set the _text given a string. """ self.index = 0 if val == "": self._text = [] else: self._text = list(val) def _accept_changed(self, old, new): new.window.focus_owner = None def _cancel_changed(self, old, new): new.window.focus_owner = None
class LabelTraits(HasTraits): text = Str font = font_trait text_position = position_trait("left") color = ColorTrait("black") shadow_color = ColorTrait("white") style = engraving_trait #image = image_trait image_position = position_trait("left") image_orientation = orientation_trait spacing_height = spacing_trait spacing_width = spacing_trait padding_left = padding_trait padding_right = padding_trait padding_top = padding_trait padding_bottom = padding_trait margin_left = margin_trait margin_right = margin_trait margin_top = margin_trait margin_bottom = margin_trait border_size = border_size_trait border_color = ColorTrait("black") bg_color = ColorTrait("clear") enabled = Bool(True) selected = Bool(False) #--------------------------------------------------------------------------- # Trait view definitions: #--------------------------------------------------------------------------- traits_view = View( Group('enabled', 'selected', id='component'), Group('text', ' ', 'font', ' ', 'color', ' ', 'shadow_color', ' ', 'style', id='text', style='custom'), Group('bg_color{Background Color}', '_', 'border_color', '_', 'border_size', id='border', style='custom'), Group( 'text_position', '_', 'image_position', '_', 'image_orientation', ' ', #'image', id='position', style='custom'), Group('spacing_height', 'spacing_width', '_', 'padding_left', 'padding_right', 'padding_top', 'padding_bottom', '_', 'margin_left', 'margin_right', 'margin_top', 'margin_bottom', id='margin'))