Source code for rkviewer.canvas.overlays

"""Widgets that are floating on top of the canvas (overlaid) which do not change position on scroll.
"""
# pylint: disable=maybe-no-member
from sortedcontainers.sortedlist import SortedKeyList
from rkviewer.canvas.elements import CanvasElement, CompartmentElt, NodeElement
from rkviewer.canvas.state import cstate
from rkviewer.config import Color
import wx
import abc
from typing import Callable, List, cast
from .geometry import Vec2, Rect, clamp_point, pt_in_rect
from .utils import draw_rect


[docs]class CanvasOverlay(abc.ABC): """Abstract class for a fixed-position overlay within the canvas. Attributes: hovering: Used to set whether the mouse is current hovering over the overlay. Note: Overlays use device positions since it makes the most sense for these static items. """ hovering: bool _size: Vec2 #: Private attribute for the 'size' property. _position: Vec2 #: Private attribute for the 'position' property. @property def size(self) -> Vec2: """Return the size (i.e. of a rectangle) of the overlay.""" return self._size @property def position(self) -> Vec2: """Return the position (i.e. of the top-left corner) of the overlay.""" return self._position @position.setter def position(self, val: Vec2): self._position = val
[docs] @abc.abstractmethod def DoPaint(self, gc: wx.GraphicsContext): """Re-paint the overlay.""" pass
[docs] @abc.abstractmethod def OnLeftDown(self, device_pos: Vec2): """Trigger a mouse left button down event on the overlay.""" pass
[docs] @abc.abstractmethod def OnLeftUp(self, device_pos: Vec2): """Trigger a mouse left button up event on the overlay.""" pass
[docs] @abc.abstractmethod def OnMotion(self, device_pos: Vec2, is_down: bool): """Trigger a mouse motion event on the overlay.""" pass
# TODO refactor this as a CanvasElement and delete this file
[docs]class Minimap(CanvasOverlay): """The minimap class that derives from CanvasOverlay. Attributes: Callback: Type of the callback function called when the position of the minimap changes. window_pos: Position of the canvas window, as updated by canvas. window_size: Size of the canvas window, as updated by canvas. device_pos: The device position (i.e. seen on screen) of the top left corner. Used for determining user click/drag offset. It is important to use the device_pos, since it does not change, whereas window_pos (logical position) changes based on scrolling. This coupled with delays in update causes very noticeable jitters when dragging. elements: The list of elements updated by canvas. """ Callback = Callable[[Vec2], None] window_pos: Vec2 window_size: Vec2 device_pos: Vec2 elements: SortedKeyList _position: Vec2 #: Unscrolled, i.e. logical position of the minimap. This varies by scrolling. _realsize: Vec2 #: Full size of the canvas _width: int _callback: Callback #: the function called when the minimap position changes _dragging: bool _drag_rel: Vec2 """Position of the mouse relative to the top-left corner of the visible window handle on minimap, the moment when dragging starts. We keep this relative distance invariant while dragging. This is used because scrolling is discrete, so we cannot add relative distance dragged since errors will accumulate. """ def __init__(self, *, pos: Vec2, device_pos: Vec2, width: int, realsize: Vec2, window_pos: Vec2 = Vec2(), window_size: Vec2, pos_callback: Callback): """The constructor of the minimap Args: pos: The position of the minimap relative to the top-left corner of the canvas window. width: The width of the minimap. The height will be set according to perspective. realsize: The actual, full size of the canvas. window_pos: The starting position of the window. window_size: The starting size of the window. pos_callback: The callback function called when the minimap window changes position. """ self._position = pos self.device_pos = device_pos # should stay fixed self._width = width self.realsize = realsize # use the setter to set the _size as well self.window_pos = window_pos self.window_size = window_size self.elements = SortedKeyList() self._callback = pos_callback self._dragging = False self._drag_rel = Vec2() self.hovering = False @property def realsize(self): """The actual, full size of the canvas, including those not visible on screen.""" return self._realsize @realsize.setter def realsize(self, val: Vec2): self._realsize = val self._size = Vec2(self._width, self._width * val.y / val.x) @property def dragging(self): """Whether the user is current dragging on the minimap window.""" return self._dragging
[docs] def DoPaint(self, gc: wx.GraphicsContext): # TODO move this somewhere else BACKGROUND_USUAL = wx.Colour(155, 155, 155, 50) FOREGROUND_USUAL = wx.Colour(255, 255, 255, 100) BACKGROUND_FOCUS = wx.Colour(155, 155, 155, 80) FOREGROUND_FOCUS = wx.Colour(255, 255, 255, 130) FOREGROUND_DRAGGING = wx.Colour(255, 255, 255, 200) background = BACKGROUND_FOCUS if (self.hovering or self._dragging) else BACKGROUND_USUAL foreground = FOREGROUND_USUAL if self._dragging: foreground = FOREGROUND_DRAGGING elif self.hovering: foreground = FOREGROUND_FOCUS scale = self._size.x / self._realsize.x draw_rect(gc, Rect(self.position, self._size), fill=background) my_botright = self.position + self._size win_pos = self.window_pos * scale + self.position win_size = self.window_size * scale # clip window size span = my_botright - win_pos win_size = win_size.reduce2(min, span) # draw visible rect draw_rect(gc, Rect(win_pos, win_size), fill=foreground) for el in self.elements: pos: Vec2 size: Vec2 fc: wx.Colour if isinstance(el, NodeElement): el = cast(NodeElement, el) pos = el.node.position * scale + self.position size = el.node.size * scale fc = (el.node.fill_color or Color(128, 128, 128)).to_wxcolour() elif isinstance(el, CompartmentElt): el = cast(CompartmentElt, el) pos = el.compartment.position * scale + self.position size = el.compartment.size * scale fc = el.compartment.fill else: continue color = wx.Colour(fc.Red(), fc.Green(), fc.Blue(), 100) draw_rect(gc, Rect(pos, size), fill=color)
[docs] def OnLeftDown(self, device_pos: Vec2): if not self._dragging: scale = self._size.x / self._realsize.x pos = device_pos - self.device_pos if pt_in_rect(pos, Rect(self.window_pos * scale, self.window_size * scale)): self._dragging = True self._drag_rel = pos - self.window_pos * scale else: topleft = pos - self.window_size * scale / 2 self._callback(topleft / scale * cstate.scale)
[docs] def OnLeftUp(self, _: Vec2): self._dragging = False
[docs] def OnMotion(self, device_pos: Vec2, is_down: bool): scale = self._size.x / self._realsize.x pos = device_pos - self.device_pos pos = clamp_point(pos, Rect(Vec2(), self.size)) if is_down: if not self._dragging: topleft = pos - self.window_size * scale / 2 self._callback(topleft / scale * cstate.scale) else: actual_pos = pos - self._drag_rel self._callback(actual_pos / scale * cstate.scale)