source: orange/Orange/OrangeCanvas/document/interactions.py @ 11199:d0e9c2fbc5f2

Revision 11199:d0e9c2fbc5f2, 33.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Fixed new link creation when more than one links are possible.

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
662    def start(self):
663        self.document.view().setCursor(Qt.CrossCursor)
664        UserInteraction.start(self)
665
666    def mousePressEvent(self, event):
667        if event.button() == Qt.LeftButton:
668            self.down_pos = event.scenePos()
669            event.accept()
670            return True
671
672    def mouseMoveEvent(self, event):
673        if event.buttons() & Qt.LeftButton:
674            if self.arrow_item is None and \
675                    (self.down_pos - event.scenePos()).manhattanLength() > \
676                    QApplication.instance().startDragDistance():
677
678                annot = scheme.SchemeArrowAnnotation(
679                    point_to_tuple(self.down_pos),
680                    point_to_tuple(event.scenePos())
681                )
682                item = self.scene.add_annotation(annot)
683                self.arrow_item = item
684                self.annotation = annot
685
686            if self.arrow_item is not None:
687                p1, p2 = map(self.arrow_item.mapFromScene,
688                             (self.down_pos, event.scenePos()))
689                self.arrow_item.setLine(QLineF(p1, p2))
690                self.arrow_item.adjustGeometry()
691
692            event.accept()
693            return True
694
695    def mouseReleaseEvent(self, event):
696        if event.button() == Qt.LeftButton:
697            if self.arrow_item is not None:
698                p1, p2 = self.down_pos, event.scenePos()
699
700                # Commit the annotation to the scheme
701                self.annotation.set_line(point_to_tuple(p1),
702                                         point_to_tuple(p2))
703
704                self.document.addAnnotation(self.annotation)
705
706                p1, p2 = map(self.arrow_item.mapFromScene, (p1, p2))
707                self.arrow_item.setLine(QLineF(p1, p2))
708                self.arrow_item.adjustGeometry()
709
710            self.end()
711            return True
712
713    def end(self):
714        self.down_pos = None
715        self.arrow_item = None
716        self.annotation = None
717        self.document.view().setCursor(Qt.ArrowCursor)
718        UserInteraction.end(self)
719
720
721def rect_to_tuple(rect):
722    return rect.x(), rect.y(), rect.width(), rect.height()
723
724
725class NewTextAnnotation(UserInteraction):
726    def __init__(self, document, *args, **kwargs):
727        UserInteraction.__init__(self, document, *args, **kwargs)
728        self.down_pos = None
729        self.annotation_item = None
730        self.annotation = None
731        self.control = None
732
733    def start(self):
734        self.document.view().setCursor(Qt.CrossCursor)
735        UserInteraction.start(self)
736
737    def createNewAnnotation(self, rect):
738        """Create a new TextAnnotation at with `rect` as the geometry.
739        """
740        annot = scheme.SchemeTextAnnotation(rect_to_tuple(rect))
741
742        item = self.scene.add_annotation(annot)
743        item.setTextInteractionFlags(Qt.TextEditorInteraction)
744        item.setFramePen(QPen(Qt.DashLine))
745
746        self.annotation_item = item
747        self.annotation = annot
748        self.control = controlpoints.ControlPointRect()
749        self.control.rectChanged.connect(
750            self.annotation_item.setGeometry
751        )
752        self.scene.addItem(self.control)
753
754    def mousePressEvent(self, event):
755        if event.button() == Qt.LeftButton:
756            self.down_pos = event.scenePos()
757            return True
758
759    def mouseMoveEvent(self, event):
760        if event.buttons() & Qt.LeftButton:
761            if self.annotation_item is None and \
762                    (self.down_pos - event.scenePos()).manhattanLength() > \
763                    QApplication.instance().startDragDistance():
764                rect = QRectF(self.down_pos, event.scenePos()).normalized()
765                self.createNewAnnotation(rect)
766
767            if self.annotation_item is not None:
768                rect = QRectF(self.down_pos, event.scenePos()).normalized()
769                self.control.setRect(rect)
770
771            return True
772
773    def mouseReleaseEvent(self, event):
774        if event.button() == Qt.LeftButton:
775            if self.annotation_item is None:
776                self.createNewAnnotation(QRectF(event.scenePos(),
777                                                event.scenePos()))
778                rect = self.defaultTextGeometry(event.scenePos())
779
780            else:
781                rect = QRectF(self.down_pos, event.scenePos()).normalized()
782
783            # Commit the annotation to the scheme.
784            self.annotation.rect = rect_to_tuple(rect)
785
786            self.document.addAnnotation(self.annotation)
787
788            self.annotation_item.setGeometry(rect)
789
790            self.control.rectChanged.disconnect(
791                self.annotation_item.setGeometry
792            )
793            self.control.hide()
794
795            # Move the focus to the editor.
796            self.annotation_item.setFramePen(QPen(Qt.NoPen))
797            self.annotation_item.setFocus(Qt.OtherFocusReason)
798            self.annotation_item.startEdit()
799
800            self.end()
801
802    def defaultTextGeometry(self, point):
803        """Return the default text geometry. Used in case the user
804        single clicked in the scene.
805
806        """
807        font = self.annotation_item.font()
808        metrics = QFontMetrics(font)
809        spacing = metrics.lineSpacing()
810        margin = self.annotation_item.document().documentMargin()
811
812        rect = QRectF(QPointF(point.x(), point.y() - spacing - margin),
813                      QSizeF(150, spacing + 2 * margin))
814        return rect
815
816    def end(self):
817        if self.control is not None:
818            self.scene.removeItem(self.control)
819
820        self.control = None
821        self.down_pos = None
822        self.annotation_item = None
823        self.annotation = None
824        self.document.view().setCursor(Qt.ArrowCursor)
825        UserInteraction.end(self)
826
827
828class ResizeTextAnnotation(UserInteraction):
829    def __init__(self, document, *args, **kwargs):
830        UserInteraction.__init__(self, document, *args, **kwargs)
831        self.item = None
832        self.annotation = None
833        self.control = None
834        self.savedFramePen = None
835        self.savedRect = None
836
837    def mousePressEvent(self, event):
838        pos = event.scenePos()
839        if self.item is None:
840            item = self.scene.item_at(pos, items.TextAnnotation)
841            if item is not None and not item.hasFocus():
842                self.editItem(item)
843                return False
844
845        return UserInteraction.mousePressEvent(self, event)
846
847    def editItem(self, item):
848        annotation = self.scene.annotation_for_item(item)
849        rect = item.geometry()  # TODO: map to scene if item has a parent.
850        control = controlpoints.ControlPointRect(rect=rect)
851        self.scene.addItem(control)
852
853        self.savedFramePen = item.framePen()
854        self.savedRect = rect
855
856        control.rectEdited.connect(item.setGeometry)
857        control.setFocusProxy(item)
858
859        item.setFramePen(QPen(Qt.DashDotLine))
860        item.geometryChanged.connect(self.__on_textGeometryChanged)
861
862        self.item = item
863
864        self.annotation = annotation
865        self.control = control
866
867    def commit(self):
868        """Commit the current item geometry state to the document.
869        """
870        rect = self.item.geometry()
871        if self.savedRect != rect:
872            command = commands.SetAttrCommand(
873                self.annotation, "rect",
874                (rect.x(), rect.y(), rect.width(), rect.height()),
875                name="Edit text geometry"
876            )
877            self.document.undoStack().push(command)
878            self.savedRect = rect
879
880    def __on_editingFinished(self):
881        self.commit()
882        self.end()
883
884    def __on_rectEdited(self, rect):
885        self.item.setGeometry(rect)
886
887    def __on_textGeometryChanged(self):
888        if not self.control.isControlActive():
889            rect = self.item.geometry()
890            self.control.setRect(rect)
891
892    def cancel(self, reason=UserInteraction.OtherReason):
893        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
894        if self.item is not None and self.savedRect is not None:
895            self.item.setGeometry(self.savedRect)
896
897        UserInteraction.cancel(self, reason)
898
899    def end(self):
900        if self.control is not None:
901            self.scene.removeItem(self.control)
902
903        if self.item is not None:
904            self.item.setFramePen(self.savedFramePen)
905
906        self.item = None
907        self.annotation = None
908        self.control = None
909
910        UserInteraction.end(self)
911
912
913class ResizeArrowAnnotation(UserInteraction):
914    def __init__(self, document, *args, **kwargs):
915        UserInteraction.__init__(self, document, *args, **kwargs)
916        self.item = None
917        self.annotation = None
918        self.control = None
919        self.savedLine = None
920
921    def mousePressEvent(self, event):
922        pos = event.scenePos()
923        if self.item is None:
924            item = self.scene.item_at(pos, items.ArrowAnnotation)
925            if item is not None and not item.hasFocus():
926                self.editItem(item)
927                return False
928
929        return UserInteraction.mousePressEvent(self, event)
930
931    def editItem(self, item):
932        annotation = self.scene.annotation_for_item(item)
933        control = controlpoints.ControlPointLine()
934        self.scene.addItem(control)
935
936        line = item.line()
937        self.savedLine = line
938
939        p1, p2 = map(item.mapToScene, (line.p1(), line.p2()))
940
941        control.setLine(QLineF(p1, p2))
942        control.setFocusProxy(item)
943        control.lineEdited.connect(self.__on_lineEdited)
944
945        item.geometryChanged.connect(self.__on_lineGeometryChanged)
946
947        self.item = item
948        self.annotation = annotation
949        self.control = control
950
951    def commit(self):
952        """Commit the current geometry of the item to the document.
953
954        .. note:: Does nothing if the actual geometry is not changed.
955
956        """
957        line = self.control.line()
958        p1, p2 = line.p1(), line.p2()
959
960        if self.item.line() != self.savedLine:
961            command = commands.SetAttrCommand(
962                self.annotation,
963                "geometry",
964                ((p1.x(), p1.y()), (p2.x(), p2.y())),
965                name="Edit arrow geometry",
966            )
967            self.document.undoStack().push(command)
968            self.savedLine = self.item.line()
969
970    def __on_editingFinished(self):
971        self.commit()
972        self.end()
973
974    def __on_lineEdited(self, line):
975        p1, p2 = map(self.item.mapFromScene, (line.p1(), line.p2()))
976        self.item.setLine(QLineF(p1, p2))
977        self.item.adjustGeometry()
978
979    def __on_lineGeometryChanged(self):
980        # Possible geometry change from out of our control, for instance
981        # item move as a part of a selection group.
982        if not self.control.isControlActive():
983            line = self.item.line()
984            p1, p2 = map(self.item.mapToScene, (line.p1(), line.p2()))
985            self.control.setLine(QLineF(p1, p2))
986
987    def cancel(self, reason=UserInteraction.OtherReason):
988        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
989        if self.item is not None and self.savedLine is not None:
990            self.item.setLine(self.savedLine)
991
992        UserInteraction.cancel(self, reason)
993
994    def end(self):
995        if self.control is not None:
996            self.scene.removeItem(self.control)
997
998        if self.item is not None:
999            self.item.geometryChanged.disconnect(self.__on_lineGeometryChanged)
1000
1001        self.control = None
1002        self.item = None
1003        self.annotation = None
1004
1005        UserInteraction.end(self)
Note: See TracBrowser for help on using the repository browser.