source: orange/Orange/OrangeCanvas/document/interactions.py @ 11202:dba4e6f2678e

Revision 11202:dba4e6f2678e, 33.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Added annotation font and color serialization.

Line 
1"""
2User interaction handlers for CanvasScene.
3
4"""
5import logging
6
7from PyQt4.QtGui import (
8    QApplication, QGraphicsRectItem, QPen, QBrush, QColor, QFontMetrics
9)
10
11from PyQt4.QtCore import Qt, QObject, QSizeF, QPointF, QRectF, QLineF
12from PyQt4.QtCore import pyqtSignal as Signal
13
14from ..registry.qt import QtWidgetRegistry
15from .. import scheme
16from ..canvas import items
17from ..canvas.items import controlpoints
18from . import commands
19
20log = logging.getLogger(__name__)
21
22
23class UserInteraction(QObject):
24    # cancel reason flags
25    NoReason = 0  # No specified reason
26    UserCancelReason = 1  # User canceled the operation (e.g. pressing ESC)
27    InteractionOverrideReason = 3  # Another interaction was set
28    ErrorReason = 4  # An internal error occurred
29    OtherReason = 5
30
31    # Emitted when the interaction is set on the scene.
32    started = Signal()
33
34    # Emitted when the interaction finishes successfully.
35    finished = Signal()
36
37    # Emitted when the interaction ends (canceled or finished)
38    ended = Signal()
39
40    # Emitted when the interaction is canceled.
41    canceled = Signal([], [int])
42
43    def __init__(self, document, parent=None, deleteOnEnd=True):
44        QObject.__init__(self, parent)
45        self.document = document
46        self.scene = document.scene()
47        self.scheme = document.scheme()
48        self.deleteOnEnd = deleteOnEnd
49
50        self.cancelOnEsc = False
51
52        self.__finished = False
53        self.__canceled = False
54        self.__cancelReason = self.NoReason
55
56    def start(self):
57        """Start the interaction. This is called by the scene when
58        the interaction is installed.
59
60        Must be called from subclass implementations.
61
62        """
63        self.started.emit()
64
65    def end(self):
66        """Finish the interaction. Restore any leftover state in
67        this method.
68
69        .. note:: This gets called from the default `cancel` implementation.
70
71        """
72        self.__finished = True
73
74        if self.scene.user_interaction_handler is self:
75            self.scene.set_user_interaction_handler(None)
76
77        if self.__canceled:
78            self.canceled.emit()
79            self.canceled[int].emit(self.__cancelReason)
80        else:
81            self.finished.emit()
82
83        self.ended.emit()
84
85        if self.deleteOnEnd:
86            self.deleteLater()
87
88    def cancel(self, reason=OtherReason):
89        """Cancel the interaction for `reason`.
90        """
91
92        self.__canceled = True
93        self.__cancelReason = reason
94
95        self.end()
96
97    def isFinished(self):
98        """Has the interaction finished.
99        """
100        return self.__finished
101
102    def isCanceled(self):
103        """Was the interaction canceled.
104        """
105        return self.__canceled
106
107    def cancelReason(self):
108        """Return the reason the interaction was canceled.
109        """
110        return self.__cancelReason
111
112    def mousePressEvent(self, event):
113        return False
114
115    def mouseMoveEvent(self, event):
116        return False
117
118    def mouseReleaseEvent(self, event):
119        return False
120
121    def mouseDoubleClickEvent(self, event):
122        return False
123
124    def keyPressEvent(self, event):
125        if self.cancelOnEsc and event.key() == Qt.Key_Escape:
126            self.cancel(self.UserCancelReason)
127        return False
128
129    def keyReleaseEvent(self, event):
130        return False
131
132
133class NoPossibleLinksError(ValueError):
134    pass
135
136
137def reversed_arguments(func):
138    """Return a function with reversed argument order.
139    """
140    def wrapped(*args):
141        return func(*reversed(args))
142    return wrapped
143
144
145class NewLinkAction(UserInteraction):
146    """User drags a new link from an existing node anchor item to create
147    a connection between two existing nodes or to a new node if the release
148    is over an empty area, in which case a quick menu for new node selection
149    is presented to the user.
150
151    """
152    # direction of the drag
153    FROM_SOURCE = 1
154    FROM_SINK = 2
155
156    def __init__(self, document, *args, **kwargs):
157        UserInteraction.__init__(self, document, *args, **kwargs)
158        self.source_item = None
159        self.sink_item = None
160        self.from_item = None
161        self.direction = None
162
163        self.current_target_item = None
164        self.tmp_link_item = None
165        self.tmp_anchor_point = None
166        self.cursor_anchor_point = None
167
168    def remove_tmp_anchor(self):
169        """Remove a temp anchor point from the current target item.
170        """
171        if self.direction == self.FROM_SOURCE:
172            self.current_target_item.removeInputAnchor(self.tmp_anchor_point)
173        else:
174            self.current_target_item.removeOutputAnchor(self.tmp_anchor_point)
175        self.tmp_anchor_point = None
176
177    def create_tmp_anchor(self, item):
178        """Create a new tmp anchor at the item (`NodeItem`).
179        """
180        assert(self.tmp_anchor_point is None)
181        if self.direction == self.FROM_SOURCE:
182            self.tmp_anchor_point = item.newInputAnchor()
183        else:
184            self.tmp_anchor_point = item.newOutputAnchor()
185
186    def can_connect(self, target_item):
187        """Is the connection between `self.from_item` (item where the drag
188        started) and `target_item`.
189
190        """
191        node1 = self.scene.node_for_item(self.from_item)
192        node2 = self.scene.node_for_item(target_item)
193
194        if self.direction == self.FROM_SOURCE:
195            return bool(self.scheme.propose_links(node1, node2))
196        else:
197            return bool(self.scheme.propose_links(node2, node1))
198
199    def set_link_target_anchor(self, anchor):
200        """Set the temp line target anchor
201        """
202        if self.direction == self.FROM_SOURCE:
203            self.tmp_link_item.setSinkItem(None, anchor)
204        else:
205            self.tmp_link_item.setSourceItem(None, anchor)
206
207    def target_node_item_at(self, pos):
208        """Return a suitable NodeItem on which a link can be dropped.
209        """
210        # Test for a suitable NodeAnchorItem or NodeItem at pos.
211        if self.direction == self.FROM_SOURCE:
212            anchor_type = items.SinkAnchorItem
213        else:
214            anchor_type = items.SourceAnchorItem
215
216        item = self.scene.item_at(pos, (anchor_type, items.NodeItem))
217
218        if isinstance(item, anchor_type):
219            item = item.parentNodeItem()
220
221        return item
222
223    def mousePressEvent(self, event):
224        anchor_item = self.scene.item_at(event.scenePos(),
225                                         items.NodeAnchorItem)
226        if anchor_item and event.button() == Qt.LeftButton:
227            # Start a new link starting at item
228            self.from_item = anchor_item.parentNodeItem()
229            if isinstance(anchor_item, items.SourceAnchorItem):
230                self.direction = NewLinkAction.FROM_SOURCE
231                self.source_item = self.from_item
232            else:
233                self.direction = NewLinkAction.FROM_SINK
234                self.sink_item = self.from_item
235
236            event.accept()
237            return True
238        else:
239            # Whoever put us in charge did not know what he was doing.
240            self.cancel(self.ErrorReason)
241            return False
242
243    def mouseMoveEvent(self, event):
244        if not self.tmp_link_item:
245            # On first mouse move event create the temp link item and
246            # initialize it to follow the `cursor_anchor_point`.
247            self.tmp_link_item = items.LinkItem()
248            # An anchor under the cursor for the duration of this action.
249            self.cursor_anchor_point = items.AnchorPoint()
250            self.cursor_anchor_point.setPos(event.scenePos())
251
252            # Set the `fixed` end of the temp link (where the drag started).
253            if self.direction == self.FROM_SOURCE:
254                self.tmp_link_item.setSourceItem(self.source_item)
255            else:
256                self.tmp_link_item.setSinkItem(self.sink_item)
257
258            self.set_link_target_anchor(self.cursor_anchor_point)
259            self.scene.addItem(self.tmp_link_item)
260
261        # `NodeItem` at the cursor position
262        item = self.target_node_item_at(event.scenePos())
263
264        if self.current_target_item is not None and \
265                (item is None or item is not self.current_target_item):
266            # `current_target_item` is no longer under the mouse cursor
267            # (was replaced by another item or the the cursor was moved over
268            # an empty scene spot.
269            log.info("%r is no longer the target.", self.current_target_item)
270            self.remove_tmp_anchor()
271            self.current_target_item = None
272
273        if item is not None and item is not self.from_item:
274            # The mouse is over an node item (different from the starting node)
275            if self.current_target_item is item:
276                # Avoid reseting the points
277                pass
278            elif self.can_connect(item):
279                # Grab a new anchor
280                log.info("%r is the new target.", item)
281                self.create_tmp_anchor(item)
282                self.set_link_target_anchor(self.tmp_anchor_point)
283                self.current_target_item = item
284            else:
285                log.info("%r does not have compatible channels", item)
286                self.set_link_target_anchor(self.cursor_anchor_point)
287                # TODO: How to indicate that the connection is not possible?
288                #       The node's anchor could be drawn with a 'disabled'
289                #       palette
290        else:
291            self.set_link_target_anchor(self.cursor_anchor_point)
292
293        self.cursor_anchor_point.setPos(event.scenePos())
294
295        return True
296
297    def mouseReleaseEvent(self, event):
298        if self.tmp_link_item:
299            item = self.target_node_item_at(event.scenePos())
300            node = None
301            stack = self.document.undoStack()
302            stack.beginMacro("Add link")
303
304            if item:
305                # If the release was over a widget item
306                # then connect them
307                node = self.scene.node_for_item(item)
308            else:
309                # Release on an empty canvas part
310                # Show a quick menu popup for a new widget creation.
311                try:
312                    node = self.create_new(event)
313                except Exception:
314                    log.error("Failed to create a new node, ending.",
315                              exc_info=True)
316                    node = None
317
318                if node is not None:
319                    self.document.addNode(node)
320
321            if node is not None:
322                self.connect_existing(node)
323            else:
324                self.end()
325
326            stack.endMacro()
327        else:
328            self.end()
329            return False
330
331    def create_new(self, event):
332        """Create and return a new node with a QuickWidgetMenu.
333        """
334        pos = event.screenPos()
335        menu = self.document.quickMenu()
336        node = self.scene.node_for_item(self.from_item)
337        from_desc = node.description
338
339        def is_compatible(source, sink):
340            return any(scheme.compatible_channels(output, input) \
341                       for output in source.outputs \
342                       for input in sink.inputs)
343
344        if self.direction == self.FROM_SINK:
345            # Reverse the argument order.
346            is_compatible = reversed_arguments(is_compatible)
347
348        def filter(index):
349            desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE)
350            if desc.isValid():
351                desc = desc.toPyObject()
352                return is_compatible(from_desc, desc)
353            else:
354                return False
355
356        menu.setFilterFunc(filter)
357        try:
358            action = menu.exec_(pos)
359        finally:
360            menu.setFilterFunc(None)
361
362        if action:
363            item = action.property("item").toPyObject()
364            desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE).toPyObject()
365            pos = event.scenePos()
366            node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
367            return node
368
369    def connect_existing(self, node):
370        """Connect anchor_item to `node`.
371        """
372        if self.direction == self.FROM_SOURCE:
373            source_item = self.source_item
374            source_node = self.scene.node_for_item(source_item)
375            sink_node = node
376        else:
377            source_node = node
378            sink_item = self.sink_item
379            sink_node = self.scene.node_for_item(sink_item)
380
381        try:
382            possible = self.scheme.propose_links(source_node, sink_node)
383
384            log.debug("proposed (weighted) links: %r",
385                      [(s1.name, s2.name, w) for s1, s2, w in possible])
386
387            if not possible:
388                raise NoPossibleLinksError
389
390            source, sink, w = possible[0]
391            links_to_add = [(source, sink)]
392
393            show_link_dialog = False
394
395            # Ambiguous new link request.
396            if len(possible) >= 2:
397                # Check for possible ties in the proposed link weights
398                _, _, w2 = possible[1]
399                if w == w2:
400                    show_link_dialog = True
401
402                # Check for destructive action (i.e. would the new link
403                # replace a previous link)
404                if sink.single and self.scheme.find_links(sink_node=sink_node,
405                                                          sink_channel=sink):
406                    show_link_dialog = True
407
408                if show_link_dialog:
409                    links_action = EditNodeLinksAction(
410                                    self.document, source_node, sink_node)
411                    try:
412                        links_action.edit_links()
413                    except Exception:
414                        log.error("'EditNodeLinksAction' failed",
415                                  exc_info=True)
416                        raise
417                    # EditNodeLinksAction already commits the links on accepted
418                    links_to_add = []
419
420            for source, sink in links_to_add:
421                if sink.single:
422                    # Remove an existing link to the sink channel if present.
423                    existing_link = self.scheme.find_links(
424                        sink_node=sink_node, sink_channel=sink
425                    )
426
427                    if existing_link:
428                        self.document.removeLink(existing_link[0])
429
430                # Check if the new link is a duplicate of an existing link
431                duplicate = self.scheme.find_links(
432                    source_node, source, sink_node, sink
433                )
434
435                if duplicate:
436                    # Do nothing.
437                    continue
438
439                # Remove temp items before creating a new link
440                self.cleanup()
441
442                link = scheme.SchemeLink(source_node, source, sink_node, sink)
443                self.document.addLink(link)
444
445        except scheme.IncompatibleChannelTypeError:
446            log.info("Cannot connect: invalid channel types.")
447            self.cancel()
448        except scheme.SchemeTopologyError:
449            log.info("Cannot connect: connection creates a cycle.")
450            self.cancel()
451        except NoPossibleLinksError:
452            log.info("Cannot connect: no possible links.")
453            self.cancel()
454        except Exception:
455            log.error("An error occurred during the creation of a new link.",
456                      exc_info=True)
457            self.cancel()
458
459        if not self.isFinished():
460            self.end()
461
462    def end(self):
463        self.cleanup()
464        UserInteraction.end(self)
465
466    def cancel(self, reason=UserInteraction.OtherReason):
467        self.cleanup()
468        UserInteraction.cancel(self, reason)
469
470    def cleanup(self):
471        """Cleanup all temp items in the scene that are left.
472        """
473        if self.tmp_link_item:
474            self.tmp_link_item.setSinkItem(None)
475            self.tmp_link_item.setSourceItem(None)
476
477            if self.tmp_link_item.scene():
478                self.scene.removeItem(self.tmp_link_item)
479
480            self.tmp_link_item = None
481
482        if self.current_target_item:
483            self.remove_tmp_anchor()
484            self.current_target_item = None
485
486        if self.cursor_anchor_point and self.cursor_anchor_point.scene():
487            self.scene.removeItem(self.cursor_anchor_point)
488            self.cursor_anchor_point = None
489
490
491class NewNodeAction(UserInteraction):
492    """Present the user with a quick menu for node selection and
493    create the selected node.
494
495    """
496
497    def mousePressEvent(self, event):
498        if event.button() == Qt.RightButton:
499            self.create_new(event.screenPos())
500            self.end()
501
502    def create_new(self, pos):
503        """Create a new widget with a QuickWidgetMenu at `pos`
504        (in screen coordinates).
505
506        """
507        menu = self.document.quickMenu()
508        menu.setFilterFunc(None)
509
510        action = menu.exec_(pos)
511        if action:
512            item = action.property("item").toPyObject()
513            desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE).toPyObject()
514            # Get the scene position
515            view = self.document.view()
516            pos = view.mapToScene(view.mapFromGlobal(pos))
517            node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
518            self.document.addNode(node)
519            return node
520
521
522class RectangleSelectionAction(UserInteraction):
523    """Select items in the scene using a Rectangle selection
524    """
525    def __init__(self, document, *args, **kwargs):
526        UserInteraction.__init__(self, document, *args, **kwargs)
527        self.initial_selection = None
528        self.selection_rect = None
529
530    def mousePressEvent(self, event):
531        pos = event.scenePos()
532        any_item = self.scene.item_at(pos)
533        if not any_item and event.button() & Qt.LeftButton:
534            self.selection_rect = QRectF(pos, QSizeF(0, 0))
535            self.rect_item = QGraphicsRectItem(
536                self.selection_rect.normalized()
537            )
538
539            self.rect_item.setPen(
540                QPen(QBrush(QColor(51, 153, 255, 192)),
541                     0.4, Qt.SolidLine, Qt.RoundCap)
542            )
543
544            self.rect_item.setBrush(
545                QBrush(QColor(168, 202, 236, 192))
546            )
547
548            self.rect_item.setZValue(-100)
549
550            # Clear the focus if necessary.
551            if not self.scene.stickyFocus():
552                self.scene.clearFocus()
553            event.accept()
554            return True
555        else:
556            self.cancel(self.ErrorReason)
557            return False
558
559    def mouseMoveEvent(self, event):
560        if not self.rect_item.scene():
561            self.scene.addItem(self.rect_item)
562        self.update_selection(event)
563
564    def mouseReleaseEvent(self, event):
565        self.update_selection(event)
566        self.end()
567
568    def update_selection(self, event):
569        if self.initial_selection is None:
570            self.initial_selection = self.scene.selectedItems()
571
572        pos = event.scenePos()
573        self.selection_rect = QRectF(self.selection_rect.topLeft(), pos)
574        self.rect_item.setRect(self.selection_rect.normalized())
575
576        selected = self.scene.items(self.selection_rect.normalized(),
577                                    Qt.IntersectsItemShape,
578                                    Qt.AscendingOrder)
579
580        selected = [item for item in selected if \
581                    item.flags() & Qt.ItemIsSelectable]
582        if event.modifiers() & Qt.ControlModifier:
583            for item in selected:
584                item.setSelected(item not in self.initial_selection)
585        else:
586            for item in self.initial_selection:
587                item.setSelected(False)
588            for item in selected:
589                item.setSelected(True)
590
591    def end(self):
592        self.initial_selection = None
593        self.rect_item.hide()
594        if self.rect_item.scene() is not None:
595            self.scene.removeItem(self.rect_item)
596        UserInteraction.end(self)
597
598
599class EditNodeLinksAction(UserInteraction):
600    def __init__(self, document, source_node, sink_node, *args, **kwargs):
601        UserInteraction.__init__(self, document, *args, **kwargs)
602        self.source_node = source_node
603        self.sink_node = sink_node
604
605    def edit_links(self):
606        from ..canvas.editlinksdialog import EditLinksDialog
607
608        log.info("Constructing a Link Editor dialog.")
609
610        parent = self.scene.views()[0]
611        dlg = EditLinksDialog(parent)
612
613        links = self.scheme.find_links(source_node=self.source_node,
614                                       sink_node=self.sink_node)
615        existing_links = [(link.source_channel, link.sink_channel)
616                          for link in links]
617
618        dlg.setNodes(self.source_node, self.sink_node)
619        dlg.setLinks(existing_links)
620
621        log.info("Executing a Link Editor Dialog.")
622        rval = dlg.exec_()
623
624        if rval == EditLinksDialog.Accepted:
625            links = dlg.links()
626
627            links_to_add = set(links) - set(existing_links)
628            links_to_remove = set(existing_links) - set(links)
629
630            stack = self.document.undoStack()
631            stack.beginMacro("Edit Links")
632
633            for source_channel, sink_channel in links_to_remove:
634                links = self.scheme.find_links(source_node=self.source_node,
635                                               source_channel=source_channel,
636                                               sink_node=self.sink_node,
637                                               sink_channel=sink_channel)
638
639                self.document.removeLink(links[0])
640
641            for source_channel, sink_channel in links_to_add:
642                link = scheme.SchemeLink(self.source_node, source_channel,
643                                         self.sink_node, sink_channel)
644
645                self.document.addLink(link)
646            stack.endMacro()
647
648
649def point_to_tuple(point):
650    return point.x(), point.y()
651
652
653class NewArrowAnnotation(UserInteraction):
654    """Create a new arrow annotation.
655    """
656    def __init__(self, document, *args, **kwargs):
657        UserInteraction.__init__(self, document, *args, **kwargs)
658        self.down_pos = None
659        self.arrow_item = None
660        self.annotation = None
661        self.color = "red"
662
663    def start(self):
664        self.document.view().setCursor(Qt.CrossCursor)
665        UserInteraction.start(self)
666
667    def setColor(self, color):
668        self.color = color
669
670    def mousePressEvent(self, event):
671        if event.button() == Qt.LeftButton:
672            self.down_pos = event.scenePos()
673            event.accept()
674            return True
675
676    def mouseMoveEvent(self, event):
677        if event.buttons() & Qt.LeftButton:
678            if self.arrow_item is None and \
679                    (self.down_pos - event.scenePos()).manhattanLength() > \
680                    QApplication.instance().startDragDistance():
681
682                annot = scheme.SchemeArrowAnnotation(
683                    point_to_tuple(self.down_pos),
684                    point_to_tuple(event.scenePos())
685                )
686                annot.set_color(self.color)
687                item = self.scene.add_annotation(annot)
688
689                self.arrow_item = item
690                self.annotation = annot
691
692            if self.arrow_item is not None:
693                p1, p2 = map(self.arrow_item.mapFromScene,
694                             (self.down_pos, event.scenePos()))
695                self.arrow_item.setLine(QLineF(p1, p2))
696                self.arrow_item.adjustGeometry()
697
698            event.accept()
699            return True
700
701    def mouseReleaseEvent(self, event):
702        if event.button() == Qt.LeftButton:
703            if self.arrow_item is not None:
704                p1, p2 = self.down_pos, event.scenePos()
705
706                # Commit the annotation to the scheme
707                self.annotation.set_line(point_to_tuple(p1),
708                                         point_to_tuple(p2))
709
710                self.document.addAnnotation(self.annotation)
711
712                p1, p2 = map(self.arrow_item.mapFromScene, (p1, p2))
713                self.arrow_item.setLine(QLineF(p1, p2))
714                self.arrow_item.adjustGeometry()
715
716            self.end()
717            return True
718
719    def end(self):
720        self.down_pos = None
721        self.arrow_item = None
722        self.annotation = None
723        self.document.view().setCursor(Qt.ArrowCursor)
724        UserInteraction.end(self)
725
726
727def rect_to_tuple(rect):
728    return rect.x(), rect.y(), rect.width(), rect.height()
729
730
731class NewTextAnnotation(UserInteraction):
732    def __init__(self, document, *args, **kwargs):
733        UserInteraction.__init__(self, document, *args, **kwargs)
734        self.down_pos = None
735        self.annotation_item = None
736        self.annotation = None
737        self.control = None
738        self.font = document.font()
739
740    def setFont(self, font):
741        self.font = font
742
743    def start(self):
744        self.document.view().setCursor(Qt.CrossCursor)
745        UserInteraction.start(self)
746
747    def createNewAnnotation(self, rect):
748        """Create a new TextAnnotation at with `rect` as the geometry.
749        """
750        annot = scheme.SchemeTextAnnotation(rect_to_tuple(rect))
751        font = {"family": unicode(self.font.family()),
752                "size": self.font.pointSize()}
753        annot.set_font(font)
754
755        item = self.scene.add_annotation(annot)
756        item.setTextInteractionFlags(Qt.TextEditorInteraction)
757        item.setFramePen(QPen(Qt.DashLine))
758
759        self.annotation_item = item
760        self.annotation = annot
761        self.control = controlpoints.ControlPointRect()
762        self.control.rectChanged.connect(
763            self.annotation_item.setGeometry
764        )
765        self.scene.addItem(self.control)
766
767    def mousePressEvent(self, event):
768        if event.button() == Qt.LeftButton:
769            self.down_pos = event.scenePos()
770            return True
771
772    def mouseMoveEvent(self, event):
773        if event.buttons() & Qt.LeftButton:
774            if self.annotation_item is None and \
775                    (self.down_pos - event.scenePos()).manhattanLength() > \
776                    QApplication.instance().startDragDistance():
777                rect = QRectF(self.down_pos, event.scenePos()).normalized()
778                self.createNewAnnotation(rect)
779
780            if self.annotation_item is not None:
781                rect = QRectF(self.down_pos, event.scenePos()).normalized()
782                self.control.setRect(rect)
783
784            return True
785
786    def mouseReleaseEvent(self, event):
787        if event.button() == Qt.LeftButton:
788            if self.annotation_item is None:
789                self.createNewAnnotation(QRectF(event.scenePos(),
790                                                event.scenePos()))
791                rect = self.defaultTextGeometry(event.scenePos())
792
793            else:
794                rect = QRectF(self.down_pos, event.scenePos()).normalized()
795
796            # Commit the annotation to the scheme.
797            self.annotation.rect = rect_to_tuple(rect)
798
799            self.document.addAnnotation(self.annotation)
800
801            self.annotation_item.setGeometry(rect)
802
803            self.control.rectChanged.disconnect(
804                self.annotation_item.setGeometry
805            )
806            self.control.hide()
807
808            # Move the focus to the editor.
809            self.annotation_item.setFramePen(QPen(Qt.NoPen))
810            self.annotation_item.setFocus(Qt.OtherFocusReason)
811            self.annotation_item.startEdit()
812
813            self.end()
814
815    def defaultTextGeometry(self, point):
816        """Return the default text geometry. Used in case the user
817        single clicked in the scene.
818
819        """
820        font = self.annotation_item.font()
821        metrics = QFontMetrics(font)
822        spacing = metrics.lineSpacing()
823        margin = self.annotation_item.document().documentMargin()
824
825        rect = QRectF(QPointF(point.x(), point.y() - spacing - margin),
826                      QSizeF(150, spacing + 2 * margin))
827        return rect
828
829    def end(self):
830        if self.control is not None:
831            self.scene.removeItem(self.control)
832
833        self.control = None
834        self.down_pos = None
835        self.annotation_item = None
836        self.annotation = None
837        self.document.view().setCursor(Qt.ArrowCursor)
838        UserInteraction.end(self)
839
840
841class ResizeTextAnnotation(UserInteraction):
842    def __init__(self, document, *args, **kwargs):
843        UserInteraction.__init__(self, document, *args, **kwargs)
844        self.item = None
845        self.annotation = None
846        self.control = None
847        self.savedFramePen = None
848        self.savedRect = None
849
850    def mousePressEvent(self, event):
851        pos = event.scenePos()
852        if self.item is None:
853            item = self.scene.item_at(pos, items.TextAnnotation)
854            if item is not None and not item.hasFocus():
855                self.editItem(item)
856                return False
857
858        return UserInteraction.mousePressEvent(self, event)
859
860    def editItem(self, item):
861        annotation = self.scene.annotation_for_item(item)
862        rect = item.geometry()  # TODO: map to scene if item has a parent.
863        control = controlpoints.ControlPointRect(rect=rect)
864        self.scene.addItem(control)
865
866        self.savedFramePen = item.framePen()
867        self.savedRect = rect
868
869        control.rectEdited.connect(item.setGeometry)
870        control.setFocusProxy(item)
871
872        item.setFramePen(QPen(Qt.DashDotLine))
873        item.geometryChanged.connect(self.__on_textGeometryChanged)
874
875        self.item = item
876
877        self.annotation = annotation
878        self.control = control
879
880    def commit(self):
881        """Commit the current item geometry state to the document.
882        """
883        rect = self.item.geometry()
884        if self.savedRect != rect:
885            command = commands.SetAttrCommand(
886                self.annotation, "rect",
887                (rect.x(), rect.y(), rect.width(), rect.height()),
888                name="Edit text geometry"
889            )
890            self.document.undoStack().push(command)
891            self.savedRect = rect
892
893    def __on_editingFinished(self):
894        self.commit()
895        self.end()
896
897    def __on_rectEdited(self, rect):
898        self.item.setGeometry(rect)
899
900    def __on_textGeometryChanged(self):
901        if not self.control.isControlActive():
902            rect = self.item.geometry()
903            self.control.setRect(rect)
904
905    def cancel(self, reason=UserInteraction.OtherReason):
906        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
907        if self.item is not None and self.savedRect is not None:
908            self.item.setGeometry(self.savedRect)
909
910        UserInteraction.cancel(self, reason)
911
912    def end(self):
913        if self.control is not None:
914            self.scene.removeItem(self.control)
915
916        if self.item is not None:
917            self.item.setFramePen(self.savedFramePen)
918
919        self.item = None
920        self.annotation = None
921        self.control = None
922
923        UserInteraction.end(self)
924
925
926class ResizeArrowAnnotation(UserInteraction):
927    def __init__(self, document, *args, **kwargs):
928        UserInteraction.__init__(self, document, *args, **kwargs)
929        self.item = None
930        self.annotation = None
931        self.control = None
932        self.savedLine = None
933
934    def mousePressEvent(self, event):
935        pos = event.scenePos()
936        if self.item is None:
937            item = self.scene.item_at(pos, items.ArrowAnnotation)
938            if item is not None and not item.hasFocus():
939                self.editItem(item)
940                return False
941
942        return UserInteraction.mousePressEvent(self, event)
943
944    def editItem(self, item):
945        annotation = self.scene.annotation_for_item(item)
946        control = controlpoints.ControlPointLine()
947        self.scene.addItem(control)
948
949        line = item.line()
950        self.savedLine = line
951
952        p1, p2 = map(item.mapToScene, (line.p1(), line.p2()))
953
954        control.setLine(QLineF(p1, p2))
955        control.setFocusProxy(item)
956        control.lineEdited.connect(self.__on_lineEdited)
957
958        item.geometryChanged.connect(self.__on_lineGeometryChanged)
959
960        self.item = item
961        self.annotation = annotation
962        self.control = control
963
964    def commit(self):
965        """Commit the current geometry of the item to the document.
966
967        .. note:: Does nothing if the actual geometry is not changed.
968
969        """
970        line = self.control.line()
971        p1, p2 = line.p1(), line.p2()
972
973        if self.item.line() != self.savedLine:
974            command = commands.SetAttrCommand(
975                self.annotation,
976                "geometry",
977                ((p1.x(), p1.y()), (p2.x(), p2.y())),
978                name="Edit arrow geometry",
979            )
980            self.document.undoStack().push(command)
981            self.savedLine = self.item.line()
982
983    def __on_editingFinished(self):
984        self.commit()
985        self.end()
986
987    def __on_lineEdited(self, line):
988        p1, p2 = map(self.item.mapFromScene, (line.p1(), line.p2()))
989        self.item.setLine(QLineF(p1, p2))
990        self.item.adjustGeometry()
991
992    def __on_lineGeometryChanged(self):
993        # Possible geometry change from out of our control, for instance
994        # item move as a part of a selection group.
995        if not self.control.isControlActive():
996            line = self.item.line()
997            p1, p2 = map(self.item.mapToScene, (line.p1(), line.p2()))
998            self.control.setLine(QLineF(p1, p2))
999
1000    def cancel(self, reason=UserInteraction.OtherReason):
1001        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
1002        if self.item is not None and self.savedLine is not None:
1003            self.item.setLine(self.savedLine)
1004
1005        UserInteraction.cancel(self, reason)
1006
1007    def end(self):
1008        if self.control is not None:
1009            self.scene.removeItem(self.control)
1010
1011        if self.item is not None:
1012            self.item.geometryChanged.disconnect(self.__on_lineGeometryChanged)
1013
1014        self.control = None
1015        self.item = None
1016        self.annotation = None
1017
1018        UserInteraction.end(self)
Note: See TracBrowser for help on using the repository browser.