Source code for arrow_designer

"""
Arrow tip designer for reaction plugin.

Version 0.01: Author: Gary Geng (2020)
"""

# pylint: disable=maybe-no-member
from rkviewer.utils import opacity_mul
from rkviewer.canvas.state import ArrowTip
import wx
from typing import List, Tuple
from rkviewer.plugin import api
from rkviewer.plugin.classes import PluginMetadata, WindowedPlugin, PluginCategory
from rkviewer.plugin.api import Vec2


[docs]class DesignerWindow(wx.Window): """ The arrow designer window. """ def __init__(self, parent, arrow_tip: ArrowTip): """ Initialize the arrow designer window with the given starting arrow tip. Args: parent: The parent window. arrow_tip: ArrowTip object defining the arrow tip used. """ dim = Vec2(22, 16) self.csize = 20 size = dim * self.csize + Vec2.repeat(1) super().__init__(parent, size=size.as_tuple()) # add 1 to range end, so that the grid rectangle side will be included. rows = [r for r in range(0, int(size.y), self.csize)] cols = [c for c in range(0, int(size.x), self.csize)] self.begin_points = list() self.end_points = list() for r in rows: self.begin_points.append(wx.Point2D(0, r)) self.end_points.append(wx.Point2D(size.x - 1, r)) for c in cols: self.begin_points.append(wx.Point2D(c, 0)) self.end_points.append(wx.Point2D(c, size.y - 1)) self.handle_c = api.get_theme('handle_color') self.hl_handle_c = api.get_theme('highlighted_handle_color') self.handle_pen = wx.Pen(self.handle_c) self.hl_handle_pen = wx.Pen(self.hl_handle_c) self.handle_brush = wx.Brush(self.handle_c) self.hl_handle_brush = wx.Brush(self.hl_handle_c) phantom_c = opacity_mul(self.handle_c, 0.5) self.phantom_pen = wx.Pen(phantom_c) self.phantom_brush = wx.Brush(phantom_c) self.arrow_tip = arrow_tip self.radius = 12 self.radius_sq = self.radius ** 2 self.hover_idx = -1 self.dragged_point = None self.dragging = False self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMotion) self.SetDoubleBuffered(True)
[docs] def OnPaint(self, evt): """ Overrides wx Paint event to draw the grid, arrow, etc. as if on a canvas. Args: self: the Designer Window to initialize. evt: the event being executed. """ dc = wx.PaintDC(self) gc = wx.GraphicsContext.Create(dc) self.draw_background(gc) self.draw_points(gc, self.arrow_tip.points, self.radius) evt.Skip()
[docs] def OnLeftDown(self, evt: wx.MouseEvent): """ Handler for mouse left button down event. """ if self.hover_idx != -1: self.dragging = True
[docs] def OnLeftUp(self, evt: wx.MouseEvent): """ Handler for mouse left button up event. """ if self.dragging: assert self.dragged_point is not None drop = self.projected_landing(self.dragged_point) assert self.hover_idx != -1 self.dragging = False self.arrow_tip.points[self.hover_idx] = Vec2(drop.x // self.csize, drop.y // self.csize) self.update_hover_idx(Vec2(evt.GetPosition())) self.Refresh()
[docs] def update_arrow_tip(self, arrow_tip: ArrowTip): """ Updating the current arrow tip in designer. Args: self: the Designer Window to initialize. arrow_tip: modified arrow tip. """ self.arrow_tip = arrow_tip self.Refresh()
[docs] def projected_landing(self, point: Vec2) -> Vec2: """ Return the projected discrete landing point for the cursor. This is to make sure the user sees where the dragged arrow tip point will be dropped on the grid. Args: point: The cursor position relative to the window. Returns: Vec2 : projected point for landing. """ lx = point.x - point.x % self.csize ly = point.y - point.y % self.csize drop_x: float drop_y: float if point.x - lx < self.csize / 2: drop_x = lx else: drop_x = lx + self.csize if point.y - ly < self.csize / 2: drop_y = ly else: drop_y = ly + self.csize return Vec2(drop_x, drop_y)
[docs] def OnMotion(self, evt: wx.MouseEvent): """ Handler for mouse motion events. Args: self: the Designer Window to initialize. evt: the event being executed. """ pos = Vec2(evt.GetPosition()) if self.dragging: self.dragged_point = pos else: self.update_hover_idx(pos) evt.Skip() self.Refresh()
[docs] def update_hover_idx(self, pos: Vec2): """ Helper to update the hovered arrow tip point index. """ self.hover_idx = -1 for i, pt in enumerate(self.arrow_tip.points): pt *= self.csize if (pos - pt).norm_sq <= self.radius_sq: self.hover_idx = i break
[docs] def draw_background(self, gc: wx.GraphicsContext): """ Drawing the gridlines background. """ gc.SetPen(wx.Pen(wx.BLACK)) gc.StrokeLineSegments(self.begin_points, self.end_points)
[docs] def draw_points(self, gc: wx.GraphicsContext, points: List[Vec2], radius: float): """ Drawing points for arrow. Args: gc: The Graphics context to modify. points: The points to be drawn, in counterclockwise order, with the last point being the tip. radius: The radius of the points. """ gc.SetPen(wx.Pen(wx.BLACK, 2)) gc.SetBrush(wx.Brush(wx.BLACK, wx.BRUSHSTYLE_FDIAGONAL_HATCH)) plotted = [p * self.csize for p in points] # points to be plotted if self.dragging: assert self.hover_idx != -1 and self.dragged_point is not None plotted[self.hover_idx] = self.dragged_point gc.DrawLines([wx.Point2D(*p) for p in plotted] + [wx.Point2D(*plotted[0])]) for i, p in enumerate(plotted): if self.dragging and i == self.hover_idx: continue if i == 3: # the last point is the tip, so draw it in a different color gc.SetPen(wx.BLACK_PEN) gc.SetBrush(wx.BLACK_BRUSH) else: gc.SetPen(self.handle_pen) gc.SetBrush(self.handle_brush) self.draw_point(gc, p, radius) # Draw the hover point if self.hover_idx != -1: point = self.dragged_point if self.dragging else plotted[self.hover_idx] assert self.hover_idx >= 0 and self.hover_idx < 4 assert point is not None gc.SetPen(self.hl_handle_pen) gc.SetBrush(self.hl_handle_brush) self.draw_point(gc, point, radius) if self.dragging: assert self.dragged_point is not None drop_point = self.projected_landing(self.dragged_point) gc.SetPen(self.phantom_pen) gc.SetBrush(self.phantom_brush) self.draw_point(gc, drop_point, radius)
[docs] def draw_point(self, gc: wx.GraphicsContext, point: Vec2, radius: float): """ Drawing a single point. Args: gc: Graphics context to modify. point: Point to be drawn. radius: Radius of the point. """ center = point - Vec2.repeat(radius / 2) gc.DrawEllipse(center.x, center.y, radius, radius)
[docs]class ArrowDesigner(WindowedPlugin): """ The ArrowDesigner plugin that subclasses WindowedPlugin. """ metadata = PluginMetadata( name='ArrowDesigner', author='Gary Geng', version='0.0.1', short_desc='Arrow tip designer for reactions.', long_desc='Arrow tip designer for reactions.', category=PluginCategory.APPEARANCE, ) def __init__(self): super().__init__() self.arrow_tip = api.get_arrow_tip()
[docs] def create_window(self, dialog): """ Called when creating a window. Create the designer window as well as control buttons. """ window = wx.Window(dialog, size=(500, 500)) sizer = wx.BoxSizer(wx.VERTICAL) self.designer = DesignerWindow(window, self.arrow_tip) save_btn = wx.Button(window, label='Save') save_btn.Bind(wx.EVT_BUTTON, self.OnSave) restore_btn = wx.Button(window, label='Restore default') restore_btn.Bind(wx.EVT_BUTTON, self.OnRestore) sizerflags = wx.SizerFlags().Align(wx.ALIGN_CENTER_HORIZONTAL).Border(wx.TOP, 20) sizer.Add(self.designer, sizerflags) sizer.Add(save_btn, sizerflags) sizer.Add(restore_btn, sizerflags) dialog.SetSizer(sizer) return window
[docs] def OnSave(self, evt): """ Handler for the "save" button. Save the new arrow tip. """ api.set_arrow_tip(self.arrow_tip) api.refresh_canvas()
[docs] def OnRestore(self, evt): """ Update the arrow point to be set to default values. """ default_tip = api.get_default_arrow_tip() api.set_arrow_tip(default_tip) self.designer.update_arrow_tip(default_tip)