Merge pull request #89 from alicevision/dev_attributeEditor

Improve AttributeEditor UI and features
This commit is contained in:
Fabien Castan 2018-02-07 11:48:21 +01:00 committed by GitHub
commit 4a2d8cdab8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 367 additions and 214 deletions

View file

@ -258,8 +258,11 @@ class Attribute(BaseObject):
return '"{}"'.format(self.value) return '"{}"'.format(self.value)
return str(self.value) return str(self.value)
def isDefault(self): def defaultValue(self):
return self._value == self.desc.value return self.desc.value
def _isDefault(self):
return self._value == self.defaultValue()
def getPrimitiveValue(self, exportDefault=True): def getPrimitiveValue(self, exportDefault=True):
return self._value return self._value
@ -273,6 +276,7 @@ class Attribute(BaseObject):
isOutput = Property(bool, isOutput.fget, constant=True) isOutput = Property(bool, isOutput.fget, constant=True)
isLinkChanged = Signal() isLinkChanged = Signal()
isLink = Property(bool, isLink.fget, notify=isLinkChanged) isLink = Property(bool, isLink.fget, notify=isLinkChanged)
isDefault = Property(bool, _isDefault, notify=valueChanged)
class ListAttribute(Attribute): class ListAttribute(Attribute):
@ -289,8 +293,7 @@ class ListAttribute(Attribute):
def _set_value(self, value): def _set_value(self, value):
self._value.clear() self._value.clear()
if value: self.extend(value)
self.extend(value)
self.requestGraphUpdate() self.requestGraphUpdate()
def append(self, value): def append(self, value):
@ -300,6 +303,7 @@ class ListAttribute(Attribute):
values = value if isinstance(value, list) else [value] values = value if isinstance(value, list) else [value]
attrs = [attribute_factory(self.attributeDesc.elementDesc, v, self.isOutput, self.node, self) for v in values] attrs = [attribute_factory(self.attributeDesc.elementDesc, v, self.isOutput, self.node, self) for v in values]
self._value.insert(index, attrs) self._value.insert(index, attrs)
self.valueChanged.emit()
self._applyExpr() self._applyExpr()
self.requestGraphUpdate() self.requestGraphUpdate()
@ -317,6 +321,7 @@ class ListAttribute(Attribute):
self.node.graph.removeEdge(attr) # delete edge if the attribute is linked self.node.graph.removeEdge(attr) # delete edge if the attribute is linked
self._value.removeAt(index, count) self._value.removeAt(index, count)
self.requestGraphUpdate() self.requestGraphUpdate()
self.valueChanged.emit()
def uid(self, uidIndex): def uid(self, uidIndex):
uids = [] uids = []
@ -334,20 +339,24 @@ class ListAttribute(Attribute):
def getExportValue(self): def getExportValue(self):
return [attr.getExportValue() for attr in self._value] return [attr.getExportValue() for attr in self._value]
def isDefault(self): def defaultValue(self):
return bool(self._value) return []
def _isDefault(self):
return len(self._value) == 0
def getPrimitiveValue(self, exportDefault=True): def getPrimitiveValue(self, exportDefault=True):
if exportDefault: if exportDefault:
return [attr.getPrimitiveValue(exportDefault=exportDefault) for attr in self._value] return [attr.getPrimitiveValue(exportDefault=exportDefault) for attr in self._value]
else: else:
return [attr.getPrimitiveValue(exportDefault=exportDefault) for attr in self._value if not attr.isDefault()] return [attr.getPrimitiveValue(exportDefault=exportDefault) for attr in self._value if not attr.isDefault]
def getValueStr(self): def getValueStr(self):
return self.attributeDesc.joinChar.join([v.getValueStr() for v in self._value]) return self.attributeDesc.joinChar.join([v.getValueStr() for v in self._value])
# Override value property setter # Override value property setter
value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged) value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged)
isDefault = Property(bool, _isDefault, notify=Attribute.valueChanged)
class GroupAttribute(Attribute): class GroupAttribute(Attribute):
@ -360,6 +369,7 @@ class GroupAttribute(Attribute):
for subAttrDesc in self.attributeDesc.groupDesc: for subAttrDesc in self.attributeDesc.groupDesc:
childAttr = attribute_factory(subAttrDesc, None, self.isOutput, self.node, self) childAttr = attribute_factory(subAttrDesc, None, self.isOutput, self.node, self)
subAttributes.append(childAttr) subAttributes.append(childAttr)
childAttr.valueChanged.connect(self.valueChanged)
self._value.reset(subAttributes) self._value.reset(subAttributes)
@ -391,20 +401,24 @@ class GroupAttribute(Attribute):
def getExportValue(self): def getExportValue(self):
return {key: attr.getExportValue() for key, attr in self._value.objects.items()} return {key: attr.getExportValue() for key, attr in self._value.objects.items()}
def isDefault(self): def _isDefault(self):
return len(self._value) == 0 return all(v.isDefault for v in self._value)
def defaultValue(self):
return {key: attr.defaultValue() for key, attr in self._value.items()}
def getPrimitiveValue(self, exportDefault=True): def getPrimitiveValue(self, exportDefault=True):
if exportDefault: if exportDefault:
return {name: attr.getPrimitiveValue(exportDefault=exportDefault) for name, attr in self._value.items()} return {name: attr.getPrimitiveValue(exportDefault=exportDefault) for name, attr in self._value.items()}
else: else:
return {name: attr.getPrimitiveValue(exportDefault=exportDefault) for name, attr in self._value.items() if not attr.isDefault()} return {name: attr.getPrimitiveValue(exportDefault=exportDefault) for name, attr in self._value.items() if not attr.isDefault}
def getValueStr(self): def getValueStr(self):
return self.attributeDesc.joinChar.join([v.getValueStr() for v in self._value.objects.values()]) return self.attributeDesc.joinChar.join([v.getValueStr() for v in self._value.objects.values()])
# Override value property # Override value property
value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged) value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged)
isDefault = Property(bool, _isDefault, notify=Attribute.valueChanged)
class Edge(BaseObject): class Edge(BaseObject):

