[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
class StatusData:
class StatusData(BaseObject):
"""
"""
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.execMode = ExecMode.NONE
self.nodeName = nodeName
@ -79,6 +79,11 @@ class StatusData:
self.hostname = ""
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):
self.status = Status.NONE
self.execMode = ExecMode.NONE
@ -112,8 +117,12 @@ class StatusData:
return d
def fromDict(self, d):
self.status = getattr(Status, d.get('status', ''), Status.NONE)
self.execMode = getattr(ExecMode, d.get('execMode', ''), ExecMode.NONE)
self.status = d.get('status', Status.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.nodeType = d.get('nodeType', '')
self.packageName = d.get('packageName', '')
@ -236,7 +245,7 @@ class NodeChunk(BaseObject):
self.node = node
self.range = range
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.statusFileLastModTime = -1
self._subprocess = None
@ -258,7 +267,7 @@ class NodeChunk(BaseObject):
@property
def statusName(self):
return self.status.status.name
return self._status.status.name
@property
def logger(self):
@ -266,24 +275,24 @@ class NodeChunk(BaseObject):
@property
def execModeName(self):
return self.status.execMode.name
return self._status.execMode.name
def updateStatusFromCache(self):
"""
Update node status based on status file content/existence.
"""
statusFile = self.statusFile
oldStatus = self.status.status
oldStatus = self._status.status
# No status file => reset status to Status.None
if not os.path.exists(statusFile):
self.statusFileLastModTime = -1
self.status.reset()
self._status.reset()
else:
with open(statusFile, 'r') as jsonFile:
statusData = json.load(jsonFile)
self.status.fromDict(statusData)
self._status.fromDict(statusData)
self.statusFileLastModTime = os.path.getmtime(statusFile)
if oldStatus != self.status.status:
if oldStatus != self._status.status:
self.statusChanged.emit()
@property
@ -311,7 +320,7 @@ class NodeChunk(BaseObject):
"""
Write node status on disk.
"""
data = self.status.toDict()
data = self._status.toDict()
statusFilepath = self.statusFile
folder = os.path.dirname(statusFilepath)
if not os.path.exists(folder):
@ -322,16 +331,16 @@ class NodeChunk(BaseObject):
renameWritingToFinalPath(statusFilepathWriting, statusFilepath)
def upgradeStatusTo(self, newStatus, execMode=None):
if newStatus.value <= self.status.status.value:
print('WARNING: downgrade status on node "{}" from {} to {}'.format(self.name, self.status.status,
newStatus))
if newStatus.value <= self._status.status.value:
logging.warning('Downgrade status on node "{}" from {} to {}'.format(self.name, self._status.status,
newStatus))
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:
self.status.execMode = execMode
self._status.execMode = execMode
self.execModeNameChanged.emit()
self.status.status = newStatus
self._status.status = newStatus
self.saveStatusFile()
self.statusChanged.emit()
@ -360,24 +369,24 @@ class NodeChunk(BaseObject):
renameWritingToFinalPath(statisticsFilepathWriting, statisticsFilepath)
def isAlreadySubmitted(self):
return self.status.status in (Status.SUBMITTED, Status.RUNNING)
return self._status.status in (Status.SUBMITTED, Status.RUNNING)
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):
return self.status.status in (Status.SUCCESS, Status.RUNNING)
return self._status.status in (Status.SUCCESS, Status.RUNNING)
def isStopped(self):
return self.status.status == Status.STOPPED
return self._status.status == Status.STOPPED
def process(self, forceCompute=False):
if not forceCompute and self.status.status == Status.SUCCESS:
print("Node chunk already computed:", self.name)
if not forceCompute and self._status.status == Status.SUCCESS:
logging.info("Node chunk already computed: {}".format(self.name))
return
global runningProcesses
runningProcesses[self.name] = self
self.status.initStartCompute()
self._status.initStartCompute()
startTime = time.time()
self.upgradeStatusTo(Status.RUNNING)
self.statThread = stats.StatisticsThread(self)
@ -385,16 +394,16 @@ class NodeChunk(BaseObject):
try:
self.node.nodeDesc.processChunk(self)
except Exception as e:
if self.status.status != Status.STOPPED:
if self._status.status != Status.STOPPED:
self.upgradeStatusTo(Status.ERROR)
raise
except (KeyboardInterrupt, SystemError, GeneratorExit) as e:
self.upgradeStatusTo(Status.STOPPED)
raise
finally:
self.status.initEndCompute()
self.status.elapsedTime = time.time() - startTime
print(' - elapsed time:', self.status.elapsedTimeStr)
self._status.initEndCompute()
self._status.elapsedTime = time.time() - startTime
logging.info(' - elapsed time: {}'.format(self._status.elapsedTimeStr))
# ask and wait for the stats thread to stop
self.statThread.stopRequest()
self.statThread.join()
@ -408,9 +417,10 @@ class NodeChunk(BaseObject):
self.node.nodeDesc.stopProcess(self)
def isExtern(self):
return self.status.execMode == ExecMode.EXTERN
return self._status.execMode == ExecMode.EXTERN
statusChanged = Signal()
status = Property(Variant, lambda self: self._status, notify=statusChanged)
statusName = Property(str, statusName.fget, notify=statusChanged)
execModeNameChanged = Signal()
execModeName = Property(str, execModeName.fget, notify=execModeNameChanged)
@ -422,7 +432,7 @@ class NodeChunk(BaseObject):
statisticsFile = Property(str, statisticsFile.fget, notify=nodeFolderChanged)
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
@ -837,6 +847,24 @@ class BaseNode(BaseObject):
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
def globalExecMode(self):
return self._chunks.at(0).execModeName
@ -1000,6 +1028,10 @@ class BaseNode(BaseObject):
size = Property(int, getSize, notify=sizeChanged)
globalStatusChanged = Signal()
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()
globalExecMode = Property(str, globalExecMode.fget, notify=globalExecModeChanged)
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
FloatingPane 1.0 FloatingPane.qml
Group 1.0 Group.qml
KeyValue 1.0 KeyValue.qml
MessageDialog 1.0 MessageDialog.qml
Panel 1.0 Panel.qml
SearchBar 1.0 SearchBar.qml

