source: orange/Orange/OrangeCanvas/document/interactions.py @ 11195:e90582a13944

Revision 11195:e90582a13944, 32.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Changed the base class of UserInteraction to QObject, added notifier signals.

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        height = metrics.lineSpacing()
806        rect = QRectF(QPointF(point.x(), point.y() - height),
807                      QSizeF(150, height))
808        return rect
809
810    def end(self):
811        if self.control is not None:
812            self.scene.removeItem(self.control)
813
814        self.control = None
815        self.down_pos = None
816        self.annotation_item = None
817        self.annotation = None
818        self.document.view().setCursor(Qt.ArrowCursor)
819        UserInteraction.end(self)
820
821
822class ResizeTextAnnotation(UserInteraction):
823    def __init__(self, document, *args, **kwargs):
824        UserInteraction.__init__(self, document, *args, **kwargs)
825        self.item = None
826        self.annotation = None
827        self.control = None
828        self.savedFramePen = None
829        self.savedRect = None
830
831    def mousePressEvent(self, event):
832        pos = event.scenePos()
833        if self.item is None:
834            item = self.scene.item_at(pos, items.TextAnnotation)
835            if item is not None and not item.hasFocus():
836                self.editItem(item)
837                return False
838
839        return UserInteraction.mousePressEvent(self, event)
840
841    def editItem(self, item):
842        annotation = self.scene.annotation_for_item(item)
843        rect = item.geometry()  # TODO: map to scene if item has a parent.
844        control = controlpoints.ControlPointRect(rect=rect)
845        self.scene.addItem(control)
846
847        self.savedFramePen = item.framePen()
848        self.savedRect = rect
849
850        control.rectEdited.connect(item.setGeometry)
851        control.setFocusProxy(item)
852
853        item.setFramePen(QPen(Qt.DashDotLine))
854        item.geometryChanged.connect(self.__on_textGeometryChanged)
855
856        self.item = item
857
858        self.annotation = annotation
859        self.control = control
860
861    def commit(self):
862        """Commit the current item geometry state to the document.
863        """
864        rect = self.item.geometry()
865        if self.savedRect != rect:
866            command = commands.SetAttrCommand(
867                self.annotation, "rect",
868                (rect.x(), rect.y(), rect.width(), rect.height()),
869                name="Edit text geometry"
870            )
871            self.document.undoStack().push(command)
872            self.savedRect = rect
873
874    def __on_editingFinished(self):
875        self.commit()
876        self.end()
877
878    def __on_rectEdited(self, rect):
879        self.item.setGeometry(rect)
880
881    def __on_textGeometryChanged(self):
882        if not self.control.isControlActive():
883            rect = self.item.geometry()
884            self.control.setRect(rect)
885
886    def cancel(self, reason=UserInteraction.OtherReason):
887        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
888        if self.item is not None and self.savedRect is not None:
889            self.item.setGeometry(self.savedRect)
890
891        UserInteraction.cancel(self, reason)
892
893    def end(self):
894        if self.control is not None:
895            self.scene.removeItem(self.control)
896
897        if self.item is not None:
898            self.item.setFramePen(self.savedFramePen)
899
900        self.item = None
901        self.annotation = None
902        self.control = None
903
904        UserInteraction.end(self)
905
906
907class ResizeArrowAnnotation(UserInteraction):
908    def __init__(self, document, *args, **kwargs):
909        UserInteraction.__init__(self, document, *args, **kwargs)
910        self.item = None
911        self.annotation = None
912        self.control = None
913        self.savedLine = None
914
915    def mousePressEvent(self, event):
916        pos = event.scenePos()
917        if self.item is None:
918            item = self.scene.item_at(pos, items.ArrowAnnotation)
919            if item is not None and not item.hasFocus():
920                self.editItem(item)
921                return False
922
923        return UserInteraction.mousePressEvent(self, event)
924
925    def editItem(self, item):
926        annotation = self.scene.annotation_for_item(item)
927        control = controlpoints.ControlPointLine()
928        self.scene.addItem(control)
929
930        line = item.line()
931        self.savedLine = line
932
933        p1, p2 = map(item.mapToScene, (line.p1(), line.p2()))
934
935        control.setLine(QLineF(p1, p2))
936        control.setFocusProxy(item)
937        control.lineEdited.connect(self.__on_lineEdited)
938
939        item.geometryChanged.connect(self.__on_lineGeometryChanged)
940
941        self.item = item
942        self.annotation = annotation
943        self.control = control
944
945    def commit(self):
946        """Commit the current geometry of the item to the document.
947
948        .. note:: Does nothing if the actual geometry is not changed.
949
950        """
951        line = self.control.line()
952        p1, p2 = line.p1(), line.p2()
953
954        if self.item.line() != self.savedLine:
955            command = commands.SetAttrCommand(
956                self.annotation,
957                "geometry",
958                ((p1.x(), p1.y()), (p2.x(), p2.y())),
959                name="Edit arrow geometry",
960            )
961            self.document.undoStack().push(command)
962            self.savedLine = self.item.line()
963
964    def __on_editingFinished(self):
965        self.commit()
966        self.end()
967
968    def __on_lineEdited(self, line):
969        p1, p2 = map(self.item.mapFromScene, (line.p1(), line.p2()))
970        self.item.setLine(QLineF(p1, p2))
971        self.item.adjustGeometry()
972
973    def __on_lineGeometryChanged(self):
974        # Possible geometry change from out of our control, for instance
975        # item move as a part of a selection group.
976        if not self.control.isControlActive():
977            line = self.item.line()
978            p1, p2 = map(self.item.mapToScene, (line.p1(), line.p2()))
979            self.control.setLine(QLineF(p1, p2))
980
981    def cancel(self, reason=UserInteraction.OtherReason):
982        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
983        if self.item is not None and self.savedLine is not None:
984            self.item.setLine(self.savedLine)
985
986        UserInteraction.cancel(self, reason)
987
988    def end(self):
989        if self.control is not None:
990            self.scene.removeItem(self.control)
991
992        if self.item is not None:
993            self.item.geometryChanged.disconnect(self.__on_lineGeometryChanged)
994
995        self.control = None
996        self.item = None
997        self.annotation = None
998
999        UserInteraction.end(self)
Note: See TracBrowser for help on using the repository browser.