Source code for gaphas.tool.itemtool

import logging
from functools import singledispatch
from typing import Optional, Tuple, Union

from gi.repository import Gdk, Gtk

from gaphas.canvas import ancestors
from gaphas.connector import Handle
from gaphas.geometry import distance_line_point, distance_point_point_fast
from gaphas.handlemove import HandleMove, item_at_point
from gaphas.item import Item
from gaphas.move import Move
from gaphas.types import Pos
from gaphas.view import GtkView

log = logging.getLogger(__name__)


[docs]def item_tool(view: GtkView) -> Gtk.GestureDrag: """Handle item movement and movement of handles.""" gesture = ( Gtk.GestureDrag.new(view) if Gtk.get_major_version() == 3 else Gtk.GestureDrag.new() ) drag_state = DragState() gesture.connect("drag-begin", on_drag_begin, drag_state) gesture.connect("drag-update", on_drag_update, drag_state) gesture.connect("drag-end", on_drag_end, drag_state) return gesture
class DragState: def __init__(self): self.reset() def reset(self): self.moving_items = set() self.moving_handle = None @property def moving(self): yield from self.moving_items if self.moving_handle: yield self.moving_handle def on_drag_begin(gesture, start_x, start_y, drag_state): view = gesture.get_widget() pos = (start_x, start_y) selection = view.selection modifiers = ( gesture.get_last_event(None).get_state()[1] if Gtk.get_major_version() == 3 else gesture.get_current_event_state() ) item, handle = find_item_and_handle_at_point(view, pos) # Deselect all items unless CTRL or SHIFT is pressed # or the item is already selected. if not ( modifiers & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) or item in selection.selected_items ): selection.unselect_all() if not item: gesture.set_state(Gtk.EventSequenceState.DENIED) return if ( not handle and item in selection.selected_items and modifiers & Gdk.ModifierType.CONTROL_MASK ): selection.unselect_item(item) gesture.set_state(Gtk.EventSequenceState.DENIED) return if not handle and item is view.selection.focused_item: handle = maybe_split_segment(view, item, pos) selection.focused_item = item gesture.set_state(Gtk.EventSequenceState.CLAIMED) if handle: drag_state.moving_handle = HandleMove(item, handle, view) else: drag_state.moving_items = set(moving_items(view)) for moving in drag_state.moving: moving.start_move((start_x, start_y)) def find_item_and_handle_at_point( view: GtkView, pos: Pos ) -> Tuple[Optional[Item], Optional[Handle]]: item, handle = handle_at_point(view, pos) return item or next(item_at_point(view, pos), None), handle # type: ignore[call-overload] def moving_items(view): """Filter the items that should eventually be moved. Returns Move aspects for the items. """ selected_items = set(view.selection.selected_items) for item in selected_items: # Do not move subitems of selected items if not set(ancestors(view.model, item)).intersection(selected_items): yield Move(item, view) def on_drag_update(gesture, offset_x, offset_y, drag_state): _, sx, sy = gesture.get_start_point() view = gesture.get_widget() allocation = view.get_allocation() x = sx + offset_x y = sy + offset_y for moving in drag_state.moving: moving.move((x, y)) if not (0 <= x <= allocation.width and 0 <= y <= allocation.height): view.clamp_item(view.selection.focused_item) def on_drag_end(gesture, offset_x, offset_y, drag_state): _, x, y = gesture.get_start_point() for moving in drag_state.moving: moving.stop_move((x + offset_x, y + offset_y)) if drag_state.moving_handle: moving = drag_state.moving_handle maybe_merge_segments(gesture.get_widget(), moving.item, moving.handle) drag_state.reset() def order_handles(handles): if handles: yield handles[0] yield handles[-1] yield from handles[1:-1] def handle_at_point( view: GtkView, pos: Pos, distance: int = 6 ) -> Union[Tuple[Item, Handle], Tuple[None, None]]: """Look for a handle at ``pos`` and return the tuple (item, handle).""" def find(item): """Find item's handle at pos.""" v2i = view.get_matrix_v2i(item) d = distance_point_point_fast(v2i.transform_distance(0, distance)) x, y = v2i.transform_point(*pos) for h in order_handles(item.handles()): if not h.movable: continue hx, hy = h.pos if -d < (hx - x) < d and -d < (hy - y) < d: return h selection = view.selection # The focused item is the preferred item for handle grabbing if selection.focused_item: h = find(selection.focused_item) if h: return selection.focused_item, h # then try hovered item if selection.hovered_item: h = find(selection.hovered_item) if h: return selection.hovered_item, h # Last try all items, checking the bounding box first x, y = pos items = reversed( list( view.get_items_in_rectangle( (x - distance, y - distance, distance * 2, distance * 2) ) ) ) for item in items: h = find(item) if h: return item, h return None, None @singledispatch class Segment: def __init__(self, item, model): raise TypeError def split_segment(self, segment, count=2): ... def split(self, pos): ... def merge_segment(self, segment, count=2): ... def maybe_split_segment(view, item, pos): try: segment = Segment(item, view.model) except TypeError: return None else: cpos = view.matrix.inverse().transform_point(*pos) return segment.split(cpos) def maybe_merge_segments(view, item, handle): handles = item.handles() # don't merge using first or last handle if handles[0] is handle or handles[-1] is handle: return # ensure at least three handles handle_index = handles.index(handle) segment = handle_index - 1 # cannot merge starting from last segment if segment == len(item.ports()) - 1: segment = -1 assert segment >= 0 and segment < len(item.ports()) - 1 before = handles[handle_index - 1] after = handles[handle_index + 1] d, p = distance_line_point(before.pos, after.pos, handle.pos) if d > 4: return try: Segment(item, view.model).merge_segment(segment) except ValueError: pass else: if handle: view.model.request_update(item)