"""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)