Source code for secv_guis.objects

# -*- coding:utf-8 -*-


"""
Object composite commands are intended for composite objects (like e.g.
a train of points). The main difference with the regular commands is that
they implement a ``state`` method (which e.g. for a train of points returns
a list of (x, y) tuples), and a ``clear`` method, which allows to 'remove'
the object without having to roll back or break the undo queue.
"""


from PySide2 import QtCore, QtGui
from .commands import CompositeCommand, UndoableLambda


# #############################################################################
# ## HELPERS
# #############################################################################


# #############################################################################
# ## OBJECT COMPOSITE COMMANDS
# #############################################################################
[docs]class PointList(CompositeCommand): """ This class provides functionality to add circles to a scene (optionally connected by lines), and to return their centers as a list of ``(x, y)`` positions. It also """ COMMAND_NAME = "Draw point list" def __init__(self, scene, diameter, fill_rgba=(100, 100, 100, 100), contour_rgba=(0, 0, 0, 255), draw_lines_between_dots=False, comp_mode=QtGui.QPainter.CompositionMode_SourceOver, parent=None): """ :scene: A pointer to the ``QGraphicsScene`` to add points to. :fill_rgba: inner circle (and optionally line) color. :contour_rgba: circle contour color :draw_lines_between_dots: If true, connect the dots :comp_mode: ``SourceOver`` means alphas get added. Check Qt composition modes. """ super().__init__(parent) # self.scene = scene self.diameter = diameter self.comp_mode = comp_mode # self.fill_rgba = fill_rgba self.contour_rgba = contour_rgba fill_color = QtGui.QColor(*fill_rgba) contour_color = QtGui.QColor(*contour_rgba) self.dot_brush = QtGui.QBrush(fill_color, bs=QtCore.Qt.SolidPattern) self.dot_pen = QtGui.QPen(contour_color) # "all points" can only grow, it will remember even if we undo # "points" will show the "active", not deleted points only self._all_points = [] self.points = [] # self.with_lines = draw_lines_between_dots r, g, b, a = fill_rgba line_color = QtGui.QColor(r, g, b, a // 2) self.line_pen = QtGui.QPen(line_color) self._lines = {}
[docs] def state(self): """ :returns: A list in the form ``[(x1, y1), ...]`` with the center of the currently active points. """ centers = [elt.rect().center().toTuple() for elt in self.points] return centers
[docs] def clear(self): """ Removes all the active points from the datastructure and the scene. """ while self.points: pmi = self.points.pop() self.scene.removeItem(pmi) try: line = self._lines[pmi] self.scene.removeItem(line) except KeyError: pass
[docs] def action(self, x, y, undo_stack=None): """ Add a new point at given position. :param undo_stack: If given, this action is added to the undo stack. """ d = self.diameter radius = float(d) / 2 rect = QtCore.QRect(*(x - radius, y - radius, d, d)) pmi = self.scene.addEllipse(rect, pen=self.dot_pen, brush=self.dot_brush) # self._all_points.append(pmi) self.points.append(pmi) # if self.with_lines: centers = self.state() if len(centers) >= 2: x1, y1 = centers[-2] x2, y2 = centers[-1] line_pmi = self.scene.addLine(x1, y1, x2, y2, pen=self.line_pen) self._lines[pmi] = line_pmi # if undo_stack is not None: # deep copy is needed pmis_before = list(self.points[:-1]) pmis_after = list(self.points) # cmd = UndoableLambda( "Draw point", lambda: self._set_active_pmis(pmis_before), lambda: self._set_active_pmis(pmis_after)) undo_stack.push(cmd)
def _set_active_pmis(self, pmis): """ Given a collection of ``pmi`` s that exist in ``self._all_points``, set those, and only those, as active. """ assert all([p in self._all_points for p in pmis]), \ "All pmis must preexist!" self.clear() for pmi in pmis: self.scene.addItem(pmi) self.points.append(pmi) try: line_pmi = self._lines[pmi] self.scene.addItem(line_pmi) except KeyError: pass
[docs] def redo(self): """ Unused """ self._set_active_pmis(self._all_points)
[docs] def undo(self): """ Unused """ self._set_active_pmis([])
[docs] def finish(self, undo_stack=NotImplemented): """ Simply sets ``self.finished`` to true, because the undo stack gets the separate ``actions`` instead of the whole composite one. """ self.finished = True
# ############################################################################# # ## OBJECT CONTAINER # #############################################################################
[docs]class ObjectContainer: """ This class is a Mixin. When a ``QGraphicsScene`` inherits from it, it acquires functionality to add multiple composite objects from this module. """ def __init__(self): """ """ # objects come on the top of masks self.objects = {} self._current_object_action = None
[docs] def close_current_object_action(self, undo_stack=None): """ If there is an open object action, closes it and optionally adds it to the undo stack """ cmd = self._current_object_action if cmd is not None: cmd.finish(undo_stack) self._current_object_action = None
[docs] def object_action(self, obj_class, action_args, obj_instantiation_args, undo_stack=None): """ This function implements a protocol to add composite objects to the scene. 1. If a different composite action was running, it closes it and starts this one, adding it to ``self.objects``. 2. If no composite action was running, starts this one and adds it. 3. If ``obj_class`` is already running, does nothing here. In all cases, including if ``obj_class`` was already running, performs the action ``obj.action(*action_args)``. .. note:: The scene simply calls the object's action. The object is responsible for keeping track of the ``scene items`` it generates, and also removing/adding them to the scene when needed. :param obj_instantiation_args: If this action needs to be started, it will be called via ``cmd = action_class(*instantiation_args)`` :param action_args: The action will be called with this args. Usage example:: # adds a point to the existing cloud or starts one otherwise scene.object_action(ExamplePointCloud, [x, y], [cloud_color, points_size...]) # check the state of the last added point cloud (this one): scene.objects[ExamplePointCloud][-1].state() """ cmd = self._current_object_action # if changed to this action without releasing the prior one, release it action_changed = obj_class is not cmd.__class__ cmd_finished = cmd is not None and cmd.finished # if action_changed: self.close_current_object_action(undo_stack) cmd = self._current_object_action # this should be None # if no open action of this class, create if cmd is None or cmd_finished: cmd = obj_class(*obj_instantiation_args) self._current_object_action = cmd self.objects.setdefault(obj_class, []).append(cmd) cmd.action(*action_args)