class Box(Component): """ The box moves wherever the user clicks and drags. """ normal_pointer = Pointer("arrow") moving_pointer = Pointer("hand") offset_x = Float offset_y = Float fill_color = (0.8, 0.0, 0.1, 1.0) moving_color = (0.0, 0.8, 0.1, 1.0) resizable = "" def _draw_mainlayer(self, gc, view_bounds=None, mode="default"): with gc: if self.event_state == "moving": gc.set_fill_color(self.moving_color) else: gc.set_fill_color(self.fill_color) dx, dy = self.bounds x, y = self.position gc.rect(x, y, dx, dy) gc.fill_path() # draw line around outer box gc.set_stroke_color((0, 0, 0, 1)) gc.rect(self.outer_x, self.outer_y, self.outer_width, self.outer_height) gc.stroke_path() def normal_key_pressed(self, event): print("Key:", event.character) def normal_left_down(self, event): self.event_state = "moving" event.window.set_pointer(self.moving_pointer) event.window.set_mouse_owner(self, event.net_transform()) self.offset_x = event.x - self.x self.offset_y = event.y - self.y event.handled = True def moving_mouse_move(self, event): self.position = [event.x - self.offset_x, event.y - self.offset_y] event.handled = True self.request_redraw() def moving_left_up(self, event): self.event_state = "normal" event.window.set_pointer(self.normal_pointer) event.window.set_mouse_owner(None) event.handled = True self.request_redraw() def moving_mouse_leave(self, event): self.moving_left_up(event) event.handled = True
class LineSegmentTool(AbstractOverlay): """ The base class for tools that allow the user to draw a series of points connected by lines. """ # The component that this tool overlays component = Instance(Component) # The current line segment being drawn. line = Instance(Line, args=()) # A list of the points in data space as (index,value) points = List # The event states are: # # normal: # The user may have selected points, and is moving the cursor around. # selecting: # The user has clicked down but hasn't let go of the button yet, # and can still drag the point around. # dragging: # The user has clicked on an existing point and is dragging it # around. When the user releases the mouse button, the tool returns # to the "normal" state event_state = Enum("normal", "selecting", "dragging") # The pixel distance from a vertex that is considered 'on' the vertex. proximity_distance = Int(4) # The data (index, value) position of the mouse cursor; this is used by various # draw() routines. mouse_position = Trait(None, None, Tuple) # The index of the vertex being dragged, if any. _dragged = Trait(None, None, Int) # Is the point being dragged is a newly placed point? This informs the # "dragging" state about what to do if the user presses Escape while # dragging. _drag_new_point = Bool(False) # The previous event state that the tool was in. This is used for states # that can be canceled (e.g., by pressing the Escape key), so that the # tool can revert to the correct state. _prev_event_state = Any # The cursor shapes to use for various modes # Cursor shape for non-tool use. original_cursor = Pointer("arrow") # Cursor shape for drawing. normal_cursor = Pointer("pencil") # Cursor shape for deleting points. delete_cursor = Pointer("bullseye") # Cursor shape for moving points. move_cursor = Pointer("sizing") #------------------------------------------------------------------------ # Traits inherited from Component #------------------------------------------------------------------------ # The tool is initially invisible, because there is nothing to draw. visible = Bool(False) #------------------------------------------------------------------------ # Public methods #------------------------------------------------------------------------ def __init__(self, component=None, **kwtraits): if "component" in kwtraits: component = kwtraits["component"] super(LineSegmentTool, self).__init__(**kwtraits) self.component = component self.reset() self.line.line_dash = (4.0, 2.0) return #------------------------------------------------------------------------ # Drawing tool methods #------------------------------------------------------------------------ def reset(self): """ Resets the tool, throwing away any points, and making the tool invisible. """ self.points = [] self.event_state = "normal" self.visible = False self.request_redraw() return def _activate(self): """ Called by a PlotComponent when this becomes the active tool. """ pass def _deactivate(self, component=None): """ Called by a PlotComponent when this is no longer the active tool. """ self.reset() #self.component.window.set_pointer("arrow") return #------------------------------------------------------------------------ # PointLine methods #------------------------------------------------------------------------ def add_point(self, point): """ Given a screen-space *point* (x,y), adds the corresponding data space point to the list for this tool. """ self.points.append(self._map_data(point)) return def get_point(self, index): """ Retrieves the indexed point and returns its screen space value. """ return self._map_screen(self.points[index]) def set_point(self, index, point): """ Sets the data-space *index* for a screen-space *point*. """ self.points[index] = self._map_data(point) return def remove_point(self, index): """ Removes the point for a given *index* from this tool's list of points. """ del self.points[index] return #------------------------------------------------------------------------ # "normal" state #------------------------------------------------------------------------ def normal_left_down(self, event): """ Handles the left mouse button being pressed while the tool is in the 'normal' state. For an existing point, if the user is pressing the Control key, the point is deleted. Otherwise, the user can drag the point. For a new point, the point is added, and the user can drag it. """ # Determine if the user is dragging/deleting an existing point, or # creating a new one over = self._over_point(event, self.line.points) if over is not None: if event.control_down: # Delete the point self.points.pop(over) self.line.points = list( self.component.map_screen(array(self.points))) self.request_redraw() else: self.event_state = "dragging" self._dragged = over self._drag_new_point = False self.dragging_mouse_move(event) else: self.points.append(self._map_data((event.x, event.y))) self._dragged = -1 self._drag_new_point = True self.visible = True self.event_state = "dragging" self.dragging_mouse_move(event) return def normal_mouse_move(self, event): """ Handles the user moving the mouse in the 'normal' state. When the user moves the cursor over an existing point, if the Control key is pressed, the cursor changes to the **delete_cursor**, indicating that the point can be deleted. Otherwise, the cursor changes to the **move_cursor**, indicating that the point can be moved. When the user moves the cursor over any other point, the cursor changes to (or stays) the **normal_cursor**. """ # If the user moves over an existing point, change the cursor to be the # move_cursor; otherwise, set it to the normal cursor over = self._over_point(event, self.line.points) if over is not None: if event.control_down: event.window.set_pointer(self.delete_cursor) else: event.window.set_pointer(self.move_cursor) else: event.handled = False event.window.set_pointer(self.normal_cursor) self.request_redraw() return def normal_draw(self, gc): """ Draws the line. """ self.line.points = list(self.component.map_screen(array(self.points))) self.line._draw(gc) return def normal_key_pressed(self, event): """ Handles the user pressing a key in the 'normal' state. If the user presses the Enter key, the tool is reset. """ if event.character == "Enter": self._finalize_selection() self.reset() return def normal_mouse_leave(self, event): """ Handles the user moving the cursor away from the tool area. """ event.window.set_pointer("arrow") return #------------------------------------------------------------------------ # "dragging" state #------------------------------------------------------------------------ def dragging_mouse_move(self, event): """ Handles the user moving the mouse while in the 'dragging' state. The screen is updated to show the new mouse position as the end of the line segment being drawn. """ mouse_position = self._map_data((event.x, event.y)) self.points[self._dragged] = mouse_position self.line.points = list(self.component.map_screen(array(self.points))) self.request_redraw() return def dragging_draw(self, gc): """ Draws the polygon in the 'dragging' state. """ self.line._draw(gc) return def dragging_left_up(self, event): """ Handles the left mouse coming up in the 'dragging' state. Switches to 'normal' state. """ self.event_state = "normal" self._dragged = None self.updated = self return def dragging_key_pressed(self, event): """ Handles a key being pressed in the 'dragging' state. If the key is "Esc", the drag operation is canceled. """ if event.character == "Esc": self._cancel_drag() return def dragging_mouse_leave(self, event): """ Handles the mouse leaving the tool area in the 'dragging' state. The drag is canceled and the cursor changes to an arrow. """ self._cancel_drag() event.window.set_pointer("arrow") return def _cancel_drag(self): """ Cancels a drag operation. """ if self._dragged != None: if self._drag_new_point: # Only remove the point if it was a newly-placed point self.points.pop(self._dragged) self._dragged = None self.mouse_position = None self.event_state = "normal" self.request_redraw() return #------------------------------------------------------------------------ # override AbstractOverlay methods #------------------------------------------------------------------------ def overlay(self, component, gc, view_bounds, mode="normal"): """ Draws this component overlaid on another component. Implements AbstractOverlay. """ draw_func = getattr(self, self.event_state + "_draw", None) if draw_func: with gc: gc.clip_to_rect(component.x, component.y, component.width - 1, component.height - 1) draw_func(gc) return def request_redraw(self): """ Requests that the component redraw itself. Overrides Enable Component. """ self.component.invalidate_draw() self.component.request_redraw() return #------------------------------------------------------------------------ # Private methods #------------------------------------------------------------------------ def _map_data(self, point): """ Maps values from screen space into data space. """ x_mapper = self.component.x_mapper y_mapper = self.component.y_mapper if self.component.orientation == 'h': ndx = x_mapper.map_data(point[0]) val = y_mapper.map_data(point[1]) else: val = x_mapper.map_data(point[0]) ndx = y_mapper.map_data(point[1]) return (ndx, val) def _map_screen(self, point): """ Maps values from data space into screen space. """ x_mapper = self.component.x_mapper y_mapper = self.component.y_mapper if self.component.orientation == 'h': x = x_mapper.map_screen(point[0]) y = y_mapper.map_screen(point[1]) else: x = x_mapper.map_screen(point[1]) y = y_mapper.map_screen(point[0]) return (x, y) def _is_near_point(self, point, event): """ Determines if the pointer is near a specified point. """ event_point = (event.x, event.y) return ((abs( point[0] - event_point[0] ) + \ abs( point[1] - event_point[1] )) <= self.proximity_distance) def _over_point(self, event, points): """ Return the index of a point in *points* that *event* is 'over'. Returns None if there is no such point. """ for i, point in enumerate(points): if self._is_near_point(point, event): result = i break else: result = None return result def _finalize_selection(self): """ Abstract method called to take action after the line selection is complete """ pass #------------------------------------------------------------------------ # Trait event handlers #------------------------------------------------------------------------ def _component_changed(self, old, new): if new: self.container = new return
class PanTool(DragTool): """ An implementation of a pan tool based on the DragTool instead of a bare BaseTool """ # The cursor to use when panning. drag_pointer = Pointer("hand") # Scaling factor on the panning "speed". speed = Float(1.0) # The modifier key that, if depressed when the drag is initiated, constrains # the panning to happen in the only direction of largest initial motion. # It is possible to permanently restrict this tool to always drag along one # direction. To do so, set constrain=True, constrain_key=None, and # constrain_direction to the desired direction. constrain_key = Enum(None, "shift", "control", "alt") # Constrain the panning to one direction? constrain = Bool(False) # The direction of constrained draw. A value of None means that the user # has initiated the drag and pressed the constrain_key, but hasn't moved # the mouse yet; the magnitude of the components of the next mouse_move # event will determine the constrain_direction. constrain_direction = Enum(None, "x", "y") # Restrict to the bounds of the plot data restrict_to_data = Bool(False) # (x,y) of the point where the mouse button was pressed. _original_xy = Tuple # Data coordinates of **_original_xy**. This may be either (index,value) # or (value,index) depending on the component's orientation. _original_data = Tuple # Was constrain=True triggered by the **contrain_key**? If False, it was # set programmatically. _auto_constrain = Bool(False) #------------------------------------------------------------------------ # Inherited BaseTool traits #------------------------------------------------------------------------ # The tool does not have a visual representation (overrides # BaseTool). draw_mode = "none" # The tool is not visible (overrides BaseTool). visible = False def drag_start(self, event): """ Called when the drag operation starts """ self._start_pan(event) def dragging(self, event): plot = self.component if self._auto_constrain and self.constrain_direction is None: # Determine the constraint direction if abs(event.x - self._original_xy[0]) > abs(event.y - self._original_xy[1]): self.constrain_direction = "x" else: self.constrain_direction = "y" for direction, bound_name, ndx in [("x","width",0), ("y","height",1)]: if not self.constrain or self.constrain_direction == direction: mapper = getattr(plot, direction + "_mapper") range = mapper.range domain_min, domain_max = mapper.domain_limits eventpos = getattr(event, direction) origpos = self._original_xy[ndx] screenlow, screenhigh = mapper.screen_bounds screendelta = self.speed * (eventpos - origpos) #if getattr(plot, direction + "_direction", None) == "flipped": # screendelta = -screendelta newlow = mapper.map_data(screenlow - screendelta) newhigh = mapper.map_data(screenhigh - screendelta) # Don't set the range in this dimension if the panning # would exceed the domain limits. # To do this offset properly, we would need to iteratively # solve for a root using map_data on successive trial # values. As a first approximation, we're just going to # use a linear approximation, which works perfectly for # linear mappers (which is used 99% of the time). if domain_min is None: if self.restrict_to_data: domain_min = min([source.get_data().min() for source in range.sources]) else: domain_min = -inf if domain_max is None: if self.restrict_to_data: domain_max = max([source.get_data().max() for source in range.sources]) else: domain_max = inf if (newlow <= domain_min) and (newhigh >= domain_max): # Don't do anything; effectively, freeze the pan continue if newlow <= domain_min: delta = newhigh - newlow newlow = domain_min # Don't let the adjusted newhigh exceed domain_max; this # can happen with a nonlinear mapper. newhigh = min(domain_max, domain_min + delta) elif newhigh >= domain_max: delta = newhigh - newlow newhigh = domain_max # Don't let the adjusted newlow go below domain_min; this # can happen with a nonlinear mapper. newlow = max(domain_min, domain_max - delta) # Use .set_bounds() so that we don't generate two range_changed # events on the DataRange range.set_bounds(newlow, newhigh) event.handled = True self._original_xy = (event.x, event.y) plot.request_redraw() return def drag_cancel(self, event): # We don't do anything for "cancelling" of the drag event because its # transient states during the drag are generally valid and useful # terminal states unto themselves. pass def drag_end(self, event): return self._end_pan(event) def _start_pan(self, event, capture_mouse=True): self._original_xy = (event.x, event.y) if self.constrain_key is not None: if getattr(event, self.constrain_key + "_down"): self.constrain = True self._auto_constrain = True self.constrain_direction = None self.event_state = "panning" if capture_mouse: event.window.set_pointer(self.drag_pointer) event.window.set_mouse_owner(self, event.net_transform()) event.handled = True return def _end_pan(self, event): if self._auto_constrain: self.constrain = False self.constrain_direction = None self.event_state = "normal" event.window.set_pointer("arrow") if event.window.mouse_owner == self: event.window.set_mouse_owner(None) event.handled = True return
class DragPolygon(DrawingTool): """ A drag drawn polygon. """ poly = Instance(Polygon, args=()) draw_mode = "overlay" # Visible style. #### # Override the vertex color so as to not draw it. vertex_color = Delegate("poly", modify=True) # Override the vertex size so as to not draw it. vertex_size = Delegate("poly", modify=True) background_color = Delegate("poly", modify=True) # Pointers. #### # Pointer for the complete state. complete_pointer = Pointer("cross") # Pointer for the drawing state. drawing_pointer = Pointer("cross") # Pointer for the normal state. normal_pointer = Pointer("cross") # Miscellaneous. #### # The context menu for the polygon. menu = Instance(MenuManager) def reset(self): self.vertex_color = (0, 0, 0, 0) self.vertex_size = 0 self.poly.model.points = [] self.event_state = "normal" ########################################################################### # 'Component' interface. ########################################################################### # 'complete' state ##################################################### def complete_draw(self, gc): """ Draw the completed polygon. """ with gc: self.poly.border_dash = None self.poly._draw_closed(gc) def complete_left_down(self, event): """ Draw a new polygon. """ self.reset() self.normal_left_down(event) def complete_right_down(self, event): """ Do the context menu if available. """ if self.menu is not None: if self._is_in((event.x + self.x, event.y - self.y)): menu = self.menu.create_menu(event.window.control) # FIXME : The call to _flip_y is necessary but inappropriate. menu.show(event.x, event.window._flip_y(event.y)) # 'drawing' state ###################################################### def drawing_draw(self, gc): """ Draw the polygon while in 'drawing' state. """ with gc: self.poly.border_dash = (4.0, 2.0) self.poly._draw_open(gc) def drawing_left_up(self, event): """ Handle the left mouse button coming up in 'drawing' state. """ self.event_state = "complete" self.pointer = self.complete_pointer self.request_redraw() self.complete = True def drawing_mouse_move(self, event): """ Handle the mouse moving in 'drawing' state. """ last_point = self.poly.model.points[-1] # If we have moved, we need to add a point. if last_point != (event.x + self.x, event.y - self.y): self.poly.model.points.append((event.x + self.x, event.y - self.y)) self.request_redraw() # 'normal' state ####################################################### def normal_left_down(self, event): """ Handle the left button down in the 'normal' state. """ self.poly.model.points.append((event.x + self.x, event.y - self.y)) self.event_state = "drawing" self.pointer = self.drawing_pointer self.request_redraw() def normal_mouse_move(self, event): """ Handle the mouse moving in the 'normal' state. """ self.pointer = self.normal_pointer
class DragSegment(DrawingTool): """A dragged line segment""" # Override the vertex color so as to not draw it. vertex_color = (0.0, 0.0, 0.0, 0.0) # Because this class subclasses DrawingTool and not Line, it contains # an instance of the Line primitive. line = Instance(Line, args=()) # Event fired when the line is complete complete = Event # Pointer for the complete state. complete_pointer = Pointer('arrow') # Pointer for the drawing state. drawing_pointer = Pointer('cross') # Pointer for the normal state. normal_pointer = Pointer('cross') #------------------------------------------------------------------------ # DrawingTool interface #------------------------------------------------------------------------ def reset(self): self.line.vertex_color = self.vertex_color self.line.points = [] self.event_state = "normal" return #------------------------------------------------------------------------ # "complete" state #------------------------------------------------------------------------ def complete_draw(self, gc): """ Draw the completed line. """ self.line.line_dash = None self.line._draw_mainlayer(gc) self.request_redraw() return #------------------------------------------------------------------------ # "drawing" state #------------------------------------------------------------------------ def drawing_draw(self, gc): self.line.line_dash = (4.0, 2.0) self.line._draw_mainlayer(gc) return def drawing_mouse_move(self, event): """ Handle the mouse moving in drawing state. """ # Change the last point to the current event point self.line.points[-1] = (event.x, event.y) self.updated = self self.request_redraw() return def drawing_left_up(self, event): """ Handle the left mouse button coming up in the 'drawing' state. """ self.event_state = 'complete' event.window.set_pointer(self.complete_pointer) self.request_redraw() self.complete = True return #------------------------------------------------------------------------ # "normal" state #------------------------------------------------------------------------ def normal_left_down(self, event): """ Handle the left button down in the 'normal' state. """ # Set points the current segment, which is just the # current point twice. current_point = (event.x, event.y) self.line.points = [current_point, current_point] self.updated = self # Go into the drawing state self.event_state = 'drawing' event.window.set_pointer(self.drawing_pointer) self.request_redraw() return def normal_mouse_move(self, event): event.window.set_pointer(self.normal_pointer) return
class PanTool(BaseTool): """ A tool that enables the user to pan a plot by clicking a mouse button and dragging. """ # The mouse button that initiates the drag operation. drag_button = Enum("left", "middle", "right") # The cursor to use when panning. drag_pointer = Pointer("hand") # Scaling factor on the panning "speed". speed = Float(1.0) # The modifier key that, if depressed when the drag is initiated, constrains # the panning to happen in the only direction of largest initial motion. # It is possible to permanently restrict this tool to always drag along one # direction. To do so, set constrain=True, constrain_key=None, and # constrain_direction to the desired direction. constrain_key = Enum(None, "shift", "control", "alt") # Keys to Pan via keyboard pan_right_key = Instance(KeySpec, args=("Right", )) pan_left_key = Instance(KeySpec, args=("Left", )) pan_up_key = Instance(KeySpec, args=("Up", )) pan_down_key = Instance(KeySpec, args=("Down", )) # number of pixels the keys should pan # disabled if 0.0 pan_keys_step = Float(0.0) # Constrain the panning to one direction? constrain = Bool(False) # The direction of constrained draw. A value of None means that the user # has initiated the drag and pressed the constrain_key, but hasn't moved # the mouse yet; the magnitude of the components of the next mouse_move # event will determine the constrain_direction. constrain_direction = Enum(None, "x", "y") # Restrict to the bounds of the plot data restrict_to_data = Bool(False) # (x,y) of the point where the mouse button was pressed. _original_xy = Tuple # Data coordinates of **_original_xy**. This may be either (index,value) # or (value,index) depending on the component's orientation. _original_data = Tuple # Was constrain=True triggered by the **contrain_key**? If False, it was # set programmatically. _auto_constrain = Bool(False) #------------------------------------------------------------------------ # Inherited BaseTool traits #------------------------------------------------------------------------ # The tool does not have a visual representation (overrides # BaseTool). draw_mode = "none" # The tool is not visible (overrides BaseTool). visible = False # The possible event states of this tool (overrides enable.Interactor). event_state = Enum("normal", "panning") def normal_key_pressed(self, event): """ Handles a key being pressed when the tool is in the 'normal' state. """ if self.pan_keys_step == 0.0: return src = self.component.bounds[0] / 2, self.component.bounds[1] / 2 dest = src if self.pan_left_key.match(event): dest = (src[0] - self.pan_keys_step, src[1]) elif self.pan_right_key.match(event): dest = (src[0] + self.pan_keys_step, src[1]) elif self.pan_down_key.match(event): dest = (src[0], src[1] - self.pan_keys_step) elif self.pan_up_key.match(event): dest = (src[0], src[1] + self.pan_keys_step) if src != dest: self._original_xy = src event.x = dest[0] event.y = dest[1] self.panning_mouse_move(event) return def normal_left_down(self, event): """ Handles the left mouse button being pressed when the tool is in the 'normal' state. Starts panning if the left mouse button is the drag button. """ if self.drag_button == "left": self._start_pan(event) return def normal_right_down(self, event): """ Handles the right mouse button being pressed when the tool is in the 'normal' state. Starts panning if the right mouse button is the drag button. """ if self.drag_button == "right": self._start_pan(event) return def normal_middle_down(self, event): """ Handles the middle mouse button being pressed when the tool is in the 'normal' state. Starts panning if the middle mouse button is the drag button. """ if self.drag_button == "middle": self._start_pan(event) return def panning_left_up(self, event): """ Handles the left mouse button coming up when the tool is in the 'panning' state. Stops panning if the left mouse button is the drag button. """ if self.drag_button == "left": self._end_pan(event) return def panning_right_up(self, event): """ Handles the right mouse button coming up when the tool is in the 'panning' state. Stops panning if the right mouse button is the drag button. """ if self.drag_button == "right": self._end_pan(event) return def panning_middle_up(self, event): """ Handles the middle mouse button coming up when the tool is in the 'panning' state. Stops panning if the middle mouse button is the drag button. """ if self.drag_button == "middle": self._end_pan(event) return def panning_mouse_move(self, event): """ Handles the mouse being moved when the tool is in the 'panning' state. """ plot = self.component if self._auto_constrain and self.constrain_direction is None: # Determine the constraint direction x_orig, y_orig = self._original_xy if abs(event.x - x_orig) > abs(event.y - y_orig): self.constrain_direction = "x" else: self.constrain_direction = "y" direction_info = [("x", "width", 0), ("y", "height", 1)] for direction, bound_name, index in direction_info: if not self.constrain or self.constrain_direction == direction: mapper = getattr(plot, direction + "_mapper") domain_min, domain_max = mapper.domain_limits eventpos = getattr(event, direction) origpos = self._original_xy[index] screenlow, screenhigh = mapper.screen_bounds screendelta = self.speed * (eventpos - origpos) newlow = mapper.map_data(screenlow - screendelta) newhigh = mapper.map_data(screenhigh - screendelta) # Don't set the range in this dimension if the panning # would exceed the domain limits. # To do this offset properly, we would need to iteratively # solve for a root using map_data on successive trial # values. As a first approximation, we're just going to # use a linear approximation, which works perfectly for # linear mappers (which is used 99% of the time). if domain_min is None: if self.restrict_to_data: domain_min = min([ source.get_data().min() for source in mapper.range.sources ]) else: domain_min = -inf if domain_max is None: if self.restrict_to_data: domain_max = max([ source.get_data().max() for source in mapper.range.sources ]) else: domain_max = inf if (newlow <= domain_min) and (newhigh >= domain_max): # Don't do anything; effectively, freeze the pan continue if newlow <= domain_min: newlow = domain_min # Calculate delta in screen space, which is always linear. screen_delta = mapper.map_screen(domain_min) - screenlow newhigh = mapper.map_data(screenhigh + screen_delta) elif newhigh >= domain_max: newhigh = domain_max # Calculate delta in screen space, which is always linear. screen_delta = mapper.map_screen(domain_max) - screenhigh newlow = mapper.map_data(screenlow + screen_delta) # Use .set_bounds() so that we don't generate two range_changed # events on the DataRange mapper.range.set_bounds(newlow, newhigh) event.handled = True self._original_xy = (event.x, event.y) plot.request_redraw() return def panning_mouse_leave(self, event): """ Handles the mouse leaving the plot when the tool is in the 'panning' state. Ends panning. """ return self._end_pan(event) def _start_pan(self, event, capture_mouse=True): self._original_xy = (event.x, event.y) if self.constrain_key is not None: if getattr(event, self.constrain_key + "_down"): self.constrain = True self._auto_constrain = True self.constrain_direction = None self.event_state = "panning" if capture_mouse: event.window.set_pointer(self.drag_pointer) event.window.set_mouse_owner(self, event.net_transform()) event.handled = True return def _end_pan(self, event): if self._auto_constrain: self.constrain = False self.constrain_direction = None self.event_state = "normal" event.window.set_pointer("arrow") if event.window.mouse_owner == self: event.window.set_mouse_owner(None) event.handled = True return
class BaseCanvas(Component): ''' ''' # directory = None select_pointer = Pointer('hand') normal_pointer = Pointer('arrow')
class Circle(Component): """ The circle moves with the mouse cursor but leaves a translucent version of itself in its original position until the mouse button is released. """ color = (0.6, 0.7, 1.0, 1.0) bgcolor = "none" normal_pointer = Pointer("arrow") moving_pointer = Pointer("hand") prev_x = Float prev_y = Float shadow_type = Enum("light", "dashed") shadow = Instance(Component) resizable = "" def __init__(self, **traits): Component.__init__(self, **traits) self.pointer = self.normal_pointer return def _draw_mainlayer(self, gc, view_bounds=None, mode="default"): with gc: gc.set_fill_color(self.color) dx, dy = self.bounds x, y = self.position radius = min(dx / 2.0, dy / 2.0) gc.arc(x + dx / 2.0, y + dy / 2.0, radius, 0.0, 2 * 3.14159) gc.fill_path() return def normal_left_down(self, event): self.event_state = "moving" self.pointer = self.moving_pointer event.window.set_mouse_owner(self, event.net_transform()) # Create our shadow if self.shadow_type == "light": klass = LightCircle else: klass = DashedCircle self.shadow = klass(bounds=self.bounds, position=self.position, color=(1.0, 1.0, 1.0, 1.0)) self.container.insert(0, self.shadow) x, y = self.position self.prev_x = event.x self.prev_y = event.y return def moving_mouse_move(self, event): self.position = [ self.x + (event.x - self.prev_x), self.y + (event.y - self.prev_y) ] self.prev_x = event.x self.prev_y = event.y self.request_redraw() return def moving_left_up(self, event): self.event_state = "normal" self.pointer = self.normal_pointer event.window.set_mouse_owner(None) event.window.redraw() # Remove our shadow self.container.remove(self.shadow) return def moving_mouse_leave(self, event): self.moving_left_up(event) return
class Click2DPanelTool(PanTool): event_state = Enum('normal', 'deciding', 'panning') drag_pointer = Pointer('arrow') panel2d = Any #Instance(TwoDimensionalPanel) panel_id = Enum('xy','xz','yz') def __init__(self, panel2d, panel_id): super(PanTool, self).__init__( component=getattr(panel2d, '{0}_plane'.format(panel_id))) self.panel2d = panel2d self.panel_id = panel_id def normal_left_down(self, event): self.event_state = 'deciding' self._original_xy = event.x, event.y def deciding_left_up(self, event): self.event_state = 'normal' x,y,z = self.panel2d.cursor #if the panel is not in the image (e.g. click on the axis), ignore it #remember event.x and event.y are in space of pixels if self.panel_id == 'xy': mx, my = self.panel2d.xy_plane.map_data((event.x, event.y)) if x == mx and y == my: return self.panel2d.move_cursor(mx, my, z) elif self.panel_id == 'xz': mx, mz = self.panel2d.xz_plane.map_data((event.x, event.y)) if x == mx and z == mz: return self.panel2d.move_cursor(mx, y, mz) elif self.panel_id == 'yz': my, mz = self.panel2d.yz_plane.map_data((event.x, event.y)) if y == my and z == mz: return self.panel2d.move_cursor(x, my, mz) else: raise NotImplementedError('FailFish') def normal_right_down(self, event): pin_name = self.panel2d.current_pin if pin_name is None: print 'Nothing currently pinned' return x,y,z = self.panel2d.cursor image_name = self.panel2d.currently_showing.name px,py,pz,pcolor = self.panel2d.pins[image_name][pin_name] if self.panel_id == 'xy': mx, my = self.panel2d.xy_plane.map_data((event.x, event.y)) if px == mx and py == my: return self.panel2d.drop_pin(mx, my, z, name=self.panel2d.current_pin, color=pcolor) elif self.panel_id == 'xz': mx, mz = self.panel2d.xz_plane.map_data((event.x, event.y)) if px == mx and pz == mz: return self.panel2d.drop_pin(mx, y, mz, name=self.panel2d.current_pin, color=pcolor) elif self.panel_id == 'yz': my, mz = self.panel2d.yz_plane.map_data((event.x, event.y)) if py == my and pz == mz: return self.panel2d.drop_pin(x, my, mz, name=self.panel2d.current_pin, color=pcolor) else: raise NotImplementedError('BabyRage') def normal_mouse_move(self, event): self._move_mouse(event) def deciding_mouse_move(self, event): self._move_mouse(event) self.event_state = 'panning' return self.panning_mouse_move(event) def _move_mouse(self, event): x,y,z = self.panel2d.cursor if self.panel_id == 'xy': mx, my = self.panel2d.xy_plane.map_data((event.x, event.y)) self.panel2d.move_mouse(mx, my, z) elif self.panel_id == 'xz': mx, mz = self.panel2d.xz_plane.map_data((event.x, event.y)) self.panel2d.move_mouse(mx, y, mz) elif self.panel_id == 'yz': my, mz = self.panel2d.yz_plane.map_data((event.x, event.y)) self.panel2d.move_mouse(x, my, mz) else: raise NotImplementedError('DansGame')
class BaseDataCanvas(DataView): """ """ # fill_padding = True # bgcolor = (0.9, 0.9, 1.0) # bgcolor = (0, 1.0, 0) # border_visible = True # use_backbuffer = True # bgcolor = 'lightblue' unified_draw = True x_range = Tuple y_range = Tuple view_x_range = Tuple view_y_range = Tuple select_pointer = Pointer('hand') normal_pointer = Pointer('arrow') cross_pointer = Pointer('cross') show_axes = Bool(True) show_grids = Bool(True) use_zoom = Bool(True) use_pan = Bool(True) plot = None def cmap_plot(self, z): from chaco.array_plot_data import ArrayPlotData from chaco.plot import Plot from chaco.default_colormaps import color_map_name_dict pd = ArrayPlotData() pd.set_data('cmapdata', z) p = Plot(pd, padding=0) p.img_plot('cmapdata', xbounds=(-25, 25), ybounds=(-25, 25), colormap=color_map_name_dict['hot']) self.add(p) return pd def line_plot(self, x, y, new_plot=True): if self.plot is None or new_plot: if isinstance(x, (float, int)): x = [x] if isinstance(y, (float, int)): y = [y] self.plot = LinePlot( index=ArrayDataSource(x), value=ArrayDataSource(y), index_mapper=LinearMapper(range=self.index_range), value_mapper=LinearMapper(range=self.value_range)) self.add(self.plot) else: datax = self.plot.index.get_data() datay = self.plot.value.get_data() nx = hstack((datax, [x])) ny = hstack((datay, [y])) self.plot.index.set_data(nx) self.plot.value.set_data(ny) def reset_plots(self): self.plot = None for c in self.components[:1]: self.remove(c) self.request_redraw() def __init__(self, *args, **kw): """ """ super(BaseDataCanvas, self).__init__(*args, **kw) if 'x_range' not in kw: self.x_range = (-25, 25) if 'y_range' not in kw: self.y_range = (-25, 25) if 'view_x_range' not in kw: self.view_x_range = (-25, 25) if 'view_y_range' not in kw: self.view_y_range = (-25, 25) # plot=BaseXYPlot plot = LinePlot sp = plot(index=ArrayDataSource(self.y_range), value=ArrayDataSource(self.x_range), index_mapper=LinearMapper(range=self.index_range), value_mapper=LinearMapper(range=self.value_range)) self.index_range.sources.append(sp.index) self.value_range.sources.append(sp.value) sp.visible = False self.add(sp) if self.use_zoom: self.add_zoom() if self.use_pan: self.add_pan() self.index_mapper.on_trait_change(self.update, 'updated') self.value_mapper.on_trait_change(self.update, 'updated') # set the view range self.set_mapper_limits('x', self.view_x_range) self.set_mapper_limits('y', self.view_y_range) # if not self.show_axes: # self.value_axis.visible = False # self.index_axis.visible = False self.value_axis.visible = self.show_axes self.index_axis.visible = self.show_axes self.x_grid.visible = self.show_grids self.y_grid.visible = self.show_grids @on_trait_change('view_x_range') def _update_xrange(self): self.set_mapper_limits('x', self.view_x_range) @on_trait_change('view_y_range') def _update_yrange(self): self.set_mapper_limits('y', self.view_y_range) @on_trait_change('show_grids') def change_grid_visibility(self): try: self.x_grid.visible = self.show_grids self.y_grid.visible = self.show_grids self.request_redraw() except AttributeError: pass def set_mapper_limits(self, mapper, limits, pad=0): """ """ mapper = getattr(self, '{}_mapper'.format(mapper)) if mapper is not None: mapper.range.low_setting = limits[0] - pad mapper.range.high_setting = limits[1] + pad self.request_redraw() def get_mapper_limits(self, mapper): mapper = getattr(self, '{}_mapper'.format(mapper)) return mapper.range.low, mapper.range.high def update(self, *args, **kw): """ """ pass def add_pan(self): """ """ p = PanTool(self) self.tools.append(p) def add_zoom(self): """ """ z = ZoomTool(component=self, always_on=False, tool_mode='box', max_zoom_out_factor=1, max_zoom_in_factor=10000) # b=BroadcasterTool() # b.tools.append(z) self.overlays.append(z) # self.tools.append(b) def get_wh(self, *args): return self._get_wh(*args) def _get_wh(self, w, h): """ """ wh, oo = self.map_screen([(w, h), (0, 0)]) w = wh[0] - oo[0] h = wh[1] - oo[1] return w, h def _vertical_line(self, gc, x, y1, y2, color=(0, 0, 0)): """ """ p1 = (x, y1) p2 = (x, y2) self.line_segment(gc, p1, p2, color) def _horizontal_line(self, gc, y, x1, x2, color=(0, 0, 0)): """ """ p1 = (x1, y) p2 = (x2, y) self.line_segment(gc, p1, p2, color) def _line_segment(self, gc, p1, p2, color=None): if color is not None: gc.set_stroke_color(color) gc.move_to(*p1) gc.line_to(*p2) gc.draw_path()
class PointLine(DrawingTool): """ A point-to-point drawn line. """ # Our contained "Line" instance; it stores the points and does the actual # drawing. line = Instance(Line, args=()) # Override the draw_mode value we inherit from DrawingTool draw_mode = "overlay" # The pixel distance from a vertex that is considered 'on' the vertex. proximity_distance = Int(4) # The cursor shapes to use for various modes normal_cursor = Pointer("arrow") drawing_cursor = Pointer("pencil") delete_cursor = Pointer("bullseye") move_cursor = Pointer("sizing") # The index of the vertex being dragged, if any. _dragged = Int complete = Event def add_point(self, point): """ Add the point. """ self.line.points.append(point) def get_point(self, index): """ Get the point at the specified index. """ return self.line.points[index] def set_point(self, index, point): """ Set the point at the specified index to point. """ self.line.points[index] = point def remove_point(self, index): """ Remove the point with the specified index. """ del self.line.points[index] # ------------------------------------------------------------------------ # DrawingTool interface # ------------------------------------------------------------------------ def reset(self): self.line.points = [] self.event_state = "normal" # ------------------------------------------------------------------------ # "complete" state # ------------------------------------------------------------------------ def complete_draw(self, gc): # Draw the completed line self.line.line_dash = None with gc: self.line._draw_mainlayer(gc) def complete_left_down(self, event): """ Handle the left mouse button going down in the 'complete' state. """ # Ignore the click if it contains modifiers we do not handle. if event.shift_down or event.alt_down: event.handled = False else: # If we are over a point, we will either move it or remove it. over = self._over_point(event, self.line.points) if over is not None: # Control down means remove it. if event.control_down: self.remove_point(over) self.updated = self # Otherwise, prepare to drag it. else: self._dragged = over event.window.set_pointer(self.move_cursor) self.event_state = "drag_point" self.request_redraw() def complete_mouse_move(self, event): """ Handle the mouse moving in the 'complete' state. """ # If we are over a point, then we have to prepare to move it. over = self._over_point(event, self.line.points) if over is not None: if event.control_down: event.window.set_pointer(self.delete_cursor) else: event.window.set_pointer(self.move_cursor) else: event.handled = False event.window.set_pointer(self.normal_cursor) self.request_redraw() # ------------------------------------------------------------------------ # "drag" state # ------------------------------------------------------------------------ def drag_point_draw(self, gc): """ Draw the polygon in the 'drag_point' state. """ self.line._draw_mainlayer(gc) def drag_point_left_up(self, event): """ Handle the left mouse coming up in the 'drag_point' state. """ self.event_state = "complete" self.updated = self def drag_point_mouse_move(self, event): """ Handle the mouse moving in the 'drag_point' state. """ # Only worry about the event if it's inside our bounds. dragged_point = self.get_point(self._dragged) # If the point has actually moved, update it. if dragged_point != (event.x, event.y): self.set_point(self._dragged, (event.x, event.y)) self.request_redraw() # ------------------------------------------------------------------------ # "incomplete" state # ------------------------------------------------------------------------ def incomplete_draw(self, gc): """ Draw the line in the 'incomplete' state. """ with gc: gc.set_fill_color((0, 0, 0, 0)) gc.rect(50, 50, 100, 100) self.line._draw_mainlayer(gc) def incomplete_left_dclick(self, event): """ Handle a left double-click in the incomplete state. """ # Remove the point that was placed by the first mouse down, since # another one will be placed on the down stroke of the double click. self.remove_point(-1) event.window.set_pointer(self.move_cursor) self.event_state = "complete" self.complete = True self.request_redraw() def incomplete_left_down(self, event): """ Handle the left mouse button coming up in incomplete state. """ # Add the point. self.add_point((event.x, event.y)) self.updated = self def incomplete_mouse_move(self, event): """ Handle the mouse moving in incomplete state. """ # If we move over the initial point, then we change the cursor. event.window.set_pointer(self.drawing_cursor) # If the point has actually changed, then we need to update our model. if self.get_point(-1) != (event.x, event.y): self.set_point(-1, (event.x, event.y)) self.request_redraw() # ------------------------------------------------------------------------ # "normal" state # ------------------------------------------------------------------------ def normal_left_down(self, event): """ Handle the left button up in the 'normal' state. """ # Append the current point twice, because we need to have the starting # point and the current point be separate, since the current point # will be moved with the mouse from now on. self.add_point((event.x, event.y)) self.add_point((event.x, event.y)) self.event_state = "incomplete" self.updated = self self.line_dash = (4.0, 2.0) def normal_mouse_move(self, event): """ Handle the mouse moving in the 'normal' state. """ event.window.set_pointer(self.drawing_cursor) # ------------------------------------------------------------------------ # Private interface # ------------------------------------------------------------------------ def _updated_fired(self, event): # The self.updated trait is used by point_line and can be used by # others to indicate that the model has been updated. For now, the # only action taken is to do a redraw. self.request_redraw() def _is_near_point(self, point, event): """ Determine if the pointer is near a specified point. """ event_point = (event.x, event.y) return (abs(point[0] - event_point[0]) + abs(point[1] - event_point[1])) <= self.proximity_distance def _is_over_start(self, event): """ Test if the event is 'over' the starting vertex. """ return len(self.points) > 0 and self._is_near_point( self.points[0], event) def _over_point(self, event, points): """ Return the index of a point in points that event is 'over'. Returns None if there is no such point. """ for i, point in enumerate(points): if self._is_near_point(point, event): result = i break else: result = None return result
class Circle(Component): """ The circle moves with the mouse cursor but leaves a translucent version of itself in its original position until the mouse button is released. """ color = (0.3, 0.4, 0.8, 1.0) bgcolor = "none" normal_pointer = Pointer("arrow") moving_pointer = Pointer("hand") offset_x = Float offset_y = Float shadow_type = Enum("light", "dashed") shadow = Instance(Component) def __init__(self, **traits): Component.__init__(self, **traits) self.pointer = self.normal_pointer def _draw_mainlayer(self, gc, view_bounds=None, mode="default"): with gc: gc.set_fill_color(self.color) dx, dy = self.bounds x, y = self.position radius = min(dx / 2.0, dy / 2.0) gc.arc(x + dx / 2.0, y + dy / 2.0, radius, 0.0, 2 * 3.14159) gc.fill_path() def normal_left_down(self, event): self.event_state = "moving" self.pointer = self.moving_pointer # Create our shadow if self.shadow_type == "light": klass = LightCircle else: klass = DashedCircle dx, dy = self.bounds self.shadow = klass(bounds=self.bounds, position=self.position, color=self.color) self.container.add(self.shadow) x, y = self.position self.offset_x = event.x - x self.offset_y = event.y - y def moving_mouse_move(self, event): self.position = [event.x - self.offset_x, event.y - self.offset_y] self.request_redraw() def moving_left_up(self, event): self.event_state = "normal" self.pointer = self.normal_pointer self.request_redraw() # Remove our shadow self.container.remove(self.shadow) def moving_mouse_leave(self, event): self.moving_left_up(event)
class Bullet(Component): color = ColorTrait("gray") c_selected = ColorTrait("yellow") c_up = ColorTrait("yellow") c_dragging = ColorTrait("yellow") c_over = ColorTrait("yellow") c_dropping = ColorTrait("yellow") drag_color = ColorTrait((0.6, 0.6, 0.6, 0.5)) border_color = ColorTrait((0.4, 0.4, 0.4, 1.0)) bgcolor = "clear" bullet_state = Enum("up", "down", "dragging", "over", "dropping") normal_pointer = Pointer("arrow") moving_pointer = Pointer("hand") # This is currently determines the size of the widget, since we're just # drawing a triangle for now bounds=[9,12] def perform(self, event): """ Called when the button is depressed. 'event' is the Enable mouse event that triggered this call. """ pass def _draw_mainlayer(self, gc, view_bounds, mode="default"): if self.event_state == "selected": enable_glyph_lib.io_bullet_over(gc, self.x, self.y, self.width, self.height, self.c_selected_) elif self.bullet_state == "up": enable_glyph_lib.io_bullet_up(gc, self.x, self.y, self.width, self.height, self.color_) elif self.bullet_state == "dragging": enable_glyph_lib.io_bullet_drag(gc, self.x, self.y, self.width, self.height, self.c_dragging_) elif self.bullet_state == "over": enable_glyph_lib.io_bullet_over(gc, self.x, self.y, self.width, self.height, self.c_over_) elif self.bullet_state == "dropping": enable_glyph_lib.io_bullet_drop_target(gc, self.x, self.y, self.width, self.height, self.color_) else: enable_glyph_lib.io_bullet_down(gc, self.x, self.y, self.width, self.height, self.color_) return def normal_left_down(self, event): self.bullet_state = "down" self.pointer = self.moving_pointer self.request_redraw() event.handled = True def normal_mouse_enter(self, event): self.bullet_state = "over" self.pointer = self.moving_pointer self.request_redraw() event.handled = True def normal_mouse_leave(self, event): self.bullet_state = "up" self.pointer = self.normal_pointer self.request_redraw() event.handled = True def normal_left_up(self, event): self.bullet_state = "up" self.pointer = self.normal_pointer self.request_redraw() self.perform(event) event.handled = True