[ui] UIGraph: add hoveredNode property + node hovering visual feedback

* keep track of currently hovered node in UIGraph on Python side
* Node: show border on hover + make MouseArea contain everything else to always get hover events, even when cursor is over children attribute pins
This commit is contained in:
Yann Lanthony 2018-12-06 19:13:17 +01:00
parent 5b991053a8
commit 05854ed897
3 changed files with 124 additions and 104 deletions

View file

@ -202,6 +202,7 @@ class UIGraph(QObject):
self._sortedDFSChunks = QObjectListModel(parent=self) self._sortedDFSChunks = QObjectListModel(parent=self)
self._layout = GraphLayout(self) self._layout = GraphLayout(self)
self._selectedNode = None self._selectedNode = None
self._hoveredNode = None
if filepath: if filepath:
self.load(filepath) self.load(filepath)
@ -236,6 +237,7 @@ class UIGraph(QObject):
def clear(self): def clear(self):
if self._graph: if self._graph:
self.clearNodeHover()
self.clearNodeSelection() self.clearNodeSelection()
self._graph.deleteLater() self._graph.deleteLater()
self._graph = None self._graph = None
@ -472,6 +474,10 @@ class UIGraph(QObject):
""" Clear node selection. """ """ Clear node selection. """
self.selectedNode = None self.selectedNode = None
def clearNodeHover(self):
""" Reset currently hovered node to None. """
self.hoveredNode = None
undoStack = Property(QObject, lambda self: self._undoStack, constant=True) undoStack = Property(QObject, lambda self: self._undoStack, constant=True)
graphChanged = Signal() graphChanged = Signal()
graph = Property(Graph, lambda self: self._graph, notify=graphChanged) graph = Property(Graph, lambda self: self._graph, notify=graphChanged)
@ -490,3 +496,7 @@ class UIGraph(QObject):
selectedNodeChanged = Signal() selectedNodeChanged = Signal()
# Currently selected node # Currently selected node
selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, clearNodeSelection) selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, clearNodeSelection)
hoveredNodeChanged = Signal()
# Currently hovered node
hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, clearNodeHover)

View file

@ -308,6 +308,7 @@ Item {
width: uigraph.layout.nodeWidth width: uigraph.layout.nodeWidth
readOnly: root.readOnly readOnly: root.readOnly
selected: uigraph.selectedNode === node selected: uigraph.selectedNode === node
hovered: uigraph.hoveredNode === node
onSelectedChanged: if(selected) forceActiveFocus() onSelectedChanged: if(selected) forceActiveFocus()
onAttributePinCreated: registerAttributePin(attribute, pin) onAttributePinCreated: registerAttributePin(attribute, pin)
@ -331,6 +332,9 @@ Item {
onMoved: uigraph.moveNode(node, position) onMoved: uigraph.moveNode(node, position)
onEntered: uigraph.hoveredNode = node
onExited: uigraph.hoveredNode = null
Keys.onDeletePressed: uigraph.removeNode(node) Keys.onDeletePressed: uigraph.removeNode(node)
Behavior on x { Behavior on x {

View file

@ -13,14 +13,17 @@ Item {
readonly property bool isCompatibilityNode: node.hasOwnProperty("compatibilityIssue") readonly property bool isCompatibilityNode: node.hasOwnProperty("compatibilityIssue")
readonly property color defaultColor: isCompatibilityNode ? "#444" : "#607D8B" readonly property color defaultColor: isCompatibilityNode ? "#444" : "#607D8B"
property bool selected: false property bool selected: false
property bool hovered: false
signal pressed(var mouse) signal pressed(var mouse)
signal doubleClicked(var mouse) signal doubleClicked(var mouse)
signal moved(var position) signal moved(var position)
signal entered()
signal exited()
signal attributePinCreated(var attribute, var pin) signal attributePinCreated(var attribute, var pin)
signal attributePinDeleted(var attribute, var pin) signal attributePinDeleted(var attribute, var pin)
implicitHeight: body.height implicitHeight: childrenRect.height
objectName: node.name objectName: node.name
SystemPalette { id: activePalette } SystemPalette { id: activePalette }
@ -39,7 +42,8 @@ Item {
} }
MouseArea { MouseArea {
anchors.fill: parent width: parent.width
height: body.height
drag.target: parent drag.target: parent
// small drag threshold to avoid moving the node by mistake // small drag threshold to avoid moving the node by mistake
drag.threshold: 2 drag.threshold: 2
@ -47,128 +51,130 @@ Item {
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: root.pressed(mouse) onPressed: root.pressed(mouse)
onDoubleClicked: root.doubleClicked(mouse) onDoubleClicked: root.doubleClicked(mouse)
onEntered: root.entered()
onExited: root.exited()
drag.onActiveChanged: { drag.onActiveChanged: {
if(!drag.active) if(!drag.active)
root.moved(Qt.point(root.x, root.y)) root.moved(Qt.point(root.x, root.y))
} }
}
// Selection border // Selection border
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: -border.width anchors.margins: -border.width
visible: root.selected visible: root.selected || root.hovered
border.width: 2.5 border.width: 2.5
border.color: activePalette.highlight border.color: root.selected ? activePalette.highlight : Qt.darker(activePalette.highlight, 1.5)
opacity: 0.9 opacity: 0.9
color: "transparent" color: "transparent"
} }
Rectangle { Rectangle {
id: background id: background
anchors.fill: parent anchors.fill: parent
color: activePalette.base color: activePalette.base
layer.enabled: true layer.enabled: true
layer.effect: DropShadow { radius: 2; color: shadowColor } layer.effect: DropShadow { radius: 2; color: shadowColor }
} }
Column { Column {
id: body id: body
width: parent.width
Label {
width: parent.width width: parent.width
horizontalAlignment: Text.AlignHCenter
padding: 4 Label {
text: node.label width: parent.width
color: "#EEE" horizontalAlignment: Text.AlignHCenter
font.pointSize: 8 padding: 4
background: Rectangle { text: node.label
color: root.baseColor color: "#EEE"
font.pointSize: 8
background: Rectangle {
color: root.baseColor
}
} }
}
// Node Chunks // Node Chunks
NodeChunks { NodeChunks {
defaultColor: Qt.darker(baseColor, 1.3) defaultColor: Qt.darker(baseColor, 1.3)
implicitHeight: 3 implicitHeight: 3
width: parent.width width: parent.width
model: node.chunks model: node.chunks
} }
Item { width: 1; height: 2} Item { width: 1; height: 2}
Item { Item {
width: parent.width + 6 width: parent.width + 6
height: childrenRect.height height: childrenRect.height
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
Column { Column {
id: inputs id: inputs
width: parent.width / 2 width: parent.width / 2
spacing: 1 spacing: 1
Repeater { Repeater {
model: node.attributes model: node.attributes
delegate: Loader { delegate: Loader {
active: !object.isOutput && object.type == "File" active: !object.isOutput && object.type == "File"
|| (object.type == "ListAttribute" && object.desc.elementDesc.type == "File") // TODO: review this || (object.type == "ListAttribute" && object.desc.elementDesc.type == "File") // TODO: review this
width: inputs.width width: inputs.width
sourceComponent: AttributePin { sourceComponent: AttributePin {
id: inPin id: inPin
nodeItem: root nodeItem: root
attribute: object attribute: object
readOnly: root.readOnly readOnly: root.readOnly
Component.onCompleted: attributePinCreated(attribute, inPin) Component.onCompleted: attributePinCreated(attribute, inPin)
Component.onDestruction: attributePinDeleted(attribute, inPin) Component.onDestruction: attributePinDeleted(attribute, inPin)
onPressed: root.pressed(mouse) onPressed: root.pressed(mouse)
onChildPinCreated: attributePinCreated(childAttribute, inPin) onChildPinCreated: attributePinCreated(childAttribute, inPin)
onChildPinDeleted: attributePinDeleted(childAttribute, inPin) onChildPinDeleted: attributePinDeleted(childAttribute, inPin)
}
}
}
}
Column {
id: outputs
width: parent.width / 2
anchors.right: parent.right
spacing: 1
Repeater {
model: node.attributes
delegate: Loader {
active: object.isOutput
anchors.right: parent.right
width: outputs.width
sourceComponent: AttributePin {
id: outPin
nodeItem: root
attribute: object
readOnly: root.readOnly
onPressed: root.pressed(mouse)
Component.onCompleted: attributePinCreated(object, outPin)
Component.onDestruction: attributePinDeleted(attribute, outPin)
}
} }
} }
} }
} }
Column { Item { width: 1; height: 2}
id: outputs }
width: parent.width / 2
anchors.right: parent.right
spacing: 1
Repeater {
model: node.attributes
delegate: Loader { // CompatibilityBadge icon for CompatibilityNodes
active: object.isOutput Loader {
anchors.right: parent.right active: root.isCompatibilityNode
width: outputs.width anchors {
right: parent.right
sourceComponent: AttributePin { top: parent.top
id: outPin margins: -4
nodeItem: root }
attribute: object sourceComponent: CompatibilityBadge {
readOnly: root.readOnly sourceComponent: iconDelegate
onPressed: root.pressed(mouse) canUpgrade: root.node.canUpgrade
Component.onCompleted: attributePinCreated(object, outPin) issueDetails: root.node.issueDetails
Component.onDestruction: attributePinDeleted(attribute, outPin)
}
}
}
} }
}
Item { width: 1; height: 2}
}
// CompatibilityBadge icon for CompatibilityNodes
Loader {
active: root.isCompatibilityNode
anchors {
right: parent.right
top: parent.top
margins: -4
}
sourceComponent: CompatibilityBadge {
sourceComponent: iconDelegate
canUpgrade: root.node.canUpgrade
issueDetails: root.node.issueDetails
} }
} }
} }