1"""
2Undo/Redo Commands
3
4"""
5import typing
6from typing import Callable, Optional, Tuple, List, Any
7
8from AnyQt.QtWidgets import QUndoCommand
9
10if typing.TYPE_CHECKING:
11    from ..scheme import (
12        Scheme, SchemeNode, SchemeLink, BaseSchemeAnnotation,
13        SchemeTextAnnotation, SchemeArrowAnnotation
14    )
15    Pos = Tuple[float, float]
16    Rect = Tuple[float, float, float, float]
17    Line = Tuple[Pos, Pos]
18
19
20class UndoCommand(QUndoCommand):
21    """
22    For pickling
23    """
24    def __init__(self, text, parent=None):
25        QUndoCommand.__init__(self, text, parent)
26        self.__parent = parent
27        self.__initialized = True
28
29        # defined and initialized in __setstate__
30        # self.__child_states = {}
31        # self.__children = []
32
33    def __getstate__(self):
34        return {
35            **{k: v for k, v in self.__dict__.items()},
36            '_UndoCommand__initialized': False,
37            '_UndoCommand__text': self.text(),
38            '_UndoCommand__children':
39                [self.child(i) for i in range(self.childCount())]
40        }
41
42    def __setstate__(self, state):
43        if hasattr(self, '_UndoCommand__initialized') and \
44                self.__initialized:
45            return
46
47        text = state['_UndoCommand__text']
48        parent = state['_UndoCommand__parent']  # type: UndoCommand
49
50        if parent is not None and \
51                (not hasattr(parent, '_UndoCommand__initialized') or
52                 not parent.__initialized):
53            # will be initialized in parent's __setstate__
54            if not hasattr(parent, '_UndoCommand__child_states'):
55                setattr(parent, '_UndoCommand__child_states', {})
56            parent.__child_states[self] = state
57            return
58
59        # init must be called on unpickle-time to recreate Qt object
60        UndoCommand.__init__(self, text, parent)
61        if hasattr(self, '_UndoCommand__child_states'):
62            for child, s in self.__child_states.items():
63                child.__setstate__(s)
64
65        self.__dict__ = {k: v for k, v in state.items()}
66        self.__initialized = True
67
68    @staticmethod
69    def from_QUndoCommand(qc: QUndoCommand, parent=None):
70        if type(qc) == QUndoCommand:
71            qc.__class__ = UndoCommand
72
73        qc.__parent = parent
74
75        children = [qc.child(i) for i in range(qc.childCount())]
76        for child in children:
77            UndoCommand.from_QUndoCommand(child, parent=qc)
78
79        return qc
80
81
82class AddNodeCommand(UndoCommand):
83    def __init__(self, scheme, node, parent=None):
84        # type: (Scheme, SchemeNode, Optional[UndoCommand]) -> None
85        super().__init__("Add %s" % node.title, parent)
86        self.scheme = scheme
87        self.node = node
88
89    def redo(self):
90        self.scheme.add_node(self.node)
91
92    def undo(self):
93        self.scheme.remove_node(self.node)
94
95
96class RemoveNodeCommand(UndoCommand):
97    def __init__(self, scheme, node, parent=None):
98        # type: (Scheme, SchemeNode, Optional[UndoCommand]) -> None
99        super().__init__("Remove %s" % node.title, parent)
100        self.scheme = scheme
101        self.node = node
102        self._index = -1
103        links = scheme.input_links(self.node) + \
104                scheme.output_links(self.node)
105
106        for link in links:
107            RemoveLinkCommand(scheme, link, parent=self)
108
109    def redo(self):
110        # redo child commands
111        super().redo()
112        self._index = self.scheme.nodes.index(self.node)
113        self.scheme.remove_node(self.node)
114
115    def undo(self):
116        assert self._index != -1
117        self.scheme.insert_node(self._index, self.node)
118        # Undo child commands
119        super().undo()
120
121
122class AddLinkCommand(UndoCommand):
123    def __init__(self, scheme, link, parent=None):
124        # type: (Scheme, SchemeLink, Optional[UndoCommand]) -> None
125        super().__init__("Add link", parent)
126        self.scheme = scheme
127        self.link = link
128
129    def redo(self):
130        self.scheme.add_link(self.link)
131
132    def undo(self):
133        self.scheme.remove_link(self.link)
134
135
136class RemoveLinkCommand(UndoCommand):
137    def __init__(self, scheme, link, parent=None):
138        # type: (Scheme, SchemeLink, Optional[UndoCommand]) -> None
139        super().__init__("Remove link", parent)
140        self.scheme = scheme
141        self.link = link
142        self._index = -1
143
144    def redo(self):
145        self._index = self.scheme.links.index(self.link)
146        self.scheme.remove_link(self.link)
147
148    def undo(self):
149        assert self._index != -1
150        self.scheme.insert_link(self._index, self.link)
151        self._index = -1
152
153
154class InsertNodeCommand(UndoCommand):
155    def __init__(
156            self,
157            scheme,     # type: Scheme
158            new_node,   # type: SchemeNode
159            old_link,   # type: SchemeLink
160            new_links,  # type: Tuple[SchemeLink, SchemeLink]
161            parent=None # type: Optional[UndoCommand]
162    ):  # type: (...) -> None
163        super().__init__("Insert widget into link", parent)
164
165        AddNodeCommand(scheme, new_node, parent=self)
166        RemoveLinkCommand(scheme, old_link, parent=self)
167        for link in new_links:
168            AddLinkCommand(scheme, link, parent=self)
169
170
171class AddAnnotationCommand(UndoCommand):
172    def __init__(self, scheme, annotation, parent=None):
173        # type: (Scheme, BaseSchemeAnnotation, Optional[UndoCommand]) -> None
174        super().__init__("Add annotation", parent)
175        self.scheme = scheme
176        self.annotation = annotation
177
178    def redo(self):
179        self.scheme.add_annotation(self.annotation)
180
181    def undo(self):
182        self.scheme.remove_annotation(self.annotation)
183
184
185class RemoveAnnotationCommand(UndoCommand):
186    def __init__(self, scheme, annotation, parent=None):
187        # type: (Scheme, BaseSchemeAnnotation, Optional[UndoCommand]) -> None
188        super().__init__("Remove annotation", parent)
189        self.scheme = scheme
190        self.annotation = annotation
191        self._index = -1
192
193    def redo(self):
194        self._index = self.scheme.annotations.index(self.annotation)
195        self.scheme.remove_annotation(self.annotation)
196
197    def undo(self):
198        assert self._index != -1
199        self.scheme.insert_annotation(self._index, self.annotation)
200        self._index = -1
201
202
203class MoveNodeCommand(UndoCommand):
204    def __init__(self, scheme, node, old, new, parent=None):
205        # type: (Scheme, SchemeNode, Pos, Pos, Optional[UndoCommand]) -> None
206        super().__init__("Move", parent)
207        self.scheme = scheme
208        self.node = node
209        self.old = old
210        self.new = new
211
212    def redo(self):
213        self.node.position = self.new
214
215    def undo(self):
216        self.node.position = self.old
217
218
219class ResizeCommand(UndoCommand):
220    def __init__(self, scheme, item, new_geom, parent=None):
221        # type: (Scheme, SchemeTextAnnotation, Rect, Optional[UndoCommand]) -> None
222        super().__init__("Resize", parent)
223        self.scheme = scheme
224        self.item = item
225        self.new_geom = new_geom
226        self.old_geom = item.rect
227
228    def redo(self):
229        self.item.rect = self.new_geom
230
231    def undo(self):
232        self.item.rect = self.old_geom
233
234
235class ArrowChangeCommand(UndoCommand):
236    def __init__(self, scheme, item, new_line, parent=None):
237        # type: (Scheme, SchemeArrowAnnotation, Line, Optional[UndoCommand]) -> None
238        super().__init__("Move arrow", parent)
239        self.scheme = scheme
240        self.item = item
241        self.new_line = new_line
242        self.old_line = (item.start_pos, item.end_pos)
243
244    def redo(self):
245        self.item.set_line(*self.new_line)
246
247    def undo(self):
248        self.item.set_line(*self.old_line)
249
250
251class AnnotationGeometryChange(UndoCommand):
252    def __init__(
253            self,
254            scheme,  # type: Scheme
255            annotation,  # type: BaseSchemeAnnotation
256            old,  # type: Any
257            new,  # type: Any
258            parent=None  # type: Optional[UndoCommand]
259    ):  # type: (...) -> None
260        super().__init__("Change Annotation Geometry", parent)
261        self.scheme = scheme
262        self.annotation = annotation
263        self.old = old
264        self.new = new
265
266    def redo(self):
267        self.annotation.geometry = self.new  # type: ignore
268
269    def undo(self):
270        self.annotation.geometry = self.old  # type: ignore
271
272
273class RenameNodeCommand(UndoCommand):
274    def __init__(self, scheme, node, old_name, new_name, parent=None):
275        # type: (Scheme, SchemeNode, str, str, Optional[UndoCommand]) -> None
276        super().__init__("Rename", parent)
277        self.scheme = scheme
278        self.node = node
279        self.old_name = old_name
280        self.new_name = new_name
281
282    def redo(self):
283        self.node.set_title(self.new_name)
284
285    def undo(self):
286        self.node.set_title(self.old_name)
287
288
289class TextChangeCommand(UndoCommand):
290    def __init__(
291            self,
292            scheme,       # type: Scheme
293            annotation,   # type: SchemeTextAnnotation
294            old_content,  # type: str
295            old_content_type,  # type: str
296            new_content,  # type: str
297            new_content_type,  # type: str
298            parent=None   # type: Optional[UndoCommand]
299    ):  # type: (...) -> None
300        super().__init__("Change text", parent)
301        self.scheme = scheme
302        self.annotation = annotation
303        self.old_content = old_content
304        self.old_content_type = old_content_type
305        self.new_content = new_content
306        self.new_content_type = new_content_type
307
308    def redo(self):
309        self.annotation.set_content(self.new_content, self.new_content_type)
310
311    def undo(self):
312        self.annotation.set_content(self.old_content, self.old_content_type)
313
314
315class SetAttrCommand(UndoCommand):
316    def __init__(
317            self,
318            obj,         # type: Any
319            attrname,    # type: str
320            newvalue,    # type: Any
321            name=None,   # type: Optional[str]
322            parent=None  # type: Optional[UndoCommand]
323    ):  # type: (...) -> None
324        if name is None:
325            name = "Set %r" % attrname
326        super().__init__(name, parent)
327        self.obj = obj
328        self.attrname = attrname
329        self.newvalue = newvalue
330        self.oldvalue = getattr(obj, attrname)
331
332    def redo(self):
333        setattr(self.obj, self.attrname, self.newvalue)
334
335    def undo(self):
336        setattr(self.obj, self.attrname, self.oldvalue)
337
338
339class SetWindowGroupPresets(UndoCommand):
340    def __init__(
341            self,
342            scheme: 'Scheme',
343            presets: List['Scheme.WindowGroup'],
344            parent: Optional[UndoCommand] = None,
345            **kwargs
346    ) -> None:
347        text = kwargs.pop("text", "Set Window Presets")
348        super().__init__(text, parent, **kwargs)
349        self.scheme = scheme
350        self.presets = presets
351        self.__undo_presets = None
352
353    def redo(self):
354        presets = self.scheme.window_group_presets()
355        self.scheme.set_window_group_presets(self.presets)
356        self.__undo_presets = presets
357
358    def undo(self):
359        self.scheme.set_window_group_presets(self.__undo_presets)
360        self.__undo_presets = None
361
362
363class SimpleUndoCommand(UndoCommand):
364    """
365    Simple undo/redo command specified by callable function pair.
366    Parameters
367    ----------
368    redo: Callable[[], None]
369        A function expressing a redo action.
370    undo : Callable[[], None]
371        A function expressing a undo action.
372    text : str
373        The command's text (see `UndoCommand.setText`)
374    parent : Optional[UndoCommand]
375    """
376
377    def __init__(
378            self,
379            redo,  # type: Callable[[], None]
380            undo,  # type: Callable[[], None]
381            text,  # type: str
382            parent=None  # type: Optional[UndoCommand]
383    ):  # type: (...) -> None
384        super().__init__(text, parent)
385        self._redo = redo
386        self._undo = undo
387
388    def undo(self):
389        # type: () -> None
390        """Reimplemented."""
391        self._undo()
392
393    def redo(self):
394        # type: () -> None
395        """Reimplemented."""
396        self._redo()
397