mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-06-05 04:12:15 +02:00
[ui] NodeEditor: refactor ChunksList and add global stats
This commit is contained in:
parent
bd5d447d12
commit
831443c29d
9 changed files with 415 additions and 307 deletions
|
@ -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)
|
||||||
|
|
52
meshroom/ui/qml/Controls/KeyValue.qml
Normal file
52
meshroom/ui/qml/Controls/KeyValue.qml
Normal 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,3 +13,12 @@ function plainToHtml(t) {
|
||||||
var escaped = t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); // escape text
|
var escaped = t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); // 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)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue