diff --git a/gaphas/connections.py b/gaphas/connections.py index d26e1cf..de23976 100644 --- a/gaphas/connections.py +++ b/gaphas/connections.py @@ -1,6 +1,8 @@ """This module contains a connections manager.""" -from typing import Callable, Iterator, NamedTuple, Optional +from __future__ import annotations + +from typing import Callable, Iterator, NamedTuple, Optional, Set from gaphas import table from gaphas.connector import Handle, Port @@ -40,8 +42,27 @@ class Connections: def __init__(self, solver: Optional[Solver] = None) -> None: self._solver = solver or Solver() self._connections: table.Table[Connection] = table.Table( - Connection, tuple(range(4)) + Connection, tuple(range(5)) ) + self._handlers: Set[Callable[[Connection], None]] = set() + + self._solver.add_handler(self._on_constraint_solved) + + def add_handler(self, handler): + """Add a callback handler. + + Handlers are triggered when a constraint has been solved. + """ + self._handlers.add(handler) + + def remove_handler(self, handler): + """Remove a previously assigned handler.""" + self._handlers.discard(handler) + + def _on_constraint_solved(self, constraint): + for cinfo in self._connections.query(constraint=constraint): + for handler in self._handlers: + handler(cinfo) @property def solver(self) -> Solver: @@ -107,7 +128,6 @@ class Connections: If handle is not None, only the connection for that handle is disconnected. """ - # disconnect on canvas level for cinfo in list(self._connections.query(item=item, handle=handle)): self._disconnect_item(*cinfo) diff --git a/gaphas/solver/solver.py b/gaphas/solver/solver.py index 937c7f9..0256677 100644 --- a/gaphas/solver/solver.py +++ b/gaphas/solver/solver.py @@ -32,7 +32,7 @@ every constraint is being asked to solve itself (`constraint.Constraint.solve_for()` method) changing appropriate variables to make the constraint valid again. """ -from typing import Collection, List, Set +from typing import Callable, Collection, List, Set from gaphas.solver.constraint import Constraint from gaphas.state import observed, reversible_pair @@ -49,6 +49,19 @@ class Solver: self._constraints: Set[Constraint] = set() self._marked_cons: List[Constraint] = [] self._solving = False + self._handlers: Set[Callable[[Constraint], None]] = set() + + def add_handler(self, handler): + """Add a callback handler, triggered when a constraint is resolved.""" + self._handlers.add(handler) + + def remove_handler(self, handler): + """Remove a previously assigned handler.""" + self._handlers.discard(handler) + + def _notify(self, constraint): + for handler in self._handlers: + handler(constraint) @property def constraints(self) -> Collection[Constraint]: @@ -154,6 +167,7 @@ class Solver: 10.0 """ marked_cons = self._marked_cons + notify = self._notify try: self._solving = True @@ -164,6 +178,7 @@ class Solver: while n < len(marked_cons): c = marked_cons[n] c.solve() + notify(c) n += 1 self._marked_cons = [] diff --git a/tests/test_connections.py b/tests/test_connections.py index 617cb33..103af3f 100644 --- a/tests/test_connections.py +++ b/tests/test_connections.py @@ -66,3 +66,38 @@ def test_remove_item_constraint_when_item_is_disconnected(connections): connections.disconnect_item(i) assert list(connections.get_connections(item=i)) == [] + + +def test_notify_on_constraint_solved(connections): + events = [] + + def on_notify(cinfo): + events.append(cinfo) + + i = item.Line(connections) + c = EqualsConstraint(i.handles()[0].pos.x, i.handles()[0].pos.x) + connections.add_constraint(i, c) + + connections.add_handler(on_notify) + connections.solve() + + assert events + assert events[0].constraint is c + + +def test_connection_remove_handler(connections): + events = [] + + def on_notify(cinfo): + events.append(cinfo) + + i = item.Line(connections) + c = EqualsConstraint(i.handles()[0].pos.x, i.handles()[0].pos.x) + connections.add_constraint(i, c) + + connections.add_handler(on_notify) + connections.remove_handler(on_notify) + connections.remove_handler(on_notify) + connections.solve() + + assert not events