View file

@ -10,57 +10,87 @@ import "common.js" as Common
/**
* ChunkListView
*/
ListView {
id: chunksLV
ColumnLayout {
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
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 {
Label {
width: chunksLV.width
elide: Label.ElideRight
text: "Chunks"
padding: 4
z: 10
background: Rectangle { color: parent.palette.window }
model: root.chunks
highlightFollowsCurrentItem: (root.chunksSummary === false)
keyNavigationEnabled: true
focus: true
currentIndex: root.currentIndex
onCurrentIndexChanged: {
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 {
Rectangle {
color: activePalette.highlight
opacity: 0.3
z: 2
header: Component {
Button {
id: allChunks
text: "Chunks"
width: parent.width
flat: true
checkable: true
property bool summaryEnabled: root.chunksSummary
checked: summaryEnabled
onSummaryEnabledChanged: {
checked = summaryEnabled
}
onClicked: {
root.currentIndex = -1
checked = true
}
}
}
}
highlightMoveDuration: 0
highlightResizeDuration: 0
highlight: Component {
Rectangle {
visible: true // !root.chunksSummary
color: activePalette.highlight
opacity: 0.3
z: 2
}
}
highlightMoveDuration: 0
highlightResizeDuration: 0
delegate: ItemDelegate {
id: chunkDelegate
property var chunk: object
text: index
width: parent.width
leftPadding: 8
onClicked: {
chunksLV.forceActiveFocus()
chunksLV.changeCurrentChunk(index)
}
Rectangle {
width: 4
height: parent.height
color: Common.getChunkColor(parent.chunk)
delegate: ItemDelegate {
id: chunkDelegate
property var chunk: object
text: index
width: parent.width
leftPadding: 8
onClicked: {
chunksLV.forceActiveFocus()
root.currentIndex = index
}
Rectangle {
width: 4
height: parent.height
color: Common.getChunkColor(parent.chunk)
}
}
}
}

View file

@ -1,5 +1,6 @@
import QtQuick 2.9
import QtQuick.Controls 2.4
import QtQuick.Controls 1.4 as Controls1 // SplitView
import QtQuick.Layouts 1.3
import MaterialIcons 2.2
import Controls 1.0
@ -19,15 +20,6 @@ Panel {
signal attributeDoubleClicked(var mouse, var attribute)
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>" : "")
icon: MaterialLabel { text: MaterialIcons.tune }
@ -114,7 +106,16 @@ Panel {
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 {
Layout.fillHeight: true
Layout.fillWidth: true
@ -122,35 +123,65 @@ Panel {
currentIndex: tabBar.currentIndex
AttributeEditor {
Layout.fillHeight: true
Layout.fillWidth: true
model: root.node.attributes
readOnly: root.readOnly || root.isCompatibilityNode
onAttributeDoubleClicked: root.attributeDoubleClicked(mouse, attribute)
onUpgradeRequest: root.upgradeRequest()
}
NodeLog {
id: nodeLog
node: root.node
chunkCurrentIndex: m.chunkCurrentIndex
onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex }
Loader {
active: (tabBar.currentIndex === 1)
Layout.fillHeight: true
Layout.fillWidth: true
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 {
id: nodeStatistics
node: root.node
chunkCurrentIndex: m.chunkCurrentIndex
onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex }
Loader {
active: (tabBar.currentIndex === 2)
Layout.fillHeight: true
Layout.fillWidth: true
sourceComponent: NodeStatistics {
id: nodeStatistics
Layout.fillHeight: true
Layout.fillWidth: true
node: root.node
currentChunkIndex: chunksLV.currentIndex
currentChunk: chunksLV.currentChunk
}
}
NodeStatus {
id: nodeStatus
node: root.node
chunkCurrentIndex: m.chunkCurrentIndex
onChangeCurrentChunk: { m.chunkCurrentIndex = chunkIndex }
Loader {
active: (tabBar.currentIndex === 3)
Layout.fillHeight: true
Layout.fillWidth: true
sourceComponent: NodeStatus {
id: nodeStatus
Layout.fillHeight: true
Layout.fillWidth: true
node: root.node
currentChunkIndex: chunksLV.currentIndex
currentChunk: chunksLV.currentChunk
}
}
NodeDocumentation {
id: nodeDocumentation
Layout.fillHeight: true
Layout.fillWidth: true
node: root.node
}

View file

@ -1,6 +1,5 @@
import QtQuick 2.11
import QtQuick.Controls 2.3
import QtQuick.Controls 1.4 as Controls1 // SplitView
import QtQuick.Layouts 1.3
import MaterialIcons 2.2
import Controls 1.0
@ -16,53 +15,34 @@ import "common.js" as Common
FocusScope {
id: root
property variant node
property alias chunkCurrentIndex: chunksLV.currentIndex
signal changeCurrentChunk(int chunkIndex)
property int currentChunkIndex
property variant currentChunk
Layout.fillWidth: true
Layout.fillHeight: true
SystemPalette { id: activePalette }
Controls1.SplitView {
Loader {
id: componentLoader
clip: true
anchors.fill: parent
// The list of chunks
ChunksListView {
id: chunksLV
Layout.fillHeight: true
model: node.chunks
onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex)
}
property string currentFile: (root.currentChunkIndex >= 0 && root.currentChunk) ? root.currentChunk["logFile"] : ""
property url source: Filepath.stringToUrl(currentFile)
Loader {
id: componentLoader
clip: true
Layout.fillWidth: true
Layout.fillHeight: true
property url source
sourceComponent: textFileViewerComponent
}
property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk["logFile"] : ""
onCurrentFileChanged: {
// 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
Component {
id: textFileViewerComponent
if(!chunksLV.count || chunksLV.currentChunk)
componentLoader.source = Filepath.stringToUrl(currentFile);
}
sourceComponent: textFileViewerComponent
}
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
}
TextFileViewer {
id: textFileViewer
anchors.fill: parent
source: componentLoader.source
autoReload: root.currentChunk !== undefined && root.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 MaterialIcons 2.2
import Controls 1.0
import Utils 1.0
import "common.js" as Common
@ -15,50 +16,44 @@ import "common.js" as Common
*/
FocusScope {
id: root
property variant node
property alias chunkCurrentIndex: chunksLV.currentIndex
signal changeCurrentChunk(int chunkIndex)
property variant currentChunkIndex
property variant currentChunk
SystemPalette { id: activePalette }
Controls1.SplitView {
Loader {
id: componentLoader
clip: true
anchors.fill: parent
property string currentFile: currentChunk ? currentChunk["statisticsFile"] : ""
property url source: Filepath.stringToUrl(currentFile)
// The list of chunks
ChunksListView {
id: chunksLV
Layout.fillHeight: true
model: node.chunks
onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex)
sourceComponent: chunksLV.chunksSummary ? statViewerComponent : chunkStatViewerComponent
}
Component {
id: chunkStatViewerComponent
StatViewer {
id: statViewer
anchors.fill: parent
source: componentLoader.source
}
}
Loader {
id: componentLoader
clip: true
Layout.fillWidth: true
Layout.fillHeight: true
property url source
Component {
id: statViewerComponent
property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk["statisticsFile"] : ""
onCurrentFileChanged: {
// 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)
componentLoader.source = Filepath.stringToUrl(currentFile);
Column {
spacing: 2
KeyValue {
key: "Time"
value: Format.sec2time(node.elapsedTime)
}
sourceComponent: statViewerComponent
}
Component {
id: statViewerComponent
StatViewer {
id: statViewer
Layout.fillWidth: true
Layout.fillHeight: true
source: componentLoader.source
KeyValue {
key: "Cumulated Time"
value: Format.sec2time(node.recursiveElapsedTime)
}
}
}

View file

@ -1,6 +1,5 @@
import QtQuick 2.11
import QtQuick.Controls 2.3
import QtQuick.Controls 1.4 as Controls1 // SplitView
import QtQuick.Layouts 1.3
import MaterialIcons 2.2
import Controls 1.0
@ -16,169 +15,148 @@ import "common.js" as Common
FocusScope {
id: root
property variant node
property alias chunkCurrentIndex: chunksLV.currentIndex
signal changeCurrentChunk(int chunkIndex)
property variant currentChunkIndex
property variant currentChunk
SystemPalette { id: activePalette }
Controls1.SplitView {
Loader {
id: componentLoader
clip: true
anchors.fill: parent
// The list of chunks
ChunksListView {
id: chunksLV
Layout.fillHeight: true
model: node.chunks
onChangeCurrentChunk: root.changeCurrentChunk(chunkIndex)
}
property string currentFile: (root.currentChunkIndex >= 0) ? root.currentChunk["statusFile"] : ""
property url source: Filepath.stringToUrl(currentFile)
Loader {
id: componentLoader
clip: true
Layout.fillWidth: true
Layout.fillHeight: true
property url source
sourceComponent: statViewerComponent
}
property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk["statusFile"] : ""
onCurrentFileChanged: {
// 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
Component {
id: statViewerComponent
Item {
id: statusViewer
property url source: componentLoader.source
property var lastModified: undefined
if(!chunksLV.count || chunksLV.currentChunk)
componentLoader.source = Filepath.stringToUrl(currentFile);
onSourceChanged: {
statusListModel.readSourceFile()
}
sourceComponent: statViewerComponent
}
ListModel {
id: statusListModel
Component {
id: statViewerComponent
Item {
id: statusViewer
property url source: componentLoader.source
property var lastModified: undefined
function readSourceFile() {
// make sure we are trying to load a statistics file
if(!Filepath.urlToString(source).endsWith("status"))
return;
onSourceChanged: {
statusListModel.readSourceFile()
}
var xhr = new XMLHttpRequest;
xhr.open("GET", source);
ListModel {
id: statusListModel
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);
function readSourceFile() {
// make sure we are trying to load a statistics file
if(!Filepath.urlToString(source).endsWith("status"))
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)
var entries = [];
// prepare data to populate the ListModel from the input json object
for(var key in jsonObject)
{
// console.warn("StatusListModel: failed to read file")
lastModified = undefined;
statusListModel.clear();
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")
lastModified = undefined;
statusListModel.clear();
}
}
else
{
// console.warn("StatusListModel: invalid file")
lastModified = undefined;
statusListModel.clear();
}
};
xhr.send();
}
}
else
{
// console.warn("StatusListModel: invalid file")
lastModified = undefined;
statusListModel.clear();
}
};
xhr.send();
}
}
ListView {
id: statusListView
anchors.fill: parent
spacing: 3
model: statusListModel
ListView {
id: statusListView
anchors.fill: parent
spacing: 3
model: statusListModel
delegate: Rectangle {
color: activePalette.window
delegate: Rectangle {
color: activePalette.window
width: parent.width
height: childrenRect.height
RowLayout {
width: parent.width
height: childrenRect.height
RowLayout {
width: parent.width
Rectangle {
id: statusKey
anchors.margins: 2
// height: statusValue.height
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 {
text: key
anchors.fill: parent
anchors.top: parent.top
topPadding: 4
leftPadding: 6
verticalAlignment: TextEdit.AlignTop
elide: Text.ElideRight
}
Rectangle {
id: statusKey
anchors.margins: 2
// height: statusValue.height
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 {
text: key
anchors.fill: parent
anchors.top: parent.top
topPadding: 4
leftPadding: 6
verticalAlignment: TextEdit.AlignTop
elide: Text.ElideRight
}
TextArea {
id: statusValue
text: value
anchors.margins: 2
Layout.fillWidth: true
wrapMode: Label.WrapAtWordBoundaryOrAnywhere
textFormat: TextEdit.PlainText
}
TextArea {
id: statusValue
text: value
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) }
}
readOnly: true
selectByMouse: true
background: Rectangle { anchors.fill: parent; color: Qt.darker(activePalette.window, 1.05) }
}
}
}
}
// Categories resize handle
Rectangle {
id: sizeHandle
height: parent.contentHeight
width: 1
x: parent.width * 0.2
MouseArea {
anchors.fill: parent
anchors.margins: -4
cursorShape: Qt.SizeHorCursor
drag {
target: parent
axis: Drag.XAxis
threshold: 0
minimumX: statusListView.width * 0.2
maximumX: statusListView.width * 0.8
}
// Categories resize handle
Rectangle {
id: sizeHandle
height: parent.contentHeight
width: 1
x: parent.width * 0.2
MouseArea {
anchors.fill: parent
anchors.margins: -4
cursorShape: Qt.SizeHorCursor
drag {
target: parent
axis: Drag.XAxis
threshold: 0
minimumX: statusListView.width * 0.2
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
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)
}