Source code for rkviewer.events

"""Custom events dispatched by the canvas.

These events may later be used within a plugin system, where plugins are allowed to bind their own
handlers to these events.
"""
# from __future__ import annotations
# pylint: disable=maybe-no-member
from collections import defaultdict
from dataclasses import dataclass, fields, is_dataclass
from typing import (
    Any,
    Callable,
    DefaultDict,
    Dict,
    List,
    Optional,
    Set,
    Tuple,
    Type, Union,
)

import wx

from rkviewer.canvas.geometry import Vec2

# ------------------------------------------------------------
from inspect import getframeinfo, stack
# ------------------------------------------------------------

[docs]class CanvasEvent:
[docs] def to_tuple(self): assert is_dataclass(self), "as_tuple requires the CanvasEvent instance to be a dataclass!" return tuple(getattr(self, f.name) for f in fields(self))
[docs]@dataclass class SelectionDidUpdateEvent(CanvasEvent): """Called after the list of selected nodes and/or reactions has changed. Attributes: node_indices: The indices of the list of selected nodes. reaction_indices: The indices of the list of selected reactions. compartment_indices: The indices of the list of selected compartments. """ node_indices: Set[int] reaction_indices: Set[int] compartment_indices: Set[int]
[docs]@dataclass class CanvasDidUpdateEvent(CanvasEvent): """Called after the canvas has been updated by the controller.""" pass
[docs]@dataclass class DidNewNetworkEvent(CanvasEvent): """ Called when the canvas is cleared by choosing "New". """ pass
[docs]@dataclass class DidMoveNodesEvent(CanvasEvent): """Called after the position of a node changes but has not been committed to the model. This event may be called many times, continuously as the user drags a group of nodes. Note that only after the drag operation has ended, is model notified of the move for undo purposes. See DidCommitDragEvent. Attributes: node_indices: The indices of the nodes that were moved. offset: The position offset. If all nodes were moved by the same offset, then a single Vec2 is given; otherwise, a list of offsets are given, with each offset matching a node. dragged: Whether the resize operation was done by the user dragging, and not, for exmaple, through the form. by_user: Whether the event was performed by the user or through a plugin. """ node_indices: List[int] offset: Union[Vec2, List[Vec2]] dragged: bool by_user: bool = True
[docs]@dataclass class DidMoveCompartmentsEvent(CanvasEvent): """ Same as `DidMoveNodesEvent` but for compartments. Attributes: compartment_indices: The indices of the compartments that were moved. offset: The position offset. If all compartments were moved by the same offset, then a single Vec2 is given; otherwise, a list of offsets are given, with each offset matching a node. dragged: Whether the resize operation was done by the user dragging, and not, for example, through the form. by_user: Whether the event was performed by the user or through a plugin. """ compartment_indices: List[int] offset: Union[Vec2, List[Vec2]] dragged: bool by_user: bool = True
[docs]@dataclass class DidResizeNodesEvent(CanvasEvent): """Called after the list of selected nodes has been resized. Attributes: node_indices: The indices of the list of resized nodes. ratio: The resize ratio. dragged: Whether the resize operation was done by the user dragging, and not, for exmaple, through the form. by_user: Whether the event was performed by the user or through a plugin. """ node_indices: List[int] ratio: Vec2 dragged: bool by_user: bool = True
[docs]@dataclass class DidResizeCompartmentsEvent(CanvasEvent): """Called after the list of selected compartments has been resized. Attributes: compartment_indices: The indices of the list of resized compartments. ratio: The resize ratio. dragged: Whether the resize operation was done by the user dragging, and not, for exmaple, through the form. by_user: Whether the event was performed by the user or through a plugin. """ compartment_indices: List[int] ratio: Union[Vec2, List[Vec2]] dragged: bool by_user: bool = True
[docs]@dataclass class DidCommitDragEvent(CanvasEvent): """Dispatched after any continuously emitted dragging event has concluded. This is dispatched for any event that is posted in quick intervals while the mouse left button is held while moving, i.e. "dragging" events. This includes: DidMoveNodesEvent, DidMoveCompartmentsEvent, DidResizeNodesEvent, DidResizeCompartmentsEvent, and DidResizeMoveBezierHandlesEvent. This event is emitted after the left mouse button is released, the model is notified of the change, and the action is complete. """ source: Any
[docs]@dataclass class DidMoveBezierHandleEvent(CanvasEvent): """Dispatched after a Bezier handle is moved. Attributes: net_index: The network index. reaction_index: The reaction index. node_index: The index of the node whose Bezier handle moved. -1 if the source centroid handle was moved, or -2 if the dest centroid handle was moved. direct: Automatically true when by_user is False. Otherwise, True if the handle is moved by the user dragging the handle directly, and False if the handle was moved by the user dragging the node associated with that handle. by_user: Whether the event was performed by the user or through a plugin. """ net_index: int reaction_index: int node_index: int by_user: bool direct: bool
[docs]@dataclass class DidMoveReactionCenterEvent(CanvasEvent): """Dispatched after the reaction center is moved by the user. Note that this is not triggered if the center moved automatically due to nodes moving. Attributes: net_index: The network index. reaction_index: The reaction index. offset: The amount moved. dragged: Whether the center is moved by the user dragging (it could have been through the form). """ net_index: int reaction_index: int offset: Vec2 dragged: bool
[docs]@dataclass class DidAddNodeEvent(CanvasEvent): """Called after a node has been added. Attributes: node: The index of the node that was added. Note: This event triggers only if the user has performed a drag operation, and not, for example, if the user moved a node in the edit panel. TODO in the documentation that this event and related ones (and DidDelete-) are emitted before controller.end_group() is called. As an alternative, maybe create a call_after() function similar to wxPython? it should be called in OnIdle() or Refresh() """ node: int
[docs]@dataclass class DidDeleteEvent(CanvasEvent): """Called after a node has been deleted. Attributes: node_indices: The set of nodes (indices )that were deleted. reaction_indices: The set of reactions (indices) that were deleted. compartment_indices: The set of compartment (indices) that were deleted. """ node_indices: Set[int] reaction_indices: Set[int] compartment_indices: Set[int]
[docs]@dataclass class DidAddReactionEvent(CanvasEvent): """Called after a reaction has been added. Attributes: reaction: The Reaction that was added. """ index: int sources: List[int] targets: List[int]
[docs]@dataclass class DidAddCompartmentEvent(CanvasEvent): """Called after a compartment has been added. Attributes: compartment: The Compartment that was added. """ index: int
[docs]@dataclass class DidChangeCompartmentOfNodesEvent(CanvasEvent): """Called after one or more nodes have been moved to a new compartment. Attributes: node_indices: The list of node indices that changed compartment. old_compi: The old compartment index, -1 for base compartment. new_compi: The new compartment index, -1 for base compartment. by_user: Whether this event was triggered directly by a user action, as opposed to by a plugin. """ node_indices: List[int] old_compi: int new_compi: int by_user: bool = True
[docs]@dataclass class DidModifyNodesEvent(CanvasEvent): """Called after a property of one or more nodes has been modified, excluding position or size. For position and size events, see DidMove...Event() and DidResize...Event() Attributes: nodes: The indices of the list of nodes that were modified. by_user: Whether this event was triggered directly by a user action and not, for example, by a plugin. """ indices: List[int] by_user: bool = True
[docs]@dataclass class DidModifyReactionEvent(CanvasEvent): """Called after a property of one or more nodes has been modified, excluding position. Attributes: indices: The indices of the list of reactions that were modified. by_user: Whether this event was triggered directly by a user action and not, for example, by a plugin. """ indices: List[int] by_user: bool = True
[docs]@dataclass class DidModifyCompartmentsEvent(CanvasEvent): """Called after a property of one or more compartments has been modified, excluding position or size. For position and size events, see DidMove...Event() and DidResize...Event() Attributes: indices: The indices of list of compartments that were modified. """ indices: List[int]
[docs]@dataclass class DidUndoEvent(CanvasEvent): """Called after an undo action is done.""" by_user: bool = True
[docs]@dataclass class DidRedoEvent(CanvasEvent): """Called after a redo action is done.""" by_user: bool = True
[docs]@dataclass class DidPaintCanvasEvent(CanvasEvent): """Called after the canvas has been painted. Attributes: gc: The graphics context of the canvas. """ gc: wx.GraphicsContext
EventCallback = Callable[[CanvasEvent], None]
[docs]class HandlerNode: next_: Optional['HandlerNode'] prev: Optional['HandlerNode'] handler: EventCallback def __init__(self, handler: EventCallback): self.handler = handler self.next_ = None
# Maps CanvasElement to a dict that maps events to handler nodes handler_map: Dict[int, Tuple['HandlerChain', HandlerNode]] = dict() # Maps event to a chain of handlers event_chains: DefaultDict[Type[CanvasEvent], 'HandlerChain'] = defaultdict(lambda: HandlerChain()) handler_id = 0
[docs]class HandlerChain: head: Optional[HandlerNode] tail: Optional[HandlerNode] def __init__(self): self.head = None self.tail = None self.it_cur = None
[docs] def remove(self, node: HandlerNode): if node.prev is not None: node.prev.next_ = node.next_ else: self.head = node.next_ if node.next_ is not None: node.next_.prev = node.prev else: self.tail = node.prev
def __iter__(self): self.it_cur = self.head return self def __next__(self): if self.it_cur is None: raise StopIteration() ret = self.it_cur.handler self.it_cur = self.it_cur.next_ return ret
[docs] def append(self, handler: EventCallback) -> HandlerNode: node = HandlerNode(handler) if self.head is None: assert self.tail is None self.head = self.tail = node else: assert self.tail is not None node.prev = self.tail self.tail.next_ = node self.tail = node return node
[docs]def bind_handler(evt_cls: Type[CanvasEvent], callback: EventCallback) -> int: global handler_id ret = handler_id chain = event_chains[evt_cls] hnode = chain.append(callback) handler_map[ret] = (chain, hnode) handler_id += 1 return ret
[docs]def unbind_handler(handler_id: int): chain, hnode = handler_map[handler_id] chain.remove(hnode) del handler_map[handler_id]
[docs]def post_event(evt: CanvasEvent): ''' # debugging if not str(evt)[:14]=="DidPaintCanvas": caller = getframeinfo(stack()[1][0]) print("%s:%d - %s" % (caller.filename, caller.lineno, str(evt))) ''' for callback in iter(event_chains[type(evt)]): callback(evt)