Turn Item into a protocol

Now we do not need to inherit from Item explicitly.
This commit is contained in:
Arjan Molenaar 2020-11-08 22:52:11 +01:00
parent 4b237b8ecd
commit 3dd5d02721
9 changed files with 140 additions and 114 deletions

View File

@ -23,9 +23,9 @@ The observer simply dispatches the function called (as ``<function ..>``, not as
Let's start with creating a Canvas instance and some items: Let's start with creating a Canvas instance and some items:
>>> from gaphas.canvas import Canvas >>> from gaphas.canvas import Canvas
>>> from gaphas.item import Item >>> from examples.exampleitems import Circle
>>> canvas = Canvas() >>> canvas = Canvas()
>>> item1, item2 = Item(), Item() >>> item1, item2 = Circle(), Circle()
For this demonstration let's use the Canvas class (which contains an add/remove For this demonstration let's use the Canvas class (which contains an add/remove
method pair). method pair).
@ -36,11 +36,11 @@ It works (see how the add method automatically schedules the item for update):
... print('event handled', event) ... print('event handled', event)
>>> state.observers.add(handler) >>> state.observers.add(handler)
>>> canvas.add(item1) # doctest: +ELLIPSIS >>> canvas.add(item1) # doctest: +ELLIPSIS
event handled (<function Canvas.add at ...>, (<gaphas.canvas.Canvas object at ...>, <gaphas.item.Item object at ...>), {}) event handled (<function Canvas.add at ...>, (<gaphas.canvas.Canvas object at ...>, <examples.exampleitems.Circle object at ...>), {})
>>> canvas.add(item2, parent=item1) # doctest: +ELLIPSIS >>> canvas.add(item2, parent=item1) # doctest: +ELLIPSIS
event handled (<function Canvas.add at ...>, (<gaphas.canvas.Canvas object at ...>, <gaphas.item.Item object at ...>), {'parent': <gaphas.item.Item object at ...>}) event handled (<function Canvas.add at ...>, (<gaphas.canvas.Canvas object at ...>, <examples.exampleitems.Circle object at ...>), {'parent': <examples.exampleitems.Circle object at ...>})
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS >>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>] [<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
Note that the handler is invoked before the actual call is made. This is Note that the handler is invoked before the actual call is made. This is
important if you want to store the (old) state for an undo mechanism. important if you want to store the (old) state for an undo mechanism.
@ -52,8 +52,8 @@ Therefore some careful crafting of methods may be necessary in order to get the
right effect (items should be removed in the right order, child first): right effect (items should be removed in the right order, child first):
>>> canvas.remove(item1) # doctest: +ELLIPSIS >>> canvas.remove(item1) # doctest: +ELLIPSIS
event handled (<function Canvas._remove at ...>, (<gaphas.canvas.Canvas object at 0x...>, <gaphas.item.Item object at 0x...>), {}) event handled (<function Canvas._remove at ...>, (<gaphas.canvas.Canvas object at 0x...>, <examples.exampleitems.Circle object at 0x...>), {})
event handled (<function Canvas._remove at ...>, (<gaphas.canvas.Canvas object at 0x...>, <gaphas.item.Item object at 0x...>), {}) event handled (<function Canvas._remove at ...>, (<gaphas.canvas.Canvas object at 0x...>, <examples.exampleitems.Circle object at 0x...>), {})
>>> list(canvas.get_all_items()) >>> list(canvas.get_all_items())
[] []
@ -126,10 +126,11 @@ Handlers for the reverse events should be registered on the subscribers list:
After that, signals can be received of undoable (reverse-)events: After that, signals can be received of undoable (reverse-)events:
>>> canvas.add(Item()) # doctest: +ELLIPSIS >>> canvas.add(Circle()) # doctest: +ELLIPSIS
event handler (<function Canvas._remove at ...>, {'self': <gaphas.canvas.Canvas object at 0x...>, 'item': <gaphas.item.Item object at 0x...>}) event handler (<function Handle._set_movable at ...>, {'self': <Handle object on (Variable(0, 20), Variable(0, 20))>, 'movable': True})
event handler (<function Canvas._remove at ...>, {'self': <gaphas.canvas.Canvas object at 0x...>, 'item': <examples.exampleitems.Circle object at 0x...>})
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS >>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>] [<examples.exampleitems.Circle object at 0x...>]
As you can see this event is constructed of only two parameters: the function As you can see this event is constructed of only two parameters: the function
that does the inverse operation of ``add()`` and the arguments that should be that does the inverse operation of ``add()`` and the arguments that should be
@ -139,7 +140,7 @@ The inverse operation is easiest performed by the function ``saveapply()``. Of
course an inverse operation is emitting a change event too: course an inverse operation is emitting a change event too:
>>> state.saveapply(*events.pop()) # doctest: +ELLIPSIS >>> state.saveapply(*events.pop()) # doctest: +ELLIPSIS
event handler (<function Canvas.add at 0x...>, {'self': <gaphas.canvas.Canvas object at 0x...>, 'item': <gaphas.item.Item object at 0x...>, 'parent': None, 'index': 0}) event handler (<function Canvas.add at 0x...>, {'self': <gaphas.canvas.Canvas object at 0x...>, 'item': <examples.exampleitems.Circle object at 0x...>, 'parent': None, 'index': 0})
>>> list(canvas.get_all_items()) >>> list(canvas.get_all_items())
[] []

View File

@ -111,25 +111,26 @@ Again, rotate does not result in an exact match, but it's close enough.
canvas.py: Canvas canvas.py: Canvas
----------------- -----------------
>>> from gaphas import Canvas, Item >>> from gaphas import Canvas
>>> from examples.exampleitems import Circle
>>> canvas = Canvas() >>> canvas = Canvas()
>>> list(canvas.get_all_items()) >>> list(canvas.get_all_items())
[] []
>>> item = Item() >>> item = Circle()
>>> canvas.add(item) >>> canvas.add(item)
The ``request_update()`` method is observed: The ``request_update()`` method is observed:
>>> len(undo_list) >>> len(undo_list)
1 2
>>> canvas.request_update(item) >>> canvas.request_update(item)
>>> len(undo_list) >>> len(undo_list)
2 3
On the canvas only ``add()`` and ``remove()`` are monitored: On the canvas only ``add()`` and ``remove()`` are monitored:
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS >>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>] [<examples.exampleitems.Circle object at 0x...>]
>>> undo() >>> undo()
>>> list(canvas.get_all_items()) >>> list(canvas.get_all_items())
[] []
@ -140,7 +141,7 @@ On the canvas only ``add()`` and ``remove()`` are monitored:
[] []
>>> undo() >>> undo()
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS >>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>] [<examples.exampleitems.Circle object at 0x...>]
>>> undo_list >>> undo_list
[] []
@ -149,15 +150,15 @@ Parent-child relationships are restored as well:
TODO! TODO!
>>> child = Item() >>> child = Circle()
>>> canvas.add(child, parent=item) >>> canvas.add(child, parent=item)
>>> canvas.get_parent(child) is item >>> canvas.get_parent(child) is item
True True
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS >>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>] [<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
>>> undo() >>> undo()
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS >>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>] [<examples.exampleitems.Circle object at 0x...>]
>>> child in canvas.get_all_items() >>> child in canvas.get_all_items()
False False
@ -168,32 +169,35 @@ Now redo the previous undo action:
>>> canvas.get_parent(child) is item >>> canvas.get_parent(child) is item
True True
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS >>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>] [<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
Remove also works when items are removed recursively (an item and it's Remove also works when items are removed recursively (an item and it's
children): children):
>>> child = Item() >>> child = Circle()
>>> canvas.add(child, parent=item) >>> canvas.add(child, parent=item)
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS >>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>] [<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
>>> del undo_list[:] >>> del undo_list[:]
>>> canvas.remove(item) >>> canvas.remove(item)
>>> list(canvas.get_all_items()) >>> list(canvas.get_all_items())
[] []
>>> undo() >>> undo()
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS >>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>] [<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
>>> canvas.get_children(item) # doctest: +ELLIPSIS >>> canvas.get_children(item) # doctest: +ELLIPSIS
[<gaphas.item.Item object at 0x...>] [<examples.exampleitems.Circle object at 0x...>]
As well as the reparent() method: As well as the reparent() method:
>>> canvas = Canvas() >>> canvas = Canvas()
>>> class NameItem(Item): >>> class NameItem:
... def __init__(self, name): ... def __init__(self, name):
... super(NameItem, self).__init__() ... super(NameItem, self).__init__()
... self.name = name ... self.name = name
... def handles(self): return []
... def ports(self): return []
... def point(self, x, y): return 0
... def __repr__(self): ... def __repr__(self):
... return '<%s>' % self.name ... return '<%s>' % self.name
>>> ni1 = NameItem('a') >>> ni1 = NameItem('a')
@ -347,9 +351,9 @@ Also creation and removal of connected lines is recorded and can be undone:
... def real_disconnect(): ... def real_disconnect():
... pass ... pass
... canvas.connections.connect_item(hitem, handle, item, port=None, constraint=None, callback=real_disconnect) ... canvas.connections.connect_item(hitem, handle, item, port=None, constraint=None, callback=real_disconnect)
>>> b0 = Item() >>> b0 = Circle()
>>> canvas.add(b0) >>> canvas.add(b0)
>>> b1 = Item() >>> b1 = Circle()
>>> canvas.add(b1) >>> canvas.add(b1)
>>> l = Line(Connections()) >>> l = Line(Connections())
>>> canvas.add(l) >>> canvas.add(l)

View File

@ -3,7 +3,7 @@
These items are used in various tests. These items are used in various tests.
""" """
from gaphas.connector import Handle from gaphas.connector import Handle
from gaphas.item import NW, Element, Item from gaphas.item import NW, Element, Matrices, Updateable
from gaphas.util import path_ellipse, text_align, text_multiline from gaphas.util import path_ellipse, text_align, text_multiline
@ -29,7 +29,7 @@ class Box(Element):
c.stroke() c.stroke()
class Text(Item): class Text(Matrices, Updateable):
"""Simple item showing some text on the canvas.""" """Simple item showing some text on the canvas."""
def __init__(self, text=None, plain=False, multiline=False, align_x=1, align_y=-1): def __init__(self, text=None, plain=False, multiline=False, align_x=1, align_y=-1):
@ -40,6 +40,15 @@ class Text(Item):
self.align_x = align_x self.align_x = align_x
self.align_y = align_y self.align_y = align_y
def handles(self):
return []
def ports(self):
return []
def point(self, x, y):
return 0
def draw(self, context): def draw(self, context):
cr = context.cairo cr = context.cairo
if self.multiline: if self.multiline:
@ -49,14 +58,11 @@ class Text(Item):
else: else:
text_align(cr, 0, 0, self.text, self.align_x, self.align_y) text_align(cr, 0, 0, self.text, self.align_x, self.align_y)
def point(self, x, y):
return 0
class Circle(Matrices, Updateable):
class Circle(Item):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._handles.extend((Handle(), Handle())) self._handles = [Handle(), Handle()]
h1, h2 = self._handles h1, h2 = self._handles
h1.movable = False h1.movable = False
@ -72,6 +78,12 @@ class Circle(Item):
radius = property(_get_radius, _set_radius) radius = property(_get_radius, _set_radius)
def handles(self):
return self._handles
def ports(self):
return []
def point(self, x, y): def point(self, x, y):
h1, _ = self._handles h1, _ = self._handles
p1 = h1.pos p1 = h1.pos

View File

@ -1,6 +1,6 @@
"""Basic items.""" """Basic items."""
from math import atan2 from math import atan2
from typing import Sequence from typing import Protocol, Sequence
from gaphas.canvas import Context from gaphas.canvas import Context
from gaphas.connector import Handle, LinePort, Port from gaphas.connector import Handle, LinePort, Port
@ -16,31 +16,48 @@ from gaphas.state import (
) )
class Item(Protocol):
@property
def matrix(self) -> Matrix:
...
@property
def matrix_i2c(self) -> Matrix:
...
def handles(self) -> Sequence[Handle]:
"""Return a list of handles owned by the item."""
def ports(self) -> Sequence[Port]:
"""Return list of ports."""
def point(self, x: float, y: float) -> float:
"""Get the distance from a point (``x``, ``y``) to the item.
``x`` and ``y`` are in item coordinates.
"""
def draw(self, context: Context):
"""Render the item to a canvas view. Context contains the following
attributes:
- cairo: the Cairo Context use this one to draw
- selected, focused, hovered, dropzone: view state of items
(True/False)
"""
def matrix_i2i(from_item, to_item): def matrix_i2i(from_item, to_item):
i2c = from_item.matrix_i2c i2c = from_item.matrix_i2c
c2i = to_item.matrix_i2c.inverse() c2i = to_item.matrix_i2c.inverse()
return i2c.multiply(c2i) return i2c.multiply(c2i)
class Item: class Matrices:
"""Base class (or interface) for items on a canvas.Canvas.
Attributes:
- matrix: item's transformation matrix
Private:
- _handles: list of handles owned by an item
- _ports: list of ports, connectable areas of an item
"""
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) # type: ignore[call-arg] super().__init__(**kwargs) # type: ignore[call-arg]
self._matrix = Matrix() self._matrix = Matrix()
self._matrix_i2c = Matrix() self._matrix_i2c = Matrix()
self._handles = []
self._ports = []
@property @property
def matrix(self) -> Matrix: def matrix(self) -> Matrix:
@ -50,6 +67,8 @@ class Item:
def matrix_i2c(self) -> Matrix: def matrix_i2c(self) -> Matrix:
return self._matrix_i2c return self._matrix_i2c
class Updateable:
def pre_update(self, context: Context): def pre_update(self, context: Context):
"""Perform any changes before item update here, for example: """Perform any changes before item update here, for example:
@ -76,36 +95,11 @@ class Item:
""" """
pass pass
def draw(self, context: Context):
"""Render the item to a canvas view. Context contains the following
attributes:
- cairo: the Cairo Context use this one to draw
- selected, focused, hovered, dropzone: view state of items
(True/False)
"""
pass
def handles(self) -> Sequence[Handle]:
"""Return a list of handles owned by the item."""
return self._handles
def ports(self) -> Sequence[Port]:
"""Return list of ports."""
return self._ports
def point(self, x: float, y: float):
"""Get the distance from a point (``x``, ``y``) to the item.
``x`` and ``y`` are in item coordinates.
"""
pass
[NW, NE, SE, SW] = list(range(4)) [NW, NE, SE, SW] = list(range(4))
class Element(Item): class Element(Matrices, Updateable):
"""An Element has 4 handles (for a start):: """An Element has 4 handles (for a start)::
NW +---+ NE | | SW +---+ SE NW +---+ NE | | SW +---+ SE
@ -199,6 +193,14 @@ class Element(Item):
height = property(_get_height, _set_height) height = property(_get_height, _set_height)
def handles(self) -> Sequence[Handle]:
"""Return a list of handles owned by the item."""
return self._handles
def ports(self) -> Sequence[Port]:
"""Return list of ports."""
return self._ports
def point(self, x, y): def point(self, x, y):
"""Distance from the point (x, y) to the item. """Distance from the point (x, y) to the item.
@ -212,6 +214,9 @@ class Element(Item):
list(map(float, (pnw.x, pnw.y, pse.x, pse.y))), (x, y) list(map(float, (pnw.x, pnw.y, pse.x, pse.y))), (x, y)
) )
def draw(self, context: Context):
pass
def create_orthogonal_constraints(handles, horizontal): def create_orthogonal_constraints(handles, horizontal):
rest = 1 if horizontal else 0 rest = 1 if horizontal else 0
@ -224,7 +229,7 @@ def create_orthogonal_constraints(handles, horizontal):
yield EqualsConstraint(a=p0.y, b=p1.y) yield EqualsConstraint(a=p0.y, b=p1.y)
class Line(Item): class Line(Matrices, Updateable):
"""A Line item. """A Line item.
Properties: Properties:
@ -411,6 +416,14 @@ class Line(Item):
p1, p0 = h1.pos, h0.pos p1, p0 = h1.pos, h0.pos
self._tail_angle = atan2(p1.y - p0.y, p1.x - p0.x) # type: ignore[assignment] self._tail_angle = atan2(p1.y - p0.y, p1.x - p0.x) # type: ignore[assignment]
def handles(self) -> Sequence[Handle]:
"""Return a list of handles owned by the item."""
return self._handles
def ports(self) -> Sequence[Port]:
"""Return list of ports."""
return self._ports
def point(self, x, y): def point(self, x, y):
""" """
>>> a = Line() >>> a = Line()

View File

@ -102,7 +102,7 @@ class BoundingBoxPainter:
): ):
self.item_painter = item_painter self.item_painter = item_painter
def paint_item(self, item, cairo): def paint_item(self, item: Item, cairo):
cairo = CairoBoundingBoxContext(cairo) cairo = CairoBoundingBoxContext(cairo)
self.item_painter.paint_item(item, cairo) self.item_painter.paint_item(item, cairo)
# Bounding box is in view (cairo root) coordinates # Bounding box is in view (cairo root) coordinates

View File

@ -86,7 +86,7 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable):
), ),
} }
def __init__(self, canvas: Optional[Model[Item]] = None): def __init__(self, canvas: Optional[Model] = None):
Gtk.DrawingArea.__init__(self) Gtk.DrawingArea.__init__(self)
self._dirty_items: Set[Item] = set() self._dirty_items: Set[Item] = set()
@ -117,7 +117,7 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable):
self._qtree: Quadtree[Item, Tuple[float, float, float, float]] = Quadtree() self._qtree: Quadtree[Item, Tuple[float, float, float, float]] = Quadtree()
self._canvas: Optional[Model[Item]] = None self._canvas: Optional[Model] = None
if canvas: if canvas:
self._set_canvas(canvas) self._set_canvas(canvas)
@ -152,7 +152,7 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable):
m.invert() m.invert()
return m return m
def _set_canvas(self, canvas: Optional[Model[Item]]) -> None: def _set_canvas(self, canvas: Optional[Model]) -> None:
""" """
Use view.canvas = my_canvas to set the canvas to be rendered Use view.canvas = my_canvas to set the canvas to be rendered
in the view. in the view.

View File

@ -1,45 +1,44 @@
from __future__ import annotations from __future__ import annotations
from typing import Iterable, Optional, Sequence, TypeVar from typing import Collection, Iterable, Optional
from typing_extensions import Protocol, runtime_checkable from typing_extensions import Protocol, runtime_checkable
T = TypeVar("T") from gaphas.item import Item
T_ct = TypeVar("T_ct", contravariant=True)
@runtime_checkable @runtime_checkable
class Model(Protocol[T]): class View(Protocol):
def get_all_items(self) -> Iterable[T]: def request_update(
self,
items: Collection[Item],
matrix_only_items: Collection[Item],
removed_items: Collection[Item],
) -> None:
... ...
def get_parent(self, item: T) -> Optional[T]:
@runtime_checkable
class Model(Protocol):
def get_all_items(self) -> Iterable[Item]:
... ...
def get_children(self, item: T) -> Iterable[T]: def get_parent(self, item: Item) -> Optional[Item]:
... ...
def sort(self, items: Sequence[T]) -> Iterable[T]: def get_children(self, item: Item) -> Iterable[Item]:
...
def sort(self, items: Collection[Item]) -> Iterable[Item]:
... ...
def update_now( def update_now(
self, dirty_items: Sequence[T], dirty_matrix_items: Sequence[T] self, dirty_items: Collection[Item], dirty_matrix_items: Collection[Item]
) -> None: ) -> None:
... ...
def register_view(self, view: View[T]) -> None: def register_view(self, view: View) -> None:
... ...
def unregister_view(self, view: View[T]) -> None: def unregister_view(self, view: View) -> None:
...
@runtime_checkable
class View(Protocol[T_ct]):
def request_update(
self,
items: Sequence[T_ct],
matrix_only_items: Sequence[T_ct],
removed_items: Sequence[T_ct],
) -> None:
... ...

View File

@ -3,12 +3,12 @@
import pytest import pytest
from gaphas.aspect import InMotion, Selection from gaphas.aspect import InMotion, Selection
from gaphas.item import Item from gaphas.item import Element
@pytest.fixture() @pytest.fixture()
def item(): def item(connections):
return Item() return Element(connections)
def test_selection_select(canvas, view, item): def test_selection_select(canvas, view, item):

View File

@ -1,7 +1,7 @@
"""Test segment aspects for items.""" """Test segment aspects for items."""
import pytest import pytest
from gaphas.item import Item from gaphas.item import Element
from gaphas.segment import HandleFinder, Line, Segment, SegmentHandleFinder from gaphas.segment import HandleFinder, Line, Segment, SegmentHandleFinder
from gaphas.tool import ConnectHandleTool from gaphas.tool import ConnectHandleTool
@ -11,15 +11,13 @@ def tool(view):
return ConnectHandleTool(view) return ConnectHandleTool(view)
def test_segment_fails_for_item(canvas): def test_segment_fails_for_element(canvas, connections):
"""Test if Segment aspect can be applied to Item.""" item = Element(connections)
item = Item()
with pytest.raises(TypeError): with pytest.raises(TypeError):
Segment(canvas, item) Segment(canvas, item)
def test_segment(canvas, connections): def test_add_segment_to_line(canvas, connections):
"""Test add a new segment to a line."""
line = Line(connections) line = Line(connections)
canvas.add(line) canvas.add(line)
segment = Segment(line, canvas) segment = Segment(line, canvas)
@ -32,7 +30,6 @@ def test_segment(canvas, connections):
def test_split_single(canvas, line): def test_split_single(canvas, line):
"""Test single line splitting."""
# Start with 2 handles & 1 port, after split: expect 3 handles & 2 ports # Start with 2 handles & 1 port, after split: expect 3 handles & 2 ports
assert len(line.handles()) == 2 assert len(line.handles()) == 2
assert len(line.ports()) == 1 assert len(line.ports()) == 1