Skip to content

Commit

Permalink
Allow items to be selected when covered
Browse files Browse the repository at this point in the history
Covered by another item. Instead look at the distance to a border of an
element, so underlaying elements can be selected.

Elements can play with this. Negative values closest to 0 (the highest
values) take precedence.
  • Loading branch information
amolenaar committed Mar 18, 2022
1 parent 509dbb9 commit de8fa0f
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 11 deletions.
46 changes: 39 additions & 7 deletions gaphas/handlemove.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from functools import singledispatch
from operator import itemgetter
from typing import Iterable, Optional, Sequence

from gi.repository import Gdk, Gtk
Expand Down Expand Up @@ -153,13 +154,12 @@ def reset_cursor(self) -> None:
) if Gtk.get_major_version() == 3 else self.view.set_cursor(self.cursor)


# Maybe make this an iterator? so extra checks can be done on the item
def item_at_point(
def item_distance(
view: GtkView,
pos: Pos,
distance: float = 0.5,
exclude: Sequence[Item] = (),
) -> Iterable[Item]:
) -> Iterable[tuple[float, Item]]:
"""Return the topmost item located at ``pos`` (x, y).
Parameters:
Expand All @@ -176,9 +176,41 @@ def item_at_point(

v2i = view.get_matrix_v2i(item)
ix, iy = v2i.transform_point(vx, vy)
item_distance = item.point(ix, iy)
if item_distance is None:
d = item.point(ix, iy)
if d is None:
log.warning("Item distance is None for %s", item)
continue
if item_distance < distance:
yield item
if d < distance:
yield d, item


def order_items(distance_items, key=itemgetter(0)):
inside = []
outside = []
for e in distance_items:
if key(e) > 0:
outside.append(e)
else:
inside.append(e)

inside.sort(key=key, reverse=True)
outside.sort(key=key)
return inside + outside


def item_at_point(
view: GtkView,
pos: Pos,
distance: float = 0.5,
exclude: Sequence[Item] = (),
) -> Iterable[Item]:
"""Return the topmost item located at ``pos`` (x, y).
Parameters:
- view: a view
- pos: Position, a tuple ``(x, y)`` in view coordinates
- selected: if False returns first non-selected item
"""
return (
item for _d, item in order_items(item_distance(view, pos, distance, exclude))
)
4 changes: 2 additions & 2 deletions gaphas/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from cairo import Context as CairoContext

from gaphas.constraint import Constraint, EqualsConstraint, constraint
from gaphas.geometry import distance_line_point, distance_rectangle_point
from gaphas.geometry import distance_line_point, distance_rectangle_border_point
from gaphas.handle import Handle
from gaphas.matrix import Matrix
from gaphas.port import LinePort, Port
Expand Down Expand Up @@ -206,7 +206,7 @@ def point(self, x: float, y: float) -> float:
h = self._handles
x0, y0 = h[NW].pos
x1, y1 = h[SE].pos
return distance_rectangle_point((x0, y0, x1 - x0, y1 - y0), (x, y))
return distance_rectangle_border_point((x0, y0, x1 - x0, y1 - y0), (x, y))

def draw(self, context: DrawContext) -> None:
pass
Expand Down
4 changes: 2 additions & 2 deletions tests/test_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ def test_resize_by_dragging_se_handle(canvas, box, count):
def test_point(box):
box.handles()[SE].pos = (100, 100)

assert box.point(50, 50) == 0
assert box.point(50, 50) == -50


def test_point_with_moved_nw_handle(box):
box.handles()[NW].pos = (-100, -100)

assert box.point(-50, -50) == 0
assert box.point(-50, -50) == -50


def test_point_outside_box(box):
Expand Down
25 changes: 25 additions & 0 deletions tests/test_tool_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from gi.repository import Gtk

from gaphas.handlemove import order_items
from gaphas.tool.itemtool import (
DragState,
handle_at_point,
Expand Down Expand Up @@ -85,6 +86,30 @@ def test_get_unselected_item_at_point(view, box):
assert next(item_at_point(view, (10, 10), exclude=(box,)), None) is None # type: ignore[call-overload]


def test_get_item_at_point_overlayed_by_bigger_item(view, canvas, connections):
"""Hover tool only reacts on motion-notify events."""
below = Box(connections)
canvas.add(below)
above = Box(connections)
canvas.add(above)

below.width = 20
below.height = 20
above.matrix.translate(-40, -40)
above.width = 100
above.height = 100
view.request_update((below, above))

assert next(item_at_point(view, (10, 10)), None) is below # type: ignore[call-overload]
assert next(item_at_point(view, (-1, -1)), None) is above # type: ignore[call-overload]


def test_order_by_distance():
m = [(0, ""), (10, ""), (-1, ""), (-3, ""), (5, ""), (4, "")]

assert [e[0] for e in order_items(m)] == [0, -1, -3, 4, 5, 10]


def test_get_handle_at_point(view, canvas, connections):
box = Box(connections)
box.min_width = 20
Expand Down

0 comments on commit de8fa0f

Please sign in to comment.