[ui] NodeEditor: refactor ChunksList and add global stats

This commit is contained in:
Fabien Castan 2021-01-22 09:40:07 +01:00
parent bd5d447d12
commit 831443c29d
9 changed files with 415 additions and 307 deletions

View file

@ -58,12 +58,12 @@ class ExecMode(Enum):
EXTERN = 2 EXTERN = 2
class StatusData: class StatusData(BaseObject):
""" """
""" """
dateTimeFormatting = '%Y-%m-%d %H:%M:%S.%f' dateTimeFormatting = '%Y-%m-%d %H:%M:%S.%f'
def __init__(self, nodeName, nodeType, packageName, packageVersion): def __init__(self, nodeName='', nodeType='', packageName='', packageVersion=''):
self.status = Status.NONE self.status = Status.NONE
self.execMode = ExecMode.NONE self.execMode = ExecMode.NONE
self.nodeName = nodeName self.nodeName = nodeName
@ -79,6 +79,11 @@ class StatusData:
self.hostname = "" self.hostname = ""
self.sessionUid = meshroom.core.sessionUid self.sessionUid = meshroom.core.sessionUid
def merge(self, other):
self.startDateTime = min(self.startDateTime, other.startDateTime)
self.endDateTime = max(self.endDateTime, other.endDateTime)
self.elapsedTime += other.elapsedTime
def reset(self): def reset(self):
self.status = Status.NONE self.status = Status.NONE
self.execMode = ExecMode.NONE self.execMode = ExecMode.NONE
@ -112,8 +117,12 @@ class StatusData:
return d return d
def fromDict(self, d): def fromDict(self, d):
self.status = getattr(Status, d.get('status', ''), Status.NONE) self.status = d.get('status', Status.NONE)
self.execMode = getattr(ExecMode, d.get('execMode', ''), ExecMode.NONE) if not isinstance(self.status, Status):
self.status = Status[self.status]
self.execMode = d.get('execMode', ExecMode.NONE)
if not isinstance(self.execMode, ExecMode):
self.execMode = ExecMode[self.execMode]
self.nodeName = d.get('nodeName', '') self.nodeName = d.get('nodeName', '')
self.nodeType = d.get('nodeType', '') self.nodeType = d.get('nodeType', '')
self.packageName = d.get('packageName', '') self.packageName = d.get('packageName', '')
@ -236,7 +245,7 @@ class NodeChunk(BaseObject):
self.node = node self.node = node
self.range = range self.range = range
self.logManager = LogManager(self) self.logManager = LogManager(self)
self.status = StatusData(node.name, node.nodeType, node.packageName, node.packageVersion) self._status = StatusData(node.name, node.nodeType, node.packageName, node.packageVersion)
self.statistics = stats.Statistics() self.statistics = stats.Statistics()
self.statusFileLastModTime = -1 self.statusFileLastModTime = -1
self._subprocess = None self._subprocess = None
@ -258,7 +267,7 @@ class NodeChunk(BaseObject):
@property @property
def statusName(self): def statusName(self):
return self.status.status.name return self._status.status.name
@property @property
def logger(self): def logger(self):
@ -266,24 +275,24 @@ class NodeChunk(BaseObject):
@property @property
def execModeName(self): def execModeName(self):
return self.status.execMode.name return self._status.execMode.name
def updateStatusFromCache(self): def updateStatusFromCache(self):
""" """
Update node status based on status file content/existence. Update node status based on status file content/existence.
""" """
statusFile = self.statusFile statusFile = self.statusFile
oldStatus = self.status.status oldStatus = self._status.status
# No status file => reset status to Status.None # No status file => reset status to Status.None
if not os.path.exists(statusFile): if not os.path.exists(statusFile):
self.statusFileLastModTime = -1 self.statusFileLastModTime = -1
self.status.reset() self._status.reset()
else: else:
with open(statusFile, 'r') as jsonFile: with open(statusFile, 'r') as jsonFile:
statusData = json.load(jsonFile) statusData = json.load(jsonFile)
self.status.fromDict(statusData) self._status.fromDict(statusData)
self.statusFileLastModTime = os.path.getmtime(statusFile) self.statusFileLastModTime = os.path.getmtime(statusFile)
if oldStatus != self.status.status: if oldStatus != self._status.status:
self.statusChanged.emit() self.statusChanged.emit()
@property @property
@ -311,7 +320,7 @@ class NodeChunk(BaseObject):
""" """
Write node status on disk. Write node status on disk.
""" """
data = self.status.toDict() data = self._status.toDict()
statusFilepath = self.statusFile statusFilepath = self.statusFile
folder = os.path.dirname(statusFilepath) folder = os.path.dirname(statusFilepath)
if not os.path.exists(folder): if not os.path.exists(folder):
@ -322,16 +331,16 @@ class NodeChunk(BaseObject):
renameWritingToFinalPath(statusFilepathWriting, statusFilepath) renameWritingToFinalPath(statusFilepathWriting, statusFilepath)
def upgradeStatusTo(self, newStatus, execMode=None): def upgradeStatusTo(self, newStatus, execMode=None):
if newStatus.value <= self.status.status.value: if newStatus.value <= self._status.status.value:
print('WARNING: downgrade status on node "{}" from {} to {}'.format(self.name, self.status.status, logging.warning('Downgrade status on node "{}" from {} to {}'.format(self.name, self._status.status,
newStatus)) newStatus))
if newStatus == Status.SUBMITTED: if newStatus == Status.SUBMITTED:
self.status = StatusData(self.node.name, self.node.nodeType, self.node.packageName, self.node.packageVersion) self._status = StatusData(self.node.name, self.node.nodeType, self.node.packageName, self.node.packageVersion)
if execMode is not None: if execMode is not None:
self.status.execMode = execMode self._status.execMode = execMode
self.execModeNameChanged.emit() self.execModeNameChanged.emit()
self.status.status = newStatus self._status.status = newStatus
self.saveStatusFile() self.saveStatusFile()
self.statusChanged.emit() self.statusChanged.emit()
@ -360,24 +369,24 @@ class NodeChunk(BaseObject):
renameWritingToFinalPath(statisticsFilepathWriting, statisticsFilepath) renameWritingToFinalPath(statisticsFilepathWriting, statisticsFilepath)
def isAlreadySubmitted(self): def isAlreadySubmitted(self):
return self.status.status in (Status.SUBMITTED, Status.RUNNING) return self._status.status in (Status.SUBMITTED, Status.RUNNING)
def isAlreadySubmittedOrFinished(self): def isAlreadySubmittedOrFinished(self):
return self.status.status in (Status.SUBMITTED, Status.RUNNING, Status.SUCCESS) return self._status.status in (Status.SUBMITTED, Status.RUNNING, Status.SUCCESS)
def isFinishedOrRunning(self): def isFinishedOrRunning(self):
return self.status.status in (Status.SUCCESS, Status.RUNNING) return self._status.status in (Status.SUCCESS, Status.RUNNING)
def isStopped(self): def isStopped(self):
return self.status.status == Status.STOPPED return self._status.status == Status.STOPPED
def process(self, forceCompute=False): def process(self, forceCompute=False):
if not forceCompute and self.status.status == Status.SUCCESS: if not forceCompute and self._status.status == Status.SUCCESS:
print("Node chunk already computed:", self.name) logging.info("Node chunk already computed: {}".format(self.name))
return return
global runningProcesses global runningProcesses
runningProcesses[self.name] = self runningProcesses[self.name] = self
self.status.initStartCompute() self._status.initStartCompute()
startTime = time.time() startTime = time.time()
self.upgradeStatusTo(Status.RUNNING) self.upgradeStatusTo(Status.RUNNING)
self.statThread = stats.StatisticsThread(self) self.statThread = stats.StatisticsThread(self)
@ -385,16 +394,16 @@ class NodeChunk(BaseObject):
try: try:
self.node.nodeDesc.processChunk(self) self.node.nodeDesc.processChunk(self)
except Exception as e: except Exception as e:
if self.status.status != Status.STOPPED: if self._status.status != Status.STOPPED:
self.upgradeStatusTo(Status.ERROR) self.upgradeStatusTo(Status.ERROR)
raise raise
except (KeyboardInterrupt, SystemError, GeneratorExit) as e: except (KeyboardInterrupt, SystemError, GeneratorExit) as e:
self.upgradeStatusTo(Status.STOPPED) self.upgradeStatusTo(Status.STOPPED)
raise raise
finally: finally:
self.status.initEndCompute() self._status.initEndCompute()
self.status.elapsedTime = time.time() - startTime self._status.elapsedTime = time.time() - startTime
print(' - elapsed time:', self.status.elapsedTimeStr) logging.info(' - elapsed time: {}'.format(self._status.elapsedTimeStr))
# ask and wait for the stats thread to stop # ask and wait for the stats thread to stop
self.statThread.stopRequest() self.statThread.stopRequest()
self.statThread.join() self.statThread.join()
@ -408,9 +417,10 @@ class NodeChunk(BaseObject):
self.node.nodeDesc.stopProcess(self) self.node.nodeDesc.stopProcess(self)
def isExtern(self): def isExtern(self):
return self.status.execMode == ExecMode.EXTERN return self._status.execMode == ExecMode.EXTERN
statusChanged = Signal() statusChanged = Signal()
status = Property(Variant, lambda self: self._status, notify=statusChanged)
statusName = Property(str, statusName.fget, notify=statusChanged) statusName = Property(str, statusName.fget, notify=statusChanged)
execModeNameChanged = Signal() execModeNameChanged = Signal()
execModeName = Property(str, execModeName.fget, notify=execModeNameChanged) execModeName = Property(str, execModeName.fget, notify=execModeNameChanged)
@ -422,7 +432,7 @@ class NodeChunk(BaseObject):
statisticsFile = Property(str, statisticsFile.fget, notify=nodeFolderChanged) statisticsFile = Property(str, statisticsFile.fget, notify=nodeFolderChanged)
nodeName = Property(str, lambda self: self.node.name, constant=True) nodeName = Property(str, lambda self: self.node.name, constant=True)
statusNodeName = Property(str, lambda self: self.status.nodeName, constant=True) statusNodeName = Property(str, lambda self: self._status.nodeName, constant=True)
# simple structure for storing node position # simple structure for storing node position
@ -837,6 +847,24 @@ class BaseNode(BaseObject):
return Status.NONE return Status.NONE
@Slot(result=StatusData)
def getFusedStatus(self):
fusedStatus = StatusData()
if self._chunks:
fusedStatus.fromDict(self._chunks[0].status.toDict())
for chunk in self._chunks[1:]:
fusedStatus.merge(chunk.status)
fusedStatus.status = self.getGlobalStatus()
return fusedStatus
@Slot(result=StatusData)
def getRecursiveFusedStatus(self):
fusedStatus = self.getFusedStatus()
nodes = self.getInputNodes(recursive=True, dependenciesOnly=True)
for node in nodes:
fusedStatus.merge(node.fusedStatus)
return fusedStatus
@property @property
def globalExecMode(self): def globalExecMode(self):
return self._chunks.at(0).execModeName return self._chunks.at(0).execModeName
@ -1000,6 +1028,10 @@ class BaseNode(BaseObject):
size = Property(int, getSize, notify=sizeChanged) size = Property(int, getSize, notify=sizeChanged)
globalStatusChanged = Signal() globalStatusChanged = Signal()
globalStatus = Property(str, lambda self: self.getGlobalStatus().name, notify=globalStatusChanged) globalStatus = Property(str, lambda self: self.getGlobalStatus().name, notify=globalStatusChanged)
fusedStatus = Property(StatusData, getFusedStatus, notify=globalStatusChanged)
elapsedTime = Property(float, lambda self: self.getFusedStatus().elapsedTime, notify=globalStatusChanged)
recursiveElapsedTime = Property(float, lambda self: self.getRecursiveFusedStatus().elapsedTime, notify=globalStatusChanged)
globalExecModeChanged = Signal() globalExecModeChanged = Signal()
globalExecMode = Property(str, globalExecMode.fget, notify=globalExecModeChanged) globalExecMode = Property(str, globalExecMode.fget, notify=globalExecModeChanged)
isComputed = Property(bool, _isComputed, notify=globalStatusChanged) isComputed = Property(bool, _isComputed, notify=globalStatusChanged)

View file

@ -0,0 +1,52 @@
import QtQuick 2.7
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import MaterialIcons 2.2
/**
* KeyValue allows to create a list of key/value, like a table.
*/
Rectangle {
property alias key: keyLabel.text
property alias value: valueText.text
color: activePalette.window
width: parent.width
height: childrenRect.height
RowLayout {
width: parent.width
Rectangle {
anchors.margins: 2
color: Qt.darker(activePalette.window, 1.1)
// Layout.preferredWidth: sizeHandle.x
Layout.minimumWidth: 10.0 * Qt.application.font.pixelSize
Layout.maximumWidth: 15.0 * Qt.application.font.pixelSize
Layout.fillWidth: false
Layout.fillHeight: true
Label {
id: keyLabel
text: "test"
anchors.fill: parent
anchors.top: parent.top
topPadding: 4
leftPadding: 6
verticalAlignment: TextEdit.AlignTop
elide: Text.ElideRight
}
}
TextArea {
id: valueText
text: ""
anchors.margins: 2
Layout.fillWidth: true
wrapMode: Label.WrapAtWordBoundaryOrAnywhere
textFormat: TextEdit.PlainText
readOnly: true
selectByMouse: true
background: Rectangle { anchors.fill: parent; color: Qt.darker(activePalette.window, 1.05) }
}
}
}

View file

@ -3,6 +3,7 @@ module Controls
ColorChart 1.0 ColorChart.qml ColorChart 1.0 ColorChart.qml
FloatingPane 1.0 FloatingPane.qml FloatingPane 1.0 FloatingPane.qml
Group 1.0 Group.qml Group 1.0 Group.qml
KeyValue 1.0 KeyValue.qml
MessageDialog 1.0 MessageDialog.qml MessageDialog 1.0 MessageDialog.qml
Panel 1.0 Panel.qml Panel 1.0 Panel.qml
SearchBar 1.0 SearchBar.qml SearchBar 1.0 SearchBar.qml

View file

@ -10,57 +10,87 @@ import "common.js" as Common
/** /**
* ChunkListView * ChunkListView
*/ */
ListView { ColumnLayout {
id: chunksLV id: root
property variant chunks
property int currentIndex: 0
property variant currentChunk: (chunks && currentIndex >= 0) ? chunks.at(currentIndex) : undefined
// model: node.chunks onChunksChanged: {
// When the list changes, ensure the current index is in the new range
if(currentIndex >= chunks.count)
currentIndex = chunks.count-1
}
property variant currentChunk: currentItem ? currentItem.chunk : undefined // chunksSummary is in sync with allChunks button (but not directly accessible as it is in a Component)
property bool chunksSummary: (currentIndex === -1)
width: 60 width: 60
Layout.fillHeight: true
highlightFollowsCurrentItem: true
keyNavigationEnabled: true
focus: true
currentIndex: 0
signal changeCurrentChunk(int chunkIndex) ListView {
id: chunksLV
Layout.fillWidth: true
Layout.fillHeight: true
header: Component { model: root.chunks
Label {
width: chunksLV.width highlightFollowsCurrentItem: (root.chunksSummary === false)
elide: Label.ElideRight keyNavigationEnabled: true
text: "Chunks" focus: true
padding: 4 currentIndex: root.currentIndex
z: 10 onCurrentIndexChanged: {
background: Rectangle { color: parent.palette.window } if(chunksLV.currentIndex !== root.currentIndex)
{
// When the list is resized, the currentIndex is reset to 0.
// So here we force it to keep the binding.
chunksLV.currentIndex = Qt.binding(function() { return root.currentIndex })
}
} }
}
highlight: Component { header: Component {
Rectangle { Button {
color: activePalette.highlight id: allChunks
opacity: 0.3 text: "Chunks"
z: 2 width: parent.width
flat: true
checkable: true
property bool summaryEnabled: root.chunksSummary
checked: summaryEnabled
onSummaryEnabledChanged: {
checked = summaryEnabled
}
onClicked: {
root.currentIndex = -1
checked = true
}
}
} }
} highlight: Component {
highlightMoveDuration: 0 Rectangle {
highlightResizeDuration: 0 visible: true // !root.chunksSummary
color: activePalette.highlight
opacity: 0.3
z: 2
}
}
highlightMoveDuration: 0
highlightResizeDuration: 0
delegate: ItemDelegate { delegate: ItemDelegate {
id: chunkDelegate id: chunkDelegate
property var chunk: object property var chunk: object
text: index text: index
width: parent.width width: parent.width
leftPadding: 8 leftPadding: 8
onClicked: { onClicked: {
chunksLV.forceActiveFocus() chunksLV.forceActiveFocus()
chunksLV.changeCurrentChunk(index) root.currentIndex = index
} }
Rectangle { Rectangle {
width: 4 width: 4
height: parent.height height: parent.height
color: Common.getChunkColor(parent.chunk) color: Common.getChunkColor(parent.chunk)
}
} }
} }
} }

View file

@ -1,5 +1,6 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.4 import QtQuick.Controls 2.4
import QtQuick.Controls 1.4 as Controls1 // SplitView
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import MaterialIcons 2.2 import MaterialIcons 2.2
import Controls 1.0 import Controls 1.0
@ -19,15 +20,6 @@ Panel {
signal attributeDoubleClicked(var mouse, var attribute) signal attributeDoubleClicked(var mouse, var attribute)
signal upgradeRequest() signal upgradeRequest()
Item {
id: m
property int chunkCurrentIndex: 0
}
onNodeChanged: {
m.chunkCurrentIndex = 0 // Needed to avoid invalid state of ChunksListView
}
title: "Node" + (node !== null ? " - <b>" + node.label + "</b>" : "") title: "Node" + (node !== null ? " - <b>" + node.label + "</b>" : "")
icon: MaterialLabel { text: MaterialIcons.tune } icon: MaterialLabel { text: MaterialIcons.tune }
@ -114,7 +106,16 @@ Panel {
Component { Component {
id: editor_component id: editor_component
ColumnLayout { Controls1.SplitView {
anchors.fill: parent
// The list of chunks
ChunksListView {
id: chunksLV
visible: (tabBar.currentIndex >= 1 && tabBar.currentIndex <= 3)
chunks: root.node.chunks
}
StackLayout { StackLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
@ -122,35 +123,65 @@ Panel {
currentIndex: tabBar.currentIndex currentIndex: tabBar.currentIndex
AttributeEditor { AttributeEditor {
Layout.fillHeight: true
Layout.fillWidth: true
model: root.node.attributes model: root.node.attributes
readOnly: root.readOnly || root.isCompatibilityNode readOnly: root.readOnly || root.isCompatibilityNode
onAttributeDoubleClicked: root.attributeDoubleClicked(mouse, attribute) onAttributeDoubleClicked: root.attributeDoubleClicked(mouse, attribute)
onUpgradeRequest: root.upgradeRequest() onUpgradeRequest: root.upgradeRequest()
} }
NodeLog { Loader {
id: nodeLog active: (tabBar.currentIndex === 1)
node: root.node Layout.fillHeight: true
chunkCurrentIndex: m.chunkCurrentIndex Layout.fillWidth: true
onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex } sourceComponent: NodeLog {
// anchors.fill: parent
Layout.fillHeight: true
Layout.fillWidth: true
width: parent.width
height: parent.height
id: nodeLog
node: root.node
currentChunkIndex: chunksLV.currentIndex
currentChunk: chunksLV.currentChunk
}
} }
NodeStatistics { Loader {
id: nodeStatistics active: (tabBar.currentIndex === 2)
node: root.node Layout.fillHeight: true
chunkCurrentIndex: m.chunkCurrentIndex Layout.fillWidth: true
onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex } sourceComponent: NodeStatistics {
id: nodeStatistics
Layout.fillHeight: true
Layout.fillWidth: true
node: root.node
currentChunkIndex: chunksLV.currentIndex
currentChunk: chunksLV.currentChunk
}
} }
NodeStatus { Loader {
id: nodeStatus active: (tabBar.currentIndex === 3)
node: root.node Layout.fillHeight: true
chunkCurrentIndex: m.chunkCurrentIndex Layout.fillWidth: true
onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex } sourceComponent: NodeStatus {
id: nodeStatus
Layout.fillHeight: true
Layout.fillWidth: true
node: root.node
currentChunkIndex: chunksLV.currentIndex
currentChunk: chunksLV.currentChunk
}
} }
NodeDocumentation { NodeDocumentation {
id: nodeDocumentation id: nodeDocumentation
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
node: root.node node: root.node
} }

View file

@ -1,6 +1,5 @@
import QtQuick 2.11 import QtQuick 2.11
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Controls 1.4 as Controls1 // SplitView
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import MaterialIcons 2.2 import MaterialIcons 2.2
import Controls 1.0 import Controls 1.0
@ -16,53 +15,34 @@ import "common.js" as Common
FocusScope { FocusScope {
id: root id: root
property variant node property variant node
property alias chunkCurrentIndex: chunksLV.currentIndex property int currentChunkIndex
signal changeCurrentChunk(int chunkIndex) property variant currentChunk
Layout.fillWidth: true
Layout.fillHeight: true
SystemPalette { id: activePalette } SystemPalette { id: activePalette }
Controls1.SplitView { Loader {
id: componentLoader
clip: true
anchors.fill: parent anchors.fill: parent
// The list of chunks property string currentFile: (root.currentChunkIndex >= 0 && root.currentChunk) ? root.currentChunk["logFile"] : ""
ChunksListView { property url source: Filepath.stringToUrl(currentFile)
id: chunksLV
Layout.fillHeight: true
model: node.chunks
onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex)
}
Loader { sourceComponent: textFileViewerComponent
id: componentLoader }
clip: true
Layout.fillWidth: true
Layout.fillHeight: true
property url source
property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk["logFile"] : "" Component {
onCurrentFileChanged: { id: textFileViewerComponent
// only set text file viewer source when ListView is fully ready
// (either empty or fully populated with a valid currentChunk)
// to avoid going through an empty url when switching between two nodes
if(!chunksLV.count || chunksLV.currentChunk) TextFileViewer {
componentLoader.source = Filepath.stringToUrl(currentFile); id: textFileViewer
anchors.fill: parent
} source: componentLoader.source
autoReload: root.currentChunk !== undefined && root.currentChunk.statusName === "RUNNING"
sourceComponent: textFileViewerComponent // source is set in fileSelector
}
Component {
id: textFileViewerComponent
TextFileViewer {
id: textFileViewer
source: componentLoader.source
Layout.fillWidth: true
Layout.fillHeight: true
autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING"
// source is set in fileSelector
}
} }
} }
} }

View file

@ -4,6 +4,7 @@ import QtQuick.Controls 1.4 as Controls1 // SplitView
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import MaterialIcons 2.2 import MaterialIcons 2.2
import Controls 1.0 import Controls 1.0
import Utils 1.0
import "common.js" as Common import "common.js" as Common
@ -15,50 +16,44 @@ import "common.js" as Common
*/ */
FocusScope { FocusScope {
id: root id: root
property variant node property variant node
property alias chunkCurrentIndex: chunksLV.currentIndex property variant currentChunkIndex
signal changeCurrentChunk(int chunkIndex) property variant currentChunk
SystemPalette { id: activePalette } SystemPalette { id: activePalette }
Controls1.SplitView { Loader {
id: componentLoader
clip: true
anchors.fill: parent anchors.fill: parent
property string currentFile: currentChunk ? currentChunk["statisticsFile"] : ""
property url source: Filepath.stringToUrl(currentFile)
// The list of chunks sourceComponent: chunksLV.chunksSummary ? statViewerComponent : chunkStatViewerComponent
ChunksListView { }
id: chunksLV
Layout.fillHeight: true Component {
model: node.chunks id: chunkStatViewerComponent
onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex) StatViewer {
id: statViewer
anchors.fill: parent
source: componentLoader.source
} }
}
Loader { Component {
id: componentLoader id: statViewerComponent
clip: true
Layout.fillWidth: true
Layout.fillHeight: true
property url source
property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk["statisticsFile"] : "" Column {
onCurrentFileChanged: { spacing: 2
// only set text file viewer source when ListView is fully ready KeyValue {
// (either empty or fully populated with a valid currentChunk) key: "Time"
// to avoid going through an empty url when switching between two nodes value: Format.sec2time(node.elapsedTime)
if(!chunksLV.count || chunksLV.currentChunk)
componentLoader.source = Filepath.stringToUrl(currentFile);
} }
KeyValue {
sourceComponent: statViewerComponent key: "Cumulated Time"
} value: Format.sec2time(node.recursiveElapsedTime)
Component {
id: statViewerComponent
StatViewer {
id: statViewer
Layout.fillWidth: true
Layout.fillHeight: true
source: componentLoader.source
} }
} }
} }

View file

@ -1,6 +1,5 @@
import QtQuick 2.11 import QtQuick 2.11
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Controls 1.4 as Controls1 // SplitView
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import MaterialIcons 2.2 import MaterialIcons 2.2
import Controls 1.0 import Controls 1.0
@ -16,169 +15,148 @@ import "common.js" as Common
FocusScope { FocusScope {
id: root id: root
property variant node property variant node
property alias chunkCurrentIndex: chunksLV.currentIndex property variant currentChunkIndex
signal changeCurrentChunk(int chunkIndex) property variant currentChunk
SystemPalette { id: activePalette } SystemPalette { id: activePalette }
Controls1.SplitView { Loader {
id: componentLoader
clip: true
anchors.fill: parent anchors.fill: parent
// The list of chunks property string currentFile: (root.currentChunkIndex >= 0) ? root.currentChunk["statusFile"] : ""
ChunksListView { property url source: Filepath.stringToUrl(currentFile)
id: chunksLV
Layout.fillHeight: true
model: node.chunks
onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex)
}
Loader { sourceComponent: statViewerComponent
id: componentLoader }
clip: true
Layout.fillWidth: true
Layout.fillHeight: true
property url source
property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk["statusFile"] : "" Component {
onCurrentFileChanged: { id: statViewerComponent
// only set text file viewer source when ListView is fully ready Item {
// (either empty or fully populated with a valid currentChunk) id: statusViewer
// to avoid going through an empty url when switching between two nodes property url source: componentLoader.source
property var lastModified: undefined
if(!chunksLV.count || chunksLV.currentChunk) onSourceChanged: {
componentLoader.source = Filepath.stringToUrl(currentFile); statusListModel.readSourceFile()
} }
sourceComponent: statViewerComponent ListModel {
} id: statusListModel
Component { function readSourceFile() {
id: statViewerComponent // make sure we are trying to load a statistics file
Item { if(!Filepath.urlToString(source).endsWith("status"))
id: statusViewer return;
property url source: componentLoader.source
property var lastModified: undefined
onSourceChanged: { var xhr = new XMLHttpRequest;
statusListModel.readSourceFile() xhr.open("GET", source);
}
ListModel { xhr.onreadystatechange = function() {
id: statusListModel if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
// console.warn("StatusListModel: read valid file")
if(lastModified === undefined || lastModified !== xhr.getResponseHeader('Last-Modified')) {
lastModified = xhr.getResponseHeader('Last-Modified')
try {
var jsonObject = JSON.parse(xhr.responseText);
function readSourceFile() { var entries = [];
// make sure we are trying to load a statistics file // prepare data to populate the ListModel from the input json object
if(!Filepath.urlToString(source).endsWith("status")) for(var key in jsonObject)
return;
var xhr = new XMLHttpRequest;
xhr.open("GET", source);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
// console.warn("StatusListModel: read valid file")
if(lastModified === undefined || lastModified !== xhr.getResponseHeader('Last-Modified')) {
lastModified = xhr.getResponseHeader('Last-Modified')
try {
var jsonObject = JSON.parse(xhr.responseText);
var entries = [];
// prepare data to populate the ListModel from the input json object
for(var key in jsonObject)
{
var entry = {};
entry["key"] = key;
entry["value"] = String(jsonObject[key]);
entries.push(entry);
}
// reset the model with prepared data (limit to one update event)
statusListModel.clear();
statusListModel.append(entries);
}
catch(exc)
{ {
// console.warn("StatusListModel: failed to read file") var entry = {};
lastModified = undefined; entry["key"] = key;
statusListModel.clear(); entry["value"] = String(jsonObject[key]);
entries.push(entry);
} }
// reset the model with prepared data (limit to one update event)
statusListModel.clear();
statusListModel.append(entries);
}
catch(exc)
{
// console.warn("StatusListModel: failed to read file")
lastModified = undefined;
statusListModel.clear();
} }
} }
else }
{ else
// console.warn("StatusListModel: invalid file") {
lastModified = undefined; // console.warn("StatusListModel: invalid file")
statusListModel.clear(); lastModified = undefined;
} statusListModel.clear();
}; }
xhr.send(); };
} xhr.send();
} }
}
ListView { ListView {
id: statusListView id: statusListView
anchors.fill: parent anchors.fill: parent
spacing: 3 spacing: 3
model: statusListModel model: statusListModel
delegate: Rectangle { delegate: Rectangle {
color: activePalette.window color: activePalette.window
width: parent.width
height: childrenRect.height
RowLayout {
width: parent.width width: parent.width
height: childrenRect.height Rectangle {
RowLayout { id: statusKey
width: parent.width anchors.margins: 2
Rectangle { // height: statusValue.height
id: statusKey color: Qt.darker(activePalette.window, 1.1)
anchors.margins: 2 Layout.preferredWidth: sizeHandle.x
// height: statusValue.height Layout.minimumWidth: 10.0 * Qt.application.font.pixelSize
color: Qt.darker(activePalette.window, 1.1) Layout.maximumWidth: 15.0 * Qt.application.font.pixelSize
Layout.preferredWidth: sizeHandle.x Layout.fillWidth: false
Layout.minimumWidth: 10.0 * Qt.application.font.pixelSize Layout.fillHeight: true
Layout.maximumWidth: 15.0 * Qt.application.font.pixelSize Label {
Layout.fillWidth: false text: key
Layout.fillHeight: true anchors.fill: parent
Label { anchors.top: parent.top
text: key topPadding: 4
anchors.fill: parent leftPadding: 6
anchors.top: parent.top verticalAlignment: TextEdit.AlignTop
topPadding: 4 elide: Text.ElideRight
leftPadding: 6
verticalAlignment: TextEdit.AlignTop
elide: Text.ElideRight
}
} }
TextArea { }
id: statusValue TextArea {
text: value id: statusValue
anchors.margins: 2 text: value
Layout.fillWidth: true anchors.margins: 2
wrapMode: Label.WrapAtWordBoundaryOrAnywhere Layout.fillWidth: true
textFormat: TextEdit.PlainText wrapMode: Label.WrapAtWordBoundaryOrAnywhere
textFormat: TextEdit.PlainText
readOnly: true readOnly: true
selectByMouse: true selectByMouse: true
background: Rectangle { anchors.fill: parent; color: Qt.darker(activePalette.window, 1.05) } background: Rectangle { anchors.fill: parent; color: Qt.darker(activePalette.window, 1.05) }
}
} }
} }
} }
}
// Categories resize handle // Categories resize handle
Rectangle { Rectangle {
id: sizeHandle id: sizeHandle
height: parent.contentHeight height: parent.contentHeight
width: 1 width: 1
x: parent.width * 0.2 x: parent.width * 0.2
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
anchors.margins: -4 anchors.margins: -4
cursorShape: Qt.SizeHorCursor cursorShape: Qt.SizeHorCursor
drag { drag {
target: parent target: parent
axis: Drag.XAxis axis: Drag.XAxis
threshold: 0 threshold: 0
minimumX: statusListView.width * 0.2 minimumX: statusListView.width * 0.2
maximumX: statusListView.width * 0.8 maximumX: statusListView.width * 0.8
}
} }
} }
} }

View file

@ -13,3 +13,12 @@ function plainToHtml(t) {
var escaped = t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); // escape text var escaped = t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); // escape text
return escaped.replace(/\n/g, '<br>'); // replace line breaks return escaped.replace(/\n/g, '<br>'); // replace line breaks
} }
function sec2time(time) {
var pad = function(num, size) { return ('000' + num).slice(size * -1); },
hours = Math.floor(time / 60 / 60),
minutes = Math.floor(time / 60) % 60,
seconds = Math.floor(time - minutes * 60);
return pad(hours, 2) + ':' + pad(minutes, 2) + ':' + pad(seconds, 2)
}