source: orange/Orange/OrangeCanvas/document/interactions.py @ 11196:df14867f80ad

Revision 11196:df14867f80ad, 32.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Slight fix for the default text annotation geometry.

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            if not possible:
385                raise NoPossibleLinksError
386
387            links_to_add = []
388            links_to_remove = []
389
390            # Check for possible ties in the proposed link weights
391            if len(possible) >= 2:
392                source, sink, w1 = possible[0]
393                _, _, w2 = possible[1]
394                if w1 == w2:
395                    # If there are ties in the weights a detailed link
396                    # dialog is presented to the user.
397                    links_action = EditNodeLinksAction(
398                                    self.document, source_node, sink_node)
399                    try:
400                        links = links_action.edit_links()
401                    except Exception:
402                        log.error("'EditNodeLinksAction' failed",
403                                  exc_info=True)
404                        raise
405                else:
406                    links_to_add = [(source, sink)]
407            else:
408                source, sink, _ = possible[0]
409                links_to_add = [(source, sink)]
410
411            for source, sink in links_to_remove:
412                existing_link = self.scheme.find_links(
413                                    source_node=source_node,
414                                    source_channel=source,
415                                    sink_node=sink_node,
416                                    sink_channel=sink)
417
418                self.document.removeLink(existing_link)
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)
500            self.end()
501
502    def create_new(self, event):
503        """Create a new widget with a QuickWidgetMenu
504        """
505        pos = event.screenPos()
506        menu = self.document.quickMenu()
507        menu.setFilterFunc(None)
508
509        action = menu.exec_(pos)
510        if action:
511            item = action.property("item").toPyObject()
512            desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE).toPyObject()
513            pos = event.scenePos()
514            node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
515            self.document.addNode(node)
516            return node
517
518
519class RectangleSelectionAction(UserInteraction):
520    """Select items in the scene using a Rectangle selection
521    """
522    def __init__(self, document, *args, **kwargs):
523        UserInteraction.__init__(self, document, *args, **kwargs)
524        self.initial_selection = None
525
526    def mousePressEvent(self, event):
527        pos = event.scenePos()
528        any_item = self.scene.item_at(pos)
529        if not any_item and event.button() & Qt.LeftButton:
530            self.selection_rect = QRectF(pos, QSizeF(0, 0))
531            self.rect_item = QGraphicsRectItem(
532                self.selection_rect.normalized()
533            )
534
535            self.rect_item.setPen(
536                QPen(QBrush(QColor(51, 153, 255, 192)),
537                     0.4, Qt.SolidLine, Qt.RoundCap)
538            )
539
540            self.rect_item.setBrush(
541                QBrush(QColor(168, 202, 236, 192))
542            )
543
544            self.rect_item.setZValue(-100)
545
546            # Clear the focus if necessary.
547            if not self.scene.stickyFocus():
548                self.scene.clearFocus()
549            event.accept()
550            return True
551        else:
552            self.cancel(self.ErrorReason)
553            return False
554
555    def mouseMoveEvent(self, event):
556        if not self.rect_item.scene():
557            self.scene.addItem(self.rect_item)
558        self.update_selection(event)
559
560    def mouseReleaseEvent(self, event):
561        self.update_selection(event)
562        self.end()
563
564    def update_selection(self, event):
565        if self.initial_selection is None:
566            self.initial_selection = self.scene.selectedItems()
567
568        pos = event.scenePos()
569        self.selection_rect = QRectF(self.selection_rect.topLeft(), pos)
570        self.rect_item.setRect(self.selection_rect.normalized())
571
572        selected = self.scene.items(self.selection_rect.normalized(),
573                                    Qt.IntersectsItemShape,
574                                    Qt.AscendingOrder)
575
576        selected = [item for item in selected if \
577                    item.flags() & Qt.ItemIsSelectable]
578        if event.modifiers() & Qt.ControlModifier:
579            for item in selected:
580                item.setSelected(item not in self.initial_selection)
581        else:
582            for item in self.initial_selection:
583                item.setSelected(False)
584            for item in selected:
585                item.setSelected(True)
586
587    def end(self):
588        self.initial_selection = None
589        self.rect_item.hide()
590        if self.rect_item.scene() is not None:
591            self.scene.removeItem(self.rect_item)
592        UserInteraction.end(self)
593
594
595class EditNodeLinksAction(UserInteraction):
596    def __init__(self, document, source_node, sink_node, *args, **kwargs):
597        UserInteraction.__init__(self, document, *args, **kwargs)
598        self.source_node = source_node
599        self.sink_node = sink_node
600
601    def edit_links(self):
602        from ..canvas.editlinksdialog import EditLinksDialog
603
604        log.info("Constructing a Link Editor dialog.")
605
606        parent = self.scene.views()[0]
607        dlg = EditLinksDialog(parent)
608
609        links = self.scheme.find_links(source_node=self.source_node,
610                                       sink_node=self.sink_node)
611        existing_links = [(link.source_channel, link.sink_channel)
612                          for link in links]
613
614        dlg.setNodes(self.source_node, self.sink_node)
615        dlg.setLinks(existing_links)
616
617        log.info("Executing a Link Editor Dialog.")
618        rval = dlg.exec_()
619
620        if rval == EditLinksDialog.Accepted:
621            links = dlg.links()
622
623            links_to_add = set(links) - set(existing_links)
624            links_to_remove = set(existing_links) - set(links)
625
626            stack = self.document.undoStack()
627            stack.beginMacro("Edit Links")
628
629            for source_channel, sink_channel in links_to_remove:
630                links = self.scheme.find_links(source_node=self.source_node,
631                                               source_channel=source_channel,
632                                               sink_node=self.sink_node,
633                                               sink_channel=sink_channel)
634
635                self.document.removeLink(links[0])
636
637            for source_channel, sink_channel in links_to_add:
638                link = scheme.SchemeLink(self.source_node, source_channel,
639                                         self.sink_node, sink_channel)
640
641                self.document.addLink(link)
642            stack.endMacro()
643
644
645def point_to_tuple(point):
646    return point.x(), point.y()
647
648
649class NewArrowAnnotation(UserInteraction):
650    """Create a new arrow annotation.
651    """
652    def __init__(self, document, *args, **kwargs):
653        UserInteraction.__init__(self, document, *args, **kwargs)
654        self.down_pos = None
655        self.arrow_item = None
656        self.annotation = None
657
658    def start(self):
659        self.document.view().setCursor(Qt.CrossCursor)
660        UserInteraction.start(self)
661
662    def mousePressEvent(self, event):
663        if event.button() == Qt.LeftButton:
664            self.down_pos = event.scenePos()
665            event.accept()
666            return True
667
668    def mouseMoveEvent(self, event):
669        if event.buttons() & Qt.LeftButton:
670            if self.arrow_item is None and \
671                    (self.down_pos - event.scenePos()).manhattanLength() > \
672                    QApplication.instance().startDragDistance():
673
674                annot = scheme.SchemeArrowAnnotation(
675                    point_to_tuple(self.down_pos),
676                    point_to_tuple(event.scenePos())
677                )
678                item = self.scene.add_annotation(annot)
679                self.arrow_item = item
680                self.annotation = annot
681
682            if self.arrow_item is not None:
683                p1, p2 = map(self.arrow_item.mapFromScene,
684                             (self.down_pos, event.scenePos()))
685                self.arrow_item.setLine(QLineF(p1, p2))
686                self.arrow_item.adjustGeometry()
687
688            event.accept()
689            return True
690
691    def mouseReleaseEvent(self, event):
692        if event.button() == Qt.LeftButton:
693            if self.arrow_item is not None:
694                p1, p2 = self.down_pos, event.scenePos()
695
696                # Commit the annotation to the scheme
697                self.annotation.set_line(point_to_tuple(p1),
698                                         point_to_tuple(p2))
699
700                self.document.addAnnotation(self.annotation)
701
702                p1, p2 = map(self.arrow_item.mapFromScene, (p1, p2))
703                self.arrow_item.setLine(QLineF(p1, p2))
704                self.arrow_item.adjustGeometry()
705
706            self.end()
707            return True
708
709    def end(self):
710        self.down_pos = None
711        self.arrow_item = None
712        self.annotation = None
713        self.document.view().setCursor(Qt.ArrowCursor)
714        UserInteraction.end(self)
715
716
717def rect_to_tuple(rect):
718    return rect.x(), rect.y(), rect.width(), rect.height()
719
720
721class NewTextAnnotation(UserInteraction):
722    def __init__(self, document, *args, **kwargs):
723        UserInteraction.__init__(self, document, *args, **kwargs)
724        self.down_pos = None
725        self.annotation_item = None
726        self.annotation = None
727        self.control = None
728
729    def start(self):
730        self.document.view().setCursor(Qt.CrossCursor)
731        UserInteraction.start(self)
732
733    def createNewAnnotation(self, rect):
734        """Create a new TextAnnotation at with `rect` as the geometry.
735        """
736        annot = scheme.SchemeTextAnnotation(rect_to_tuple(rect))
737
738        item = self.scene.add_annotation(annot)
739        item.setTextInteractionFlags(Qt.TextEditorInteraction)
740        item.setFramePen(QPen(Qt.DashLine))
741
742        self.annotation_item = item
743        self.annotation = annot
744        self.control = controlpoints.ControlPointRect()
745        self.control.rectChanged.connect(
746            self.annotation_item.setGeometry
747        )
748        self.scene.addItem(self.control)
749
750    def mousePressEvent(self, event):
751        if event.button() == Qt.LeftButton:
752            self.down_pos = event.scenePos()
753            return True
754
755    def mouseMoveEvent(self, event):
756        if event.buttons() & Qt.LeftButton:
757            if self.annotation_item is None and \
758                    (self.down_pos - event.scenePos()).manhattanLength() > \
759                    QApplication.instance().startDragDistance():
760                rect = QRectF(self.down_pos, event.scenePos()).normalized()
761                self.createNewAnnotation(rect)
762
763            if self.annotation_item is not None:
764                rect = QRectF(self.down_pos, event.scenePos()).normalized()
765                self.control.setRect(rect)
766
767            return True
768
769    def mouseReleaseEvent(self, event):
770        if event.button() == Qt.LeftButton:
771            if self.annotation_item is None:
772                self.createNewAnnotation(QRectF(event.scenePos(),
773                                                event.scenePos()))
774                rect = self.defaultTextGeometry(event.scenePos())
775
776            else:
777                rect = QRectF(self.down_pos, event.scenePos()).normalized()
778
779            # Commit the annotation to the scheme.
780            self.annotation.rect = rect_to_tuple(rect)
781
782            self.document.addAnnotation(self.annotation)
783
784            self.annotation_item.setGeometry(rect)
785
786            self.control.rectChanged.disconnect(
787                self.annotation_item.setGeometry
788            )
789            self.control.hide()
790
791            # Move the focus to the editor.
792            self.annotation_item.setFramePen(QPen(Qt.NoPen))
793            self.annotation_item.setFocus(Qt.OtherFocusReason)
794            self.annotation_item.startEdit()
795
796            self.end()
797
798    def defaultTextGeometry(self, point):
799        """Return the default text geometry. Used in case the user
800        single clicked in the scene.
801
802        """
803        font = self.annotation_item.font()
804        metrics = QFontMetrics(font)
805        spacing = metrics.lineSpacing()
806        margin = self.annotation_item.document().documentMargin()
807
808        rect = QRectF(QPointF(point.x(), point.y() - spacing - margin),
809                      QSizeF(150, spacing + 2 * margin))
810        return rect
811
812    def end(self):
813        if self.control is not None:
814            self.scene.removeItem(self.control)
815
816        self.control = None
817        self.down_pos = None
818        self.annotation_item = None
819        self.annotation = None
820        self.document.view().setCursor(Qt.ArrowCursor)
821        UserInteraction.end(self)
822
823
824class ResizeTextAnnotation(UserInteraction):
825    def __init__(self, document, *args, **kwargs):
826        UserInteraction.__init__(self, document, *args, **kwargs)
827        self.item = None
828        self.annotation = None
829        self.control = None
830        self.savedFramePen = None
831        self.savedRect = None
832
833    def mousePressEvent(self, event):
834        pos = event.scenePos()
835        if self.item is None:
836            item = self.scene.item_at(pos, items.TextAnnotation)
837            if item is not None and not item.hasFocus():
838                self.editItem(item)
839                return False
840
841        return UserInteraction.mousePressEvent(self, event)
842
843    def editItem(self, item):
844        annotation = self.scene.annotation_for_item(item)
845        rect = item.geometry()  # TODO: map to scene if item has a parent.
846        control = controlpoints.ControlPointRect(rect=rect)
847        self.scene.addItem(control)
848
849        self.savedFramePen = item.framePen()
850        self.savedRect = rect
851
852        control.rectEdited.connect(item.setGeometry)
853        control.setFocusProxy(item)
854
855        item.setFramePen(QPen(Qt.DashDotLine))
856        item.geometryChanged.connect(self.__on_textGeometryChanged)
857
858        self.item = item
859
860        self.annotation = annotation
861        self.control = control
862
863    def commit(self):
864        """Commit the current item geometry state to the document.
865        """
866        rect = self.item.geometry()
867        if self.savedRect != rect:
868            command = commands.SetAttrCommand(
869                self.annotation, "rect",
870                (rect.x(), rect.y(), rect.width(), rect.height()),
871                name="Edit text geometry"
872            )
873            self.document.undoStack().push(command)
874            self.savedRect = rect
875
876    def __on_editingFinished(self):
877        self.commit()
878        self.end()
879
880    def __on_rectEdited(self, rect):
881        self.item.setGeometry(rect)
882
883    def __on_textGeometryChanged(self):
884        if not self.control.isControlActive():
885            rect = self.item.geometry()
886            self.control.setRect(rect)
887
888    def cancel(self, reason=UserInteraction.OtherReason):
889        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
890        if self.item is not None and self.savedRect is not None:
891            self.item.setGeometry(self.savedRect)
892
893        UserInteraction.cancel(self, reason)
894
895    def end(self):
896        if self.control is not None:
897            self.scene.removeItem(self.control)
898
899        if self.item is not None:
900            self.item.setFramePen(self.savedFramePen)
901
902        self.item = None
903        self.annotation = None
904        self.control = None
905
906        UserInteraction.end(self)
907
908
909class ResizeArrowAnnotation(UserInteraction):
910    def __init__(self, document, *args, **kwargs):
911        UserInteraction.__init__(self, document, *args, **kwargs)
912        self.item = None
913        self.annotation = None
914        self.control = None
915        self.savedLine = None
916
917    def mousePressEvent(self, event):
918        pos = event.scenePos()
919        if self.item is None:
920            item = self.scene.item_at(pos, items.ArrowAnnotation)
921            if item is not None and not item.hasFocus():
922                self.editItem(item)
923                return False
924
925        return UserInteraction.mousePressEvent(self, event)
926
927    def editItem(self, item):
928        annotation = self.scene.annotation_for_item(item)
929        control = controlpoints.ControlPointLine()
930        self.scene.addItem(control)
931
932        line = item.line()
933        self.savedLine = line
934
935        p1, p2 = map(item.mapToScene, (line.p1(), line.p2()))
936
937        control.setLine(QLineF(p1, p2))
938        control.setFocusProxy(item)
939        control.lineEdited.connect(self.__on_lineEdited)
940
941        item.geometryChanged.connect(self.__on_lineGeometryChanged)
942
943        self.item = item
944        self.annotation = annotation
945        self.control = control
946
947    def commit(self):
948        """Commit the current geometry of the item to the document.
949
950        .. note:: Does nothing if the actual geometry is not changed.
951
952        """
953        line = self.control.line()
954        p1, p2 = line.p1(), line.p2()
955
956        if self.item.line() != self.savedLine:
957            command = commands.SetAttrCommand(
958                self.annotation,
959                "geometry",
960                ((p1.x(), p1.y()), (p2.x(), p2.y())),
961                name="Edit arrow geometry",
962            )
963            self.document.undoStack().push(command)
964            self.savedLine = self.item.line()
965
966    def __on_editingFinished(self):
967        self.commit()
968        self.end()
969
970    def __on_lineEdited(self, line):
971        p1, p2 = map(self.item.mapFromScene, (line.p1(), line.p2()))
972        self.item.setLine(QLineF(p1, p2))
973        self.item.adjustGeometry()
974
975    def __on_lineGeometryChanged(self):
976        # Possible geometry change from out of our control, for instance
977        # item move as a part of a selection group.
978        if not self.control.isControlActive():
979            line = self.item.line()
980            p1, p2 = map(self.item.mapToScene, (line.p1(), line.p2()))
981            self.control.setLine(QLineF(p1, p2))
982
983    def cancel(self, reason=UserInteraction.OtherReason):
984        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
985        if self.item is not None and self.savedLine is not None:
986            self.item.setLine(self.savedLine)
987
988        UserInteraction.cancel(self, reason)
989
990    def end(self):
991        if self.control is not None:
992            self.scene.removeItem(self.control)
993
994        if self.item is not None:
995            self.item.geometryChanged.disconnect(self.__on_lineGeometryChanged)
996
997        self.control = None
998        self.item = None
999        self.annotation = None
1000
1001        UserInteraction.end(self)
Note: See TracBrowser for help on using the repository browser.