source: orange/Orange/OrangeCanvas/document/interactions.py @ 11201:04262f6c3392

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

Added annotation action menus for font size and arrow color.

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