View file

@ -54,7 +54,13 @@ class FeatureMatching(desc.CommandLineNode):
desc.ChoiceParam( desc.ChoiceParam(
name='photometricMatchingMethod', name='photometricMatchingMethod',
label='Photometric Matching Method', label='Photometric Matching Method',
description='''For Scalar based regions descriptor: * BRUTE_FORCE_L2: L2 BruteForce matching * ANN_L2: L2 Approximate Nearest Neighbor matching * CASCADE_HASHING_L2: L2 Cascade Hashing matching * FAST_CASCADE_HASHING_L2: L2 Cascade Hashing with precomputed hashed regions (faster than CASCADE_HASHING_L2 but use more memory) For Binary based descriptor: * BRUTE_FORCE_HAMMING: BruteForce Hamming matching''', description='For Scalar based regions descriptor\n'
' * BRUTE_FORCE_L2: L2 BruteForce matching\n'
' * ANN_L2: L2 Approximate Nearest Neighbor matching\n'
' * CASCADE_HASHING_L2: L2 Cascade Hashing matching\n'
' * FAST_CASCADE_HASHING_L2: L2 Cascade Hashing with precomputed hashed regions (faster than CASCADE_HASHING_L2 but use more memory) \n'
'For Binary based descriptor\n'
' * BRUTE_FORCE_HAMMING: BruteForce Hamming matching',
value='ANN_L2', value='ANN_L2',
values=('BRUTE_FORCE_L2', 'ANN_L2', 'CASCADE_HASHING_L2', 'FAST_CASCADE_HASHING_L2', 'BRUTE_FORCE_HAMMING'), values=('BRUTE_FORCE_L2', 'ANN_L2', 'CASCADE_HASHING_L2', 'FAST_CASCADE_HASHING_L2', 'BRUTE_FORCE_HAMMING'),
exclusive=True, exclusive=True,
@ -73,7 +79,7 @@ class FeatureMatching(desc.CommandLineNode):
name='savePutativeMatches', name='savePutativeMatches',
label='Save Putative Matches', label='Save Putative Matches',
description='''putative matches.''', description='''putative matches.''',
value='', value=False,
uid=[0], uid=[0],
), ),
desc.BoolParam( desc.BoolParam(

View file

@ -276,6 +276,11 @@ class UIGraph(QObject):
def setAttribute(self, attribute, value): def setAttribute(self, attribute, value):
self.push(commands.SetAttributeCommand(self._graph, attribute, value)) self.push(commands.SetAttributeCommand(self._graph, attribute, value))
@Slot(graph.Attribute)
def resetAttribute(self, attribute):
""" Reset 'attribute' to its default value """
self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))
@Slot(graph.Attribute, QJsonValue) @Slot(graph.Attribute, QJsonValue)
def appendAttribute(self, attribute, value=QJsonValue()): def appendAttribute(self, attribute, value=QJsonValue()):
if isinstance(value, QJsonValue): if isinstance(value, QJsonValue):

View file

@ -1,6 +1,7 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import MaterialIcons 2.2
/** /**
A component to display and edit a Node's attributes. A component to display and edit a Node's attributes.
@ -31,8 +32,11 @@ ColumnLayout {
} }
ToolButton { ToolButton {
text: "⚙" text: MaterialIcons.settings
font.family: MaterialIcons.fontFamily
onClicked: settingsMenu.popup() onClicked: settingsMenu.popup()
checkable: true
checked: settingsMenu.visible
} }
} }
Menu { Menu {
@ -64,42 +68,25 @@ ColumnLayout {
id: attributesListView id: attributesListView
anchors.fill: parent anchors.fill: parent
anchors.margins: 6 anchors.margins: 4
clip: true clip: true
spacing: 4 spacing: 1
ScrollBar.vertical: ScrollBar { id: scrollBar } ScrollBar.vertical: ScrollBar { id: scrollBar }
model: node ? node.attributes : undefined model: node ? node.attributes : undefined
delegate: RowLayout { delegate: AttributeItemDelegate {
labelWidth: 180
width: attributesListView.width width: attributesListView.width
spacing: 4 attribute: object
}
Label { // Helper MouseArea to lose edit/activeFocus
id: parameterLabel // when clicking on the background
text: object.label MouseArea {
Layout.preferredWidth: 180 anchors.fill: parent
color: object.isOutput ? "orange" : palette.text onClicked: root.forceActiveFocus()
elide: Label.ElideRight z: -1
ToolTip.text: object.desc.description
ToolTip.visible: parameterMA.containsMouse && object.desc.description
ToolTip.delay: 200
MouseArea {
id: parameterMA
anchors.fill: parent
hoverEnabled: true
}
}
AttributeItemDelegate {
Layout.fillWidth: true
Layout.rightMargin: scrollBar.width
height: childrenRect.height
attribute: object
readOnly: root.readOnly
}
} }
} }
} }

View file

@ -1,216 +1,338 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import MaterialIcons 2.2
import "../filepath.js" as Filepath
/** /**
Instantiate a control to visualize and edit an Attribute based on its type. Instantiate a control to visualize and edit an Attribute based on its type.
*/ */
Loader { RowLayout {
id: root id: root
property variant attribute: null property variant attribute: null
property bool readOnly: false property bool readOnly: false // whether the attribute's value can be modified
property alias label: parameterLabel // accessor to the internal Label (attribute's name)
property int labelWidth // shortcut to set the fixed size of the Label
readonly property bool editable: !attribute.isOutput && !attribute.isLink && !readOnly readonly property bool editable: !attribute.isOutput && !attribute.isLink && !readOnly
sourceComponent: { spacing: 4
switch(attribute.type)
{
case "ChoiceParam": return attribute.desc.exclusive ? comboBox_component : multiChoice_component
case "IntParam": return slider_component
case "FloatParam": return slider_component
case "BoolParam": return checkbox_component
case "ListAttribute": return listAttribute_component
case "GroupAttribute": return groupAttribute_component
default: return textField_component
}
}
function setTextFieldAttribute(attribute, value) Label {
{ id: parameterLabel
// editingFinished called even when TextField is readonly
if(editable)
_reconstruction.setAttribute(attribute, value.trim())
}
Component { Layout.preferredWidth: labelWidth || implicitWidth
id: textField_component Layout.fillHeight: true
TextField { horizontalAlignment: attribute.isOutput ? Qt.AlignRight : Qt.AlignLeft
readOnly: !root.editable elide: Label.ElideRight
text: attribute.value padding: 5
selectByMouse: true wrapMode: Label.WrapAtWordBoundaryOrAnywhere
onEditingFinished: setTextFieldAttribute(attribute, text)
onAccepted: setTextFieldAttribute(attribute, text)
}
}
Component { text: attribute.label
id: comboBox_component
ComboBox { // Tooltip hint with attribute's description
enabled: root.editable ToolTip.text: object.desc.description
model: attribute.desc.values ToolTip.visible: parameterMA.containsMouse && object.desc.description
Component.onCompleted: currentIndex = find(attribute.value) ToolTip.delay: 800
onActivated: _reconstruction.setAttribute(attribute, currentText)
Connections { // make label bold if attribute's value is not the default one
target: attribute font.bold: !object.isOutput && !object.isDefault
onValueChanged: currentIndex = find(attribute.value)
// make label italic if attribute is a link
font.italic: object.isLink
background: Rectangle { color: Qt.darker(palette.window, 1.2) }
MouseArea {
id: parameterMA
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.AllButtons
property Component menuComp: Menu {
id: paramMenu
property bool isFileAttribute: attribute.type == "File"
property bool isFilepath: isFileAttribute && Filepath.isFile(attribute.value)
MenuItem {
text: "Reset To Default Value"
enabled: !attribute.isOutput && !attribute.isLink && !attribute.isDefault
onTriggered: _reconstruction.resetAttribute(attribute)
}
MenuSeparator {
visible: paramMenu.isFileAttribute
height: visible ? implicitHeight : 0
}
MenuItem {
visible: paramMenu.isFileAttribute
height: visible ? implicitHeight : 0
text: paramMenu.isFilepath ? "Open Containing Folder" : "Open Folder"
onClicked: paramMenu.isFilepath ? Qt.openUrlExternally(Filepath.dirname(attribute.value)) :
Qt.openUrlExternally(attribute.value)
}
MenuItem {
visible: paramMenu.isFilepath
height: visible ? implicitHeight : 0
text: "Open File"
onClicked: Qt.openUrlExternally(attribute.value)
}
}
onClicked: {
forceActiveFocus()
if(mouse.button == Qt.RightButton)
{
var menu = menuComp.createObject(parameterLabel)
menu.parent = parameterLabel
menu.popup()
}
} }
} }
} }
Component { function setTextFieldAttribute(value)
id: multiChoice_component {
Flow { // editingFinished called even when TextField is readonly
Repeater { if(!editable)
id: checkbox_repeater return
model: attribute.desc.values switch(attribute.type)
delegate: CheckBox { {
case "IntParam":
case "FloatParam":
_reconstruction.setAttribute(root.attribute, Number(value))
break;
case "File":
_reconstruction.setAttribute(root.attribute, value.replace("file://", "").trim())
break;
default:
_reconstruction.setAttribute(root.attribute, value.trim())
}
}
Loader {
Layout.fillWidth: true
sourceComponent: {
switch(attribute.type)
{
case "ChoiceParam": return attribute.desc.exclusive ? comboBox_component : multiChoice_component
case "IntParam": return slider_component
case "FloatParam": return slider_component
case "BoolParam": return checkbox_component
case "ListAttribute": return listAttribute_component
case "GroupAttribute": return groupAttribute_component
default: return textField_component
}
}
Component {
id: textField_component
TextField {
readOnly: !root.editable
text: attribute.value
selectByMouse: true
onEditingFinished: setTextFieldAttribute(text)
onAccepted: {
setTextFieldAttribute(text)
root.forceActiveFocus()
}
DropArea {
enabled: root.editable enabled: root.editable
text: modelData anchors.fill: parent
checked: attribute.value.indexOf(modelData) >= 0 onDropped: {
onToggled: { if(drop.hasUrls)
var t = attribute.value setTextFieldAttribute(drop.urls[0].toLocaleString())
if(!checked) { t.splice(t.indexOf(modelData), 1) } // remove element else if(drop.hasText && drop.text != '')
else { t.push(modelData) } // add element setTextFieldAttribute(drop.text)
_reconstruction.setAttribute(attribute, t)
} }
} }
} }
} }
}
Component { Component {
id: slider_component id: comboBox_component
RowLayout { ComboBox {
TextField {
IntValidator {
id: intValidator
}
DoubleValidator {
id: doubleValidator
}
implicitWidth: 70
enabled: root.editable enabled: root.editable
text: s.pressed ? s.value : attribute.value model: attribute.desc.values
selectByMouse: true Component.onCompleted: currentIndex = find(attribute.value)
validator: attribute.type == "FloatParam" ? doubleValidator : intValidator onActivated: _reconstruction.setAttribute(attribute, currentText)
onEditingFinished: setTextFieldAttribute(attribute, text) Connections {
} target: attribute
Slider { onValueChanged: currentIndex = find(attribute.value)
id: s
Layout.fillWidth: true
enabled: root.editable
value: attribute.value
from: attribute.desc.range[0]
to: attribute.desc.range[1]
stepSize: attribute.desc.range[2]
snapMode: Slider.SnapAlways
onPressedChanged: {
if(!pressed)
_reconstruction.setAttribute(attribute, value)
} }
} }
}
}
Component {
id: checkbox_component
Row {
CheckBox {
enabled: root.editable
checked: attribute.value
onToggled: _reconstruction.setAttribute(attribute, !attribute.value)
}
} }
}
Component { Component {
id: listAttribute_component id: multiChoice_component
ColumnLayout { Flow {
id: listAttribute_layout Repeater {
width: parent.width id: checkbox_repeater
property bool expanded: false model: attribute.desc.values
Row { delegate: CheckBox {
spacing: 2 enabled: root.editable
ToolButton { text: modelData
text: listAttribute_layout.expanded ? "▾" : "▸" checked: attribute.value.indexOf(modelData) >= 0
onClicked: listAttribute_layout.expanded = !listAttribute_layout.expanded onToggled: {
var t = attribute.value
if(!checked) { t.splice(t.indexOf(modelData), 1) } // remove element
else { t.push(modelData) } // add element
_reconstruction.setAttribute(attribute, t)
}
}
} }
Label { }
anchors.verticalCenter: parent.verticalCenter }
text: attribute.value.count + " elements"
} Component {
ToolButton { id: slider_component
text: "+" RowLayout {
TextField {
IntValidator {
id: intValidator
}
DoubleValidator {
id: doubleValidator
}
implicitWidth: 70
enabled: root.editable enabled: root.editable
onClicked: _reconstruction.appendAttribute(attribute, undefined) text: s.pressed ? s.value : attribute.value
selectByMouse: true
validator: attribute.type == "FloatParam" ? doubleValidator : intValidator
onEditingFinished: setTextFieldAttribute(text)
onAccepted: setTextFieldAttribute(text)
}
Slider {
id: s
Layout.fillWidth: true
enabled: root.editable
value: attribute.value
from: attribute.desc.range[0]
to: attribute.desc.range[1]
stepSize: attribute.desc.range[2]
snapMode: Slider.SnapAlways
onPressedChanged: {
if(!pressed)
_reconstruction.setAttribute(attribute, value)
}
}
}
}
Component {
id: checkbox_component
Row {
CheckBox {
enabled: root.editable
checked: attribute.value
onToggled: _reconstruction.setAttribute(attribute, !attribute.value)
} }
} }
}
Component {
id: listAttribute_component
ColumnLayout {
id: listAttribute_layout
width: parent.width
property bool expanded: false
RowLayout {
spacing: 4
ToolButton {
text: listAttribute_layout.expanded ? "▾" : "▸"
onClicked: listAttribute_layout.expanded = !listAttribute_layout.expanded
}
Label {
anchors.verticalCenter: parent.verticalCenter
text: attribute.value.count + " elements"
}
ToolButton {
text: MaterialIcons.add_circle_outline
font.family: MaterialIcons.fontFamily
font.pointSize: 11
padding: 2
enabled: root.editable
onClicked: _reconstruction.appendAttribute(attribute, undefined)
}
}
ListView {
id: lv
model: listAttribute_layout.expanded ? attribute.value : undefined
visible: model != undefined && count > 0
implicitHeight: Math.min(childrenRect.height, 300)
Layout.fillWidth: true
Layout.margins: 4
clip: true
spacing: 4
ScrollBar.vertical: ScrollBar { id: sb }
delegate: RowLayout {
id: item
property var childAttrib: object
layoutDirection: Qt.RightToLeft
width: lv.width - sb.width
Component.onCompleted: {
var cpt = Qt.createComponent("AttributeItemDelegate.qml")
var obj = cpt.createObject(item,
{'attribute': Qt.binding(function() { return item.childAttrib }),
'readOnly': Qt.binding(function() { return root.readOnly })
})
obj.Layout.fillWidth = true
obj.label.text = index
obj.label.horizontalAlignment = Text.AlignHCenter
obj.label.verticalAlignment = Text.AlignVCenter
}
ToolButton {
enabled: root.editable
text: MaterialIcons.remove_circle_outline
font.family: MaterialIcons.fontFamily
font.pointSize: 11
padding: 2
ToolTip.text: "Remove Element"
ToolTip.visible: hovered
onClicked: _reconstruction.removeAttribute(item.childAttrib)
}
}
}
}
}
Component {
id: groupAttribute_component
ListView { ListView {
id: lv id: chilrenListView
model: listAttribute_layout.expanded ? attribute.value : undefined model: attribute.value
visible: model != undefined && count > 0 implicitWidth: parent.width
implicitHeight: Math.min(childrenRect.height, 300) implicitHeight: childrenRect.height
Layout.fillWidth: true onCountChanged: forceLayout()
Layout.margins: 4 spacing: 2
clip: true
spacing: 10
ScrollBar.vertical: ScrollBar { id: sb } delegate: RowLayout {
id: row
delegate: RowLayout { width: chilrenListView.width
id: item
property var childAttrib: object property var childAttrib: object
layoutDirection: Qt.RightToLeft
width: lv.width - sb.width Component.onCompleted: {
Component.onCompleted: {
var cpt = Qt.createComponent("AttributeItemDelegate.qml") var cpt = Qt.createComponent("AttributeItemDelegate.qml")
var obj = cpt.createObject(item, var obj = cpt.createObject(row,
{'attribute': Qt.binding(function() { return item.childAttrib }), {'attribute': Qt.binding(function() { return row.childAttrib }),
'readOnly': Qt.binding(function() { return root.readOnly }) 'readOnly': Qt.binding(function() { return root.readOnly })
}) })
obj.Layout.fillWidth = true obj.Layout.fillWidth = true
} obj.labelWidth = 100 // reduce label width for children (space gain)
ToolButton {
enabled: root.editable
text: "∅"
ToolTip.text: "Remove Element"
ToolTip.visible: hovered
onClicked: _reconstruction.removeAttribute(item.childAttrib)
} }
} }
} }
} }
} }
Component {
id: groupAttribute_component
ListView {
id: someview
model: attribute.value
implicitWidth: parent.width
implicitHeight: childrenRect.height
onCountChanged: forceLayout()
spacing: 2
delegate: RowLayout {
id: row
width: someview.width
property var childAttrib: object
Label { text: childAttrib.name }
Component.onCompleted: {
var cpt = Qt.createComponent("AttributeItemDelegate.qml")
var obj = cpt.createObject(row,
{'attribute': Qt.binding(function() { return row.childAttrib }),
'readOnly': Qt.binding(function() { return root.readOnly })
})
obj.Layout.fillWidth = true
}
}
}
}
} }

View file

@ -60,6 +60,7 @@ Item {
} }
onReleased: { onReleased: {
drag.target = undefined // stop drag drag.target = undefined // stop drag
root.forceActiveFocus()
workspaceClicked() workspaceClicked()
} }
onPositionChanged: { onPositionChanged: {

View file

@ -15,3 +15,10 @@ function extension(path) {
var dot_pos = path.lastIndexOf('.'); var dot_pos = path.lastIndexOf('.');
return dot_pos > -1 ? path.substring(dot_pos, path.length) : "" return dot_pos > -1 ? path.substring(dot_pos, path.length) : ""
} }
/// Return whether the given path is a path to a file.
/// (only based on the fact that the last portion of the path
/// matches the 'basename.extension' pattern)
function isFile(path) {
return extension(path) !== ""
}

View file

@ -290,6 +290,9 @@ ApplicationWindow {
anchors.fill: parent anchors.fill: parent
orientation: Qt.Vertical orientation: Qt.Vertical
// Setup global tooltip style
ToolTip.toolTip.background: Rectangle { color: palette.base; border.color: palette.mid }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true

View file

@ -1,7 +1,7 @@
import os import os
import time import time
from PySide2.QtCore import QFileSystemWatcher, QUrl, Slot from PySide2.QtCore import QFileSystemWatcher, QUrl, Slot, QTimer
from PySide2.QtQml import QQmlApplicationEngine from PySide2.QtQml import QQmlApplicationEngine
@ -29,6 +29,8 @@ class QmlInstantEngine(QQmlApplicationEngine):
self._rootItem = None self._rootItem = None
def onObjectCreated(root, url): def onObjectCreated(root, url):
if not root:
return
# Restore root item geometry # Restore root item geometry
if self._rootItem: if self._rootItem:
root.setGeometry(self._rootItem.geometry()) root.setGeometry(self._rootItem.geometry())
@ -160,6 +162,11 @@ class QmlInstantEngine(QQmlApplicationEngine):
@Slot(str) @Slot(str)
def onFileChanged(self, filepath): def onFileChanged(self, filepath):
""" Handle changes in a watched file. """ """ Handle changes in a watched file. """
if filepath not in self._watchedFiles:
# could happen if a file has just been reloaded
# and has not been re-added yet to the watched files
return
if self._verbose: if self._verbose:
print("Source file changed : ", filepath) print("Source file changed : ", filepath)
# Clear the QQuickEngine cache # Clear the QQuickEngine cache
@ -177,9 +184,10 @@ class QmlInstantEngine(QQmlApplicationEngine):
self.reload() self.reload()
# Finally, read the modified file to the watch system # Finally, re-add the modified file to the watch system
self.addFile(filepath) # after a short cooldown to avoid multiple consecutive reloads
QTimer.singleShot(200, lambda: self.addFile(filepath))
def reload(self): def reload(self):
print("Reloading ", self._sourceFile) print("Reloading {}".format(self._sourceFile))
self.load(self._sourceFile) self.load(self._sourceFile)