Skip to content

Commit e6c793a

Browse files
authored
Merge pull request #3352 from irgolic/edit-links-redesign
Edit Links window redesign
2 parents fe61735 + e85a227 commit e6c793a

File tree

1 file changed

+178
-38
lines changed

1 file changed

+178
-38
lines changed

Orange/canvas/document/editlinksdialog.py

Lines changed: 178 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@
1616
QGraphicsView, QGraphicsWidget, QGraphicsRectItem,
1717
QGraphicsLineItem, QGraphicsTextItem, QGraphicsLayoutItem,
1818
QGraphicsLinearLayout, QGraphicsGridLayout, QGraphicsPixmapItem,
19-
QGraphicsDropShadowEffect, QSizePolicy
19+
QGraphicsDropShadowEffect, QSizePolicy, QGraphicsItem
20+
)
21+
from AnyQt.QtGui import (
22+
QPalette, QPen, QPainter, QIcon, QColor, QPainterPathStroker
2023
)
21-
from AnyQt.QtGui import QPalette, QPen, QPainter, QIcon
22-
2324
from AnyQt.QtCore import (
24-
Qt, QObject, QSize, QSizeF, QPointF, QRectF, QT_VERSION
25+
Qt, QObject, QSize, QSizeF, QPointF, QRectF, QT_VERSION,
2526
)
2627
from AnyQt.QtCore import pyqtSignal as Signal
2728

2829
from ..scheme import compatible_channels
29-
# 'Unused' imports are used in docstrings
30-
from ..registry import InputSignal, OutputSignal # pylint: disable=unused-import
30+
from ..registry import InputSignal, OutputSignal
3131

3232
from ..resources import icon_loader
3333

@@ -120,6 +120,7 @@ def __onGeometryChanged(self):
120120
left, top, right, bottom = self.getContentsMargins()
121121
self.view.setFixedSize(size.toSize() + \
122122
QSize(left + right + 4, top + bottom + 4))
123+
self.view.setSceneRect(self.scene.editWidget.geometry())
123124

124125

125126
def find_item_at(scene, pos, order=Qt.DescendingOrder, type=None,
@@ -141,8 +142,7 @@ def find_item_at(scene, pos, order=Qt.DescendingOrder, type=None,
141142
item.objectName() != name:
142143
continue
143144
return item
144-
else:
145-
return None
145+
return None
146146

147147

148148
class LinksEditScene(QGraphicsScene):
@@ -251,10 +251,11 @@ def mousePressEvent(self, event):
251251
if event.button() == Qt.LeftButton:
252252
startItem = find_item_at(self.scene(), event.pos(),
253253
type=ChannelAnchor)
254-
if startItem is not None:
254+
if startItem is not None and startItem.isEnabled():
255255
# Start a connection line drag.
256256
self.__dragStartItem = startItem
257257
self.__tmpLine = None
258+
258259
event.accept()
259260
return
260261

@@ -278,11 +279,12 @@ def mouseMoveEvent(self, event):
278279
(downPos - event.pos()).manhattanLength() > \
279280
QApplication.instance().startDragDistance():
280281
# Start a line drag
281-
line = QGraphicsLineItem(self)
282+
line = LinkLineItem(self)
282283
start = self.__dragStartItem.boundingRect().center()
283284
start = self.mapFromItem(self.__dragStartItem, start)
284-
line.setLine(start.x(), start.y(),
285-
event.pos().x(), event.pos().y())
285+
286+
eventPos = event.pos()
287+
line.setLine(start.x(), start.y(), eventPos.x(), eventPos.y())
286288

287289
pen = QPen(self.palette().color(QPalette.Foreground), 4)
288290
pen.setCapStyle(Qt.RoundCap)
@@ -291,16 +293,35 @@ def mouseMoveEvent(self, event):
291293

292294
self.__tmpLine = line
293295

296+
if self.__dragStartItem in self.sourceNodeWidget.channelAnchors:
297+
for anchor in self.sinkNodeWidget.channelAnchors:
298+
self.__updateAnchorState(anchor, [self.__dragStartItem])
299+
else:
300+
for anchor in self.sourceNodeWidget.channelAnchors:
301+
self.__updateAnchorState(anchor, [self.__dragStartItem])
302+
294303
if self.__tmpLine:
295304
# Update the temp line
296305
line = self.__tmpLine.line()
297-
line.setP2(event.pos())
306+
307+
maybe_anchor = find_item_at(self.scene(), event.scenePos(),
308+
type=ChannelAnchor)
309+
# If hovering over anchor
310+
if maybe_anchor is not None and maybe_anchor.isEnabled():
311+
target_pos = maybe_anchor.boundingRect().center()
312+
target_pos = self.mapFromItem(maybe_anchor, target_pos)
313+
line.setP2(target_pos)
314+
else:
315+
target_pos = event.pos()
316+
line.setP2(target_pos)
317+
298318
self.__tmpLine.setLine(line)
299319

300320
QGraphicsWidget.mouseMoveEvent(self, event)
301321

302322
def mouseReleaseEvent(self, event):
303323
if event.button() == Qt.LeftButton and self.__tmpLine:
324+
self.__resetAnchorStates()
304325
endItem = find_item_at(self.scene(), event.scenePos(),
305326
type=ChannelAnchor)
306327

@@ -312,6 +333,7 @@ def mouseReleaseEvent(self, event):
312333

313334
# Make sure the drag was from input to output (or reversed) and
314335
# not between input -> input or output -> output
336+
# pylint: disable=unidiomatic-typecheck
315337
if type(startChannel) != type(endChannel):
316338
if isinstance(startChannel, InputSignal):
317339
startChannel, endChannel = endChannel, startChannel
@@ -350,7 +372,7 @@ def addLink(self, output, input):
350372
if s2 == input:
351373
self.removeLink(s1, s2)
352374

353-
line = QGraphicsLineItem(self)
375+
line = LinkLineItem(self)
354376

355377
source_anchor = self.sourceNodeWidget.anchor(output)
356378
sink_anchor = self.sinkNodeWidget.anchor(input)
@@ -360,11 +382,7 @@ def addLink(self, output, input):
360382

361383
sink_pos = sink_anchor.boundingRect().center()
362384
sink_pos = self.mapFromItem(sink_anchor, sink_pos)
363-
line.setLine(source_pos.x(), source_pos.y(),
364-
sink_pos.x(), sink_pos.y())
365-
pen = QPen(self.palette().color(QPalette.Foreground), 4)
366-
pen.setCapStyle(Qt.RoundCap)
367-
line.setPen(pen)
385+
line.setLine(source_pos.x(), source_pos.y(), sink_pos.x(), sink_pos.y())
368386

369387
self.__links.append(_Link(output, input, line))
370388

@@ -450,6 +468,61 @@ def __updateState(self):
450468
self.sourceNodeTitle = left_title
451469
self.sinkNodeTitle = right_title
452470

471+
self.__resetAnchorStates()
472+
473+
# AnchorHover hover over anchor before hovering over line
474+
class AnchorHover(QGraphicsRectItem):
475+
def __init__(self, anchor, parent=None):
476+
QGraphicsRectItem.__init__(self, parent=parent)
477+
self.setAcceptHoverEvents(True)
478+
479+
self.anchor = anchor
480+
self.setRect(anchor.boundingRect())
481+
482+
self.setPos(self.mapFromScene(anchor.scenePos()))
483+
self.setFlag(QGraphicsItem.ItemHasNoContents, True)
484+
485+
def hoverEnterEvent(self, event):
486+
if self.anchor.isEnabled():
487+
self.anchor.hoverEnterEvent(event)
488+
else:
489+
event.ignore()
490+
491+
def hoverLeaveEvent(self, event):
492+
if self.anchor.isEnabled():
493+
self.anchor.hoverLeaveEvent(event)
494+
else:
495+
event.ignore()
496+
497+
for anchor in left_node.channelAnchors + right_node.channelAnchors:
498+
anchor_hover = AnchorHover(anchor, parent=self)
499+
anchor_hover.setZValue(2.0)
500+
501+
def __resetAnchorStates(self):
502+
source_anchors = self.sourceNodeWidget.channelAnchors
503+
sink_anchors = self.sinkNodeWidget.channelAnchors
504+
for anchor in source_anchors:
505+
self.__updateAnchorState(anchor, sink_anchors)
506+
for anchor in sink_anchors:
507+
self.__updateAnchorState(anchor, source_anchors)
508+
509+
def __updateAnchorState(self, anchor, opposite_anchors):
510+
first_channel = anchor.channel()
511+
for opposite_anchor in opposite_anchors:
512+
second_channel = opposite_anchor.channel()
513+
if isinstance(first_channel, OutputSignal) and \
514+
compatible_channels(first_channel, second_channel) or \
515+
isinstance(first_channel, InputSignal) and \
516+
compatible_channels(second_channel, first_channel):
517+
anchor.setEnabled(True)
518+
anchor.setToolTip("Click and drag to connect widgets!")
519+
return
520+
if isinstance(first_channel, OutputSignal):
521+
anchor.setToolTip("No compatible input channel.")
522+
else:
523+
anchor.setToolTip("No compatible output channel.")
524+
anchor.setEnabled(False)
525+
453526
if QT_VERSION < 0x40700:
454527
geometryChanged = Signal()
455528

@@ -490,7 +563,7 @@ def __init__(self, parent=None, direction=Qt.LeftToRight,
490563
self.__iconLayoutItem = GraphicsItemLayoutItem(item=self.__iconItem)
491564

492565
self.__channelLayout = QGraphicsGridLayout()
493-
self.__channelAnchors = []
566+
self.channelAnchors = []
494567

495568
if self.__direction == Qt.LeftToRight:
496569
self.layout().addItem(self.__iconLayoutItem)
@@ -576,7 +649,7 @@ def setSchemeNode(self, node):
576649
label_row = 1
577650
anchor_row = 0
578651

579-
self.__channelAnchors = []
652+
self.channelAnchors = []
580653
grid = self.__channelLayout
581654

582655
for i, channel in enumerate(channels):
@@ -595,20 +668,17 @@ def setSchemeNode(self, node):
595668
anchor = ChannelAnchor(self, channel=channel,
596669
rect=QRectF(0, 0, 20, 20))
597670

598-
anchor.setBrush(self.palette().brush(QPalette.Mid))
599-
600671
layout_item = GraphicsItemLayoutItem(grid, item=anchor)
601672
grid.addItem(layout_item, i, anchor_row,
602673
alignment=anchor_alignment)
603-
anchor.setToolTip(escape(channel.type))
604674

605-
self.__channelAnchors.append(anchor)
675+
self.channelAnchors.append(anchor)
606676

607677
def anchor(self, channel):
608678
"""
609679
Return the anchor item for the `channel` name.
610680
"""
611-
for anchor in self.__channelAnchors:
681+
for anchor in self.channelAnchors:
612682
if anchor.channel() == channel:
613683
return anchor
614684

@@ -672,10 +742,12 @@ class ChannelAnchor(QGraphicsRectItem):
672742
"""
673743
def __init__(self, parent=None, channel=None, rect=None, **kwargs):
674744
QGraphicsRectItem.__init__(self, **kwargs)
675-
self.setAcceptHoverEvents(True)
676745
self.setAcceptedMouseButtons(Qt.NoButton)
677746
self.__channel = None
678747

748+
# AnchorHover handles hover events
749+
# self.setAcceptHoverEvents(True)
750+
679751
if rect is None:
680752
rect = QRectF(0, 0, 20, 20)
681753

@@ -684,10 +756,13 @@ def __init__(self, parent=None, channel=None, rect=None, **kwargs):
684756
if channel:
685757
self.setChannel(channel)
686758

687-
self.__shadow = QGraphicsDropShadowEffect(blurRadius=5,
688-
offset=QPointF(0, 0))
689-
self.setGraphicsEffect(self.__shadow)
690-
self.__shadow.setEnabled(False)
759+
self.__default_pen = QPen(QColor('#000000'), 1)
760+
self.__hover_pen = QPen(QColor('#000000'), 2)
761+
self.setPen(self.__default_pen)
762+
763+
self.enabledBrush = QColor('#FFFFFF')
764+
self.disabledBrush = QColor('#BBBBBB')
765+
self.setBrush(self.enabledBrush)
691766

692767
def setChannel(self, channel):
693768
"""
@@ -696,23 +771,32 @@ def setChannel(self, channel):
696771
if channel != self.__channel:
697772
self.__channel = channel
698773

699-
if hasattr(channel, "description"):
700-
self.setToolTip(channel.description)
701-
# TODO: Should also include name, type, flags, dynamic in the
702-
# tool tip as well as add visual clues to the anchor
703-
704774
def channel(self):
705775
"""
706776
Return the channel description.
707777
"""
708778
return self.__channel
709779

780+
def setEnabled(self, enabled):
781+
QGraphicsRectItem.setEnabled(self, enabled)
782+
if enabled:
783+
self.setBrush(self.enabledBrush)
784+
else:
785+
self.setBrush(self.disabledBrush)
786+
787+
def paint(self, painter, option, widget=None):
788+
QGraphicsRectItem.paint(self, painter, option, widget)
789+
# if disabled, draw X over box
790+
if not self.isEnabled():
791+
painter.drawLine(self.rect().topLeft(), self.rect().bottomRight())
792+
painter.drawLine(self.rect().topRight(), self.rect().bottomLeft())
793+
710794
def hoverEnterEvent(self, event):
711-
self.__shadow.setEnabled(True)
795+
self.setPen(self.__hover_pen)
712796
QGraphicsRectItem.hoverEnterEvent(self, event)
713797

714798
def hoverLeaveEvent(self, event):
715-
self.__shadow.setEnabled(False)
799+
self.setPen(self.__default_pen)
716800
QGraphicsRectItem.hoverLeaveEvent(self, event)
717801

718802

@@ -783,3 +867,59 @@ def setDocument(self, doc):
783867
def _onDocumentSizeChanged(self, size):
784868
"""The doc size has changed"""
785869
self.updateGeometry()
870+
871+
872+
class LinkLineItem(QGraphicsLineItem):
873+
"""
874+
A line connecting two Channel Anchors.
875+
"""
876+
877+
def __init__(self, parent=None):
878+
QGraphicsLineItem.__init__(self, parent)
879+
self.setAcceptHoverEvents(True)
880+
881+
self.__shape = None
882+
883+
self.__default_pen = QPen(QColor('#383838'), 4)
884+
self.__default_pen.setCapStyle(Qt.RoundCap)
885+
self.__hover_pen = QPen(QColor('#000000'), 4)
886+
self.__hover_pen.setCapStyle(Qt.RoundCap)
887+
self.setPen(self.__default_pen)
888+
889+
self.__shadow = QGraphicsDropShadowEffect(
890+
blurRadius=10, color=QColor('#9CACB4'),
891+
offset=QPointF(0, 0)
892+
)
893+
894+
self.setGraphicsEffect(self.__shadow)
895+
self.prepareGeometryChange()
896+
self.__shadow.setEnabled(False)
897+
898+
def setLine(self, *args, **kwargs):
899+
super().setLine(*args, **kwargs)
900+
901+
# extends mouse hit area
902+
stroke_path = QPainterPathStroker()
903+
stroke_path.setCapStyle(Qt.RoundCap)
904+
905+
stroke_path.setWidth(10)
906+
self.__shape = stroke_path.createStroke(super().shape())
907+
908+
def shape(self):
909+
if self.__shape is None:
910+
return QGraphicsLineItem.shape(self)
911+
return self.__shape
912+
913+
def hoverEnterEvent(self, event):
914+
self.prepareGeometryChange()
915+
self.__shadow.setEnabled(True)
916+
self.setPen(self.__hover_pen)
917+
self.setZValue(1.0)
918+
QGraphicsLineItem.hoverEnterEvent(self, event)
919+
920+
def hoverLeaveEvent(self, event):
921+
self.prepareGeometryChange()
922+
self.__shadow.setEnabled(False)
923+
self.setPen(self.__default_pen)
924+
self.setZValue(0.0)
925+
QGraphicsLineItem.hoverLeaveEvent(self, event)

0 commit comments

Comments
 (0)