Source code for pantra.components.controllers

from __future__ import annotations

from abc import ABC, abstractmethod
import typing
import inspect

from .render.render_node import RenderNode
from ..session import trace_errors, trace_errors_async, Session
from ..workers.decorators import thread_worker

if typing.TYPE_CHECKING:
    from typing import *
    from .context import HTMLElement, Context

__all__ = ['DragOptions', 'DragController']


[docs] class DragOptions(typing.NamedTuple): """Initial drag'n'drop control options Attributes: mouse_button: code of `mouse button <https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button>`__ to push for drag allow_move_by_x: allow move dragged box by x-axis allow_move_by_y: allow move dragged box by y-axis border_top: drag zone border top max coordinate border_bottom: border bottom max coordinate border_left: border left max coordinate border_right: border right max coordinate """ mouse_button: int = 0 allow_move_by_x: bool = True allow_move_by_y: bool = True border_top: Optional[int] = None border_left: Optional[int] = None border_bottom: Optional[int] = None border_right: Optional[int] = None
[docs] class DragController(ABC): """Abstract class to perform drag'n'drop action Attributes: from_x: initial x coordinate from_y: initial y coordinate x: current x coordinate y: current y coordinate delta_x: changes of x coordinate since last time delta_y: changes of y coordinate since last time options: control options in_moving: box in moving process """ def __init__(self, node: HTMLElement): self.from_x: int = 0 self.from_y: int = 0 self.x: int = 0 self.y: int = 0 self.delta_x: int = 0 self.delta_y: int = 0 self.options: DragOptions = self.get_options(node) self.in_moving: bool = False
[docs] def get_options(self, node: HTMLElement): """get options for node Override this method for custom options for specified node """ return DragOptions()
def mousedown(self, node, x, y, mouse_button): if mouse_button != self.options.mouse_button: return False self.from_x = self.x = x self.from_y = self.y = y self.in_moving = True return self.start(node) def mousemove(self, x, y): if self.options.allow_move_by_x: if self.options.border_left is not None and x < self.options.border_left: x = self.options.border_left elif self.options.border_right is not None and x > self.options.border_right: x = self.options.border_right self.delta_x = x-self.x self.x = x if self.options.allow_move_by_y: if self.options.border_top is not None and y < self.options.border_top: y = self.options.border_top elif self.options.border_bottom is not None and y > self.options.border_bottom: y = self.options.border_bottom self.delta_y = y-self.y self.y = y self.move() def mouseup(self): self.in_moving = False self.stop()
[docs] @abstractmethod def start(self, node) -> bool: """method invoked when drag action performed and satisfied to options""" return False
[docs] @abstractmethod def move(self): """method invoked on drag box moving"""
[docs] @abstractmethod def stop(self): """method invoked on drag box drop"""
@thread_worker @trace_errors def process_drag_start(session: Session, method: str, oid: int, x: int, y: int, button: int): node: Context = session.get_node(oid) if node is None: return func = node[method] session.state['drag']: DragController = func(node) if session.state['drag'].mousedown(node, x, y, button): session.send_message({'m': 'dm'}) @thread_worker @trace_errors def process_drag_move(session: Session, x: int, y: int): session.state['drag'].mousemove(x, y) @thread_worker @trace_errors def process_drag_stop(session: Session, x: int, y: int): session.state['drag'].mouseup() del session.state["drag"] @trace_errors def process_call(session: Session, node: Context | HTMLElement, method: str, *args): for m in method.split(' '): if m.startswith('update:'): ref_name = m[len('update:'):] ref_node = node.context.refs.get(ref_name) if ref_node is None: raise ValueError(f"No such ref: `{ref_name}`") if not isinstance(ref_node, RenderNode): raise ValueError(f"Ref: `{ref_name}` must be a RenderNode") ref_node.update(True) return if m.startswith('scope:'): scope_name = m[len('scope:'):] try: caller = node.context.scope[scope_name] except KeyError: raise ValueError(f"No such scope data: `{scope_name}`") else: caller = node.get_caller(m) if caller is None: return elif inspect.iscoroutinefunction(caller): session.server_worker.run_coroutine(node, caller, trace_errors_async(session, caller(*args))) elif callable(caller): with session.server_worker.wrap_session_task(node, caller): caller(*args) else: raise ValueError(f"Can`t call type `{type(caller)}` ({caller})") # node[m] = args[0] @thread_worker def process_click(session: Session, method: str, oid: int): node: HTMLElement = session.get_node(oid) if node is None or not method: return process_call(session, node, method, node) @thread_worker def process_select(session: Session, method: str, oid: int, opts: List[int]): node: HTMLElement = session.get_node(oid) if not node or not method: return opts = [session.get_node(i) for i in opts] node._value = len(opts) == 1 and opts[0] or opts process_call(session, node, method, node) @thread_worker def process_change(session: Session, method: str, oid: int, value: Any): node: HTMLElement = session.get_node(oid) if not node: return node._value = value process_call(session, node, method, node) def process_bind_value(node: HTMLElement, variable: str, value: str): node._value = value node.set_quietly(variable, value) @thread_worker def process_key(session: Session, method: str, oid: int, key: str): node: HTMLElement = session.get_node(oid) if not node or not method: return process_call(session, node, method, node, key) @thread_worker def process_direct_call(session: Session, oid: int, method: str, args: list[Any]): node: Context = session.get_node(oid) if not node or not method: return if node.is_call_allowed(method): process_call(session, node, method, args)