mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-30 10:47:34 +02:00
Merge pull request #466 from alicevision/dev_logsAndStatus
Logs display and status update improvements
This commit is contained in:
commit
e26b3dee30
13 changed files with 541 additions and 213 deletions
|
@ -9,6 +9,7 @@ from PySide2.QtWidgets import QApplication
|
||||||
import meshroom
|
import meshroom
|
||||||
from meshroom.core import nodesDesc
|
from meshroom.core import nodesDesc
|
||||||
from meshroom.ui import components
|
from meshroom.ui import components
|
||||||
|
from meshroom.ui.components.clipboard import ClipboardHelper
|
||||||
from meshroom.ui.components.filepath import FilepathHelper
|
from meshroom.ui.components.filepath import FilepathHelper
|
||||||
from meshroom.ui.components.scene3D import Scene3DHelper
|
from meshroom.ui.components.scene3D import Scene3DHelper
|
||||||
from meshroom.ui.palette import PaletteManager
|
from meshroom.ui.palette import PaletteManager
|
||||||
|
@ -84,17 +85,25 @@ class MeshroomApp(QApplication):
|
||||||
|
|
||||||
# expose available node types that can be instantiated
|
# expose available node types that can be instantiated
|
||||||
self.engine.rootContext().setContextProperty("_nodeTypes", sorted(nodesDesc.keys()))
|
self.engine.rootContext().setContextProperty("_nodeTypes", sorted(nodesDesc.keys()))
|
||||||
|
|
||||||
|
# instantiate Reconstruction object
|
||||||
r = Reconstruction(parent=self)
|
r = Reconstruction(parent=self)
|
||||||
self.engine.rootContext().setContextProperty("_reconstruction", r)
|
self.engine.rootContext().setContextProperty("_reconstruction", r)
|
||||||
pm = PaletteManager(self.engine, parent=self)
|
|
||||||
self.engine.rootContext().setContextProperty("_PaletteManager", pm)
|
# those helpers should be available from QML Utils module as singletons, but:
|
||||||
fpHelper = FilepathHelper(parent=self)
|
# - qmlRegisterUncreatableType is not yet available in PySide2
|
||||||
self.engine.rootContext().setContextProperty("Filepath", fpHelper)
|
# - declaring them as singleton in qmldir file causes random crash at exit
|
||||||
scene3DHelper = Scene3DHelper(parent=self)
|
# => expose them as context properties instead
|
||||||
self.engine.rootContext().setContextProperty("Scene3DHelper", scene3DHelper)
|
self.engine.rootContext().setContextProperty("Filepath", FilepathHelper(parent=self))
|
||||||
|
self.engine.rootContext().setContextProperty("Scene3DHelper", Scene3DHelper(parent=self))
|
||||||
|
self.engine.rootContext().setContextProperty("Clipboard", ClipboardHelper(parent=self))
|
||||||
|
|
||||||
|
# additional context properties
|
||||||
|
self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self))
|
||||||
self.engine.rootContext().setContextProperty("MeshroomApp", self)
|
self.engine.rootContext().setContextProperty("MeshroomApp", self)
|
||||||
# Request any potential computation to stop on exit
|
|
||||||
self.aboutToQuit.connect(r.stopExecution)
|
# request any potential computation to stop on exit
|
||||||
|
self.aboutToQuit.connect(r.stopChildThreads)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.')
|
parser = argparse.ArgumentParser(prog=args[0], description='Launch Meshroom UI.')
|
||||||
parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False,
|
parser.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False,
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
|
|
||||||
def registerTypes():
|
def registerTypes():
|
||||||
from PySide2.QtQml import qmlRegisterType
|
from PySide2.QtQml import qmlRegisterType
|
||||||
|
from meshroom.ui.components.clipboard import ClipboardHelper
|
||||||
from meshroom.ui.components.edge import EdgeMouseArea
|
from meshroom.ui.components.edge import EdgeMouseArea
|
||||||
from meshroom.ui.components.filepath import FilepathHelper
|
from meshroom.ui.components.filepath import FilepathHelper
|
||||||
from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController
|
from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController
|
||||||
|
|
||||||
qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea")
|
qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea")
|
||||||
|
qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable
|
||||||
qmlRegisterType(FilepathHelper, "Meshroom.Helpers", 1, 0, "FilepathHelper") # TODO: uncreatable
|
qmlRegisterType(FilepathHelper, "Meshroom.Helpers", 1, 0, "FilepathHelper") # TODO: uncreatable
|
||||||
qmlRegisterType(Scene3DHelper, "Meshroom.Helpers", 1, 0, "Scene3DHelper") # TODO: uncreatable
|
qmlRegisterType(Scene3DHelper, "Meshroom.Helpers", 1, 0, "Scene3DHelper") # TODO: uncreatable
|
||||||
qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController")
|
qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController")
|
||||||
|
|
20
meshroom/ui/components/clipboard.py
Normal file
20
meshroom/ui/components/clipboard.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from PySide2.QtCore import Slot, QObject
|
||||||
|
from PySide2.QtGui import QClipboard
|
||||||
|
|
||||||
|
|
||||||
|
class ClipboardHelper(QObject):
|
||||||
|
"""
|
||||||
|
Simple wrapper around a QClipboard with methods exposed as Slots for QML use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(ClipboardHelper, self).__init__(parent)
|
||||||
|
self._clipboard = QClipboard(parent=self)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def setText(self, value):
|
||||||
|
self._clipboard.setText(value)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def clear(self):
|
||||||
|
self._clipboard.clear()
|
|
@ -118,7 +118,7 @@ class EdgeMouseArea(QQuickItem):
|
||||||
containsMouse = Property(float, getContainsMouse, notify=containsMouseChanged)
|
containsMouse = Property(float, getContainsMouse, notify=containsMouseChanged)
|
||||||
acceptedButtons = Property(int,
|
acceptedButtons = Property(int,
|
||||||
lambda self: super(EdgeMouseArea, self).acceptedMouseButtons,
|
lambda self: super(EdgeMouseArea, self).acceptedMouseButtons,
|
||||||
lambda self, value: super(EdgeMouseArea, self).setAcceptedMouseButtons(value))
|
lambda self, value: super(EdgeMouseArea, self).setAcceptedMouseButtons(Qt.MouseButtons(value)))
|
||||||
|
|
||||||
pressed = Signal(MouseEvent)
|
pressed = Signal(MouseEvent)
|
||||||
released = Signal(MouseEvent)
|
released = Signal(MouseEvent)
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
# coding:utf-8
|
# coding:utf-8
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from threading import Thread
|
from threading import Thread, Event
|
||||||
|
from multiprocessing.pool import ThreadPool
|
||||||
|
|
||||||
from PySide2.QtCore import Slot, QJsonValue, QObject, QUrl, Property, Signal, QPoint
|
from PySide2.QtCore import Slot, QJsonValue, QObject, QUrl, Property, Signal, QPoint
|
||||||
|
|
||||||
|
@ -16,6 +18,61 @@ from meshroom.ui import commands
|
||||||
from meshroom.ui.utils import makeProperty
|
from meshroom.ui.utils import makeProperty
|
||||||
|
|
||||||
|
|
||||||
|
class FilesModTimePollerThread(QObject):
|
||||||
|
"""
|
||||||
|
Thread responsible for non-blocking polling of last modification times of a list of files.
|
||||||
|
Uses a Python ThreadPool internally to split tasks on multiple threads.
|
||||||
|
"""
|
||||||
|
timesAvailable = Signal(list)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(FilesModTimePollerThread, self).__init__(parent)
|
||||||
|
self._thread = None
|
||||||
|
self._threadPool = ThreadPool(4)
|
||||||
|
self._stopFlag = Event()
|
||||||
|
self._refreshInterval = 5 # refresh interval in seconds
|
||||||
|
self._files = []
|
||||||
|
|
||||||
|
def start(self, files):
|
||||||
|
""" Start polling thread.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files: the list of files to monitor
|
||||||
|
"""
|
||||||
|
if self._thread:
|
||||||
|
# thread already running, return
|
||||||
|
return
|
||||||
|
if not files:
|
||||||
|
# file list is empty
|
||||||
|
return
|
||||||
|
self._stopFlag.clear()
|
||||||
|
self._files = files or []
|
||||||
|
self._thread = Thread(target=self.run)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
""" Request polling thread to stop. """
|
||||||
|
if not self._thread:
|
||||||
|
return
|
||||||
|
self._stopFlag.set()
|
||||||
|
self._thread.join()
|
||||||
|
self._thread = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getFileLastModTime(f):
|
||||||
|
""" Return 'mtime' of the file if it exists, -1 otherwise. """
|
||||||
|
try:
|
||||||
|
return os.path.getmtime(f)
|
||||||
|
except OSError:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
""" Poll watched files for last modification time. """
|
||||||
|
while not self._stopFlag.wait(self._refreshInterval):
|
||||||
|
times = self._threadPool.map(FilesModTimePollerThread.getFileLastModTime, self._files)
|
||||||
|
self.timesAvailable.emit(times)
|
||||||
|
|
||||||
|
|
||||||
class ChunksMonitor(QObject):
|
class ChunksMonitor(QObject):
|
||||||
"""
|
"""
|
||||||
ChunksMonitor regularly check NodeChunks' status files for modification and trigger their update on change.
|
ChunksMonitor regularly check NodeChunks' status files for modification and trigger their update on change.
|
||||||
|
@ -29,52 +86,69 @@ class ChunksMonitor(QObject):
|
||||||
def __init__(self, chunks=(), parent=None):
|
def __init__(self, chunks=(), parent=None):
|
||||||
super(ChunksMonitor, self).__init__(parent)
|
super(ChunksMonitor, self).__init__(parent)
|
||||||
self.lastModificationRecords = dict()
|
self.lastModificationRecords = dict()
|
||||||
|
self._filesTimePoller = FilesModTimePollerThread(parent=self)
|
||||||
|
self._filesTimePoller.timesAvailable.connect(self.compareFilesTimes)
|
||||||
|
self._pollerOutdated = False
|
||||||
self.setChunks(chunks)
|
self.setChunks(chunks)
|
||||||
# Check status files every x seconds
|
|
||||||
# TODO: adapt frequency according to graph compute status
|
|
||||||
self.startTimer(5000)
|
|
||||||
|
|
||||||
def setChunks(self, chunks):
|
def setChunks(self, chunks):
|
||||||
""" Set the list of chunks to monitor. """
|
""" Set the list of chunks to monitor. """
|
||||||
|
self._filesTimePoller.stop()
|
||||||
self.clear()
|
self.clear()
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
f = chunk.statusFile
|
# initialize last modification times to current time for all chunks
|
||||||
# Store a record of {chunk: status file last modification}
|
self.lastModificationRecords[chunk] = time.time()
|
||||||
self.lastModificationRecords[chunk] = self.getFileLastModTime(f)
|
|
||||||
# For local use, handle statusChanged emitted directly from the node chunk
|
# For local use, handle statusChanged emitted directly from the node chunk
|
||||||
chunk.statusChanged.connect(self.onChunkStatusChanged)
|
chunk.statusChanged.connect(self.onChunkStatusChanged)
|
||||||
|
self._pollerOutdated = True
|
||||||
self.chunkStatusChanged.emit(None, -1)
|
self.chunkStatusChanged.emit(None, -1)
|
||||||
|
self._filesTimePoller.start(self.statusFiles)
|
||||||
|
self._pollerOutdated = False
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
""" Stop the status files monitoring. """
|
||||||
|
self._filesTimePoller.stop()
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
""" Clear the list of monitored chunks """
|
""" Clear the list of monitored chunks. """
|
||||||
for ch in self.lastModificationRecords:
|
for chunk in self.lastModificationRecords:
|
||||||
ch.statusChanged.disconnect(self.onChunkStatusChanged)
|
chunk.statusChanged.disconnect(self.onChunkStatusChanged)
|
||||||
self.lastModificationRecords.clear()
|
self.lastModificationRecords.clear()
|
||||||
|
|
||||||
def timerEvent(self, evt):
|
|
||||||
self.checkFileTimes()
|
|
||||||
|
|
||||||
def onChunkStatusChanged(self):
|
def onChunkStatusChanged(self):
|
||||||
""" React to change of status coming from the NodeChunk itself. """
|
""" React to change of status coming from the NodeChunk itself. """
|
||||||
chunk = self.sender()
|
chunk = self.sender()
|
||||||
assert chunk in self.lastModificationRecords
|
assert chunk in self.lastModificationRecords
|
||||||
# Update record entry for this file so that it's up-to-date on next timerEvent
|
# update record entry for this file so that it's up-to-date on next timerEvent
|
||||||
self.lastModificationRecords[chunk] = self.getFileLastModTime(chunk.statusFile)
|
# use current time instead of actual file's mtime to limit filesystem requests
|
||||||
|
self.lastModificationRecords[chunk] = time.time()
|
||||||
self.chunkStatusChanged.emit(chunk, chunk.status.status)
|
self.chunkStatusChanged.emit(chunk, chunk.status.status)
|
||||||
|
|
||||||
@staticmethod
|
@property
|
||||||
def getFileLastModTime(f):
|
def statusFiles(self):
|
||||||
""" Return 'mtime' of the file if it exists, -1 otherwise. """
|
""" Get status file paths from current chunks. """
|
||||||
return os.path.getmtime(f) if os.path.exists(f) else -1
|
return [c.statusFile for c in self.lastModificationRecords.keys()]
|
||||||
|
|
||||||
def checkFileTimes(self):
|
def compareFilesTimes(self, times):
|
||||||
""" Check status files last modification time and compare with stored value """
|
"""
|
||||||
for chunk, t in self.lastModificationRecords.items():
|
Compare previous file modification times with results from last poll.
|
||||||
lastMod = self.getFileLastModTime(chunk.statusFile)
|
Trigger chunk status update if file was modified since.
|
||||||
if lastMod != t:
|
|
||||||
self.lastModificationRecords[chunk] = lastMod
|
Args:
|
||||||
|
times: the last modification times for currently monitored files.
|
||||||
|
"""
|
||||||
|
if self._pollerOutdated:
|
||||||
|
return
|
||||||
|
|
||||||
|
newRecords = dict(zip(self.lastModificationRecords.keys(), times))
|
||||||
|
for chunk, previousTime in self.lastModificationRecords.items():
|
||||||
|
lastModTime = newRecords.get(chunk, -1)
|
||||||
|
# update chunk status if:
|
||||||
|
# - last modification time is more recent than previous record
|
||||||
|
# - file is no more available (-1)
|
||||||
|
if lastModTime > previousTime or (lastModTime == -1 != previousTime):
|
||||||
|
self.lastModificationRecords[chunk] = lastModTime
|
||||||
chunk.updateStatusFromCache()
|
chunk.updateStatusFromCache()
|
||||||
logging.debug("Status for node {} changed: {}".format(chunk.node, chunk.status.status))
|
|
||||||
|
|
||||||
chunkStatusChanged = Signal(NodeChunk, int)
|
chunkStatusChanged = Signal(NodeChunk, int)
|
||||||
|
|
||||||
|
@ -244,6 +318,11 @@ class UIGraph(QObject):
|
||||||
self._sortedDFSChunks.clear()
|
self._sortedDFSChunks.clear()
|
||||||
self._undoStack.clear()
|
self._undoStack.clear()
|
||||||
|
|
||||||
|
def stopChildThreads(self):
|
||||||
|
""" Stop all child threads. """
|
||||||
|
self.stopExecution()
|
||||||
|
self._chunksMonitor.stop()
|
||||||
|
|
||||||
def load(self, filepath):
|
def load(self, filepath):
|
||||||
g = Graph('')
|
g = Graph('')
|
||||||
g.load(filepath)
|
g.load(filepath)
|
||||||
|
|
340
meshroom/ui/qml/Controls/TextFileViewer.qml
Normal file
340
meshroom/ui/qml/Controls/TextFileViewer.qml
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
import QtQuick 2.11
|
||||||
|
import QtQuick.Controls 2.4
|
||||||
|
import QtQuick.Layouts 1.11
|
||||||
|
import MaterialIcons 2.2
|
||||||
|
|
||||||
|
import Utils 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text file viewer with auto-reload feature.
|
||||||
|
* Uses a ListView with one delegate by line instead of a TextArea for performance reasons.
|
||||||
|
*/
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/// Source text file to load
|
||||||
|
property url source
|
||||||
|
/// Whether to periodically reload the source file
|
||||||
|
property bool autoReload: false
|
||||||
|
/// Interval (in ms) at which source file should be reloaded if autoReload is enabled
|
||||||
|
property int autoReloadInterval: 2000
|
||||||
|
/// Whether the source is currently being loaded
|
||||||
|
property bool loading: false
|
||||||
|
|
||||||
|
onSourceChanged: loadSource()
|
||||||
|
onVisibleChanged: if(visible) loadSource()
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// Toolbar
|
||||||
|
Pane {
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
Layout.fillHeight: true
|
||||||
|
padding: 0
|
||||||
|
background: Rectangle { color: Qt.darker(Colors.sysPalette.window, 1.2) }
|
||||||
|
Column {
|
||||||
|
height: parent.height
|
||||||
|
ToolButton {
|
||||||
|
text: MaterialIcons.refresh
|
||||||
|
ToolTip.text: "Reload"
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
font.family: MaterialIcons.fontFamily
|
||||||
|
onClicked: loadSource()
|
||||||
|
}
|
||||||
|
ToolButton {
|
||||||
|
text: MaterialIcons.vertical_align_top
|
||||||
|
ToolTip.text: "Scroll to Top"
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
font.family: MaterialIcons.fontFamily
|
||||||
|
onClicked: textView.positionViewAtBeginning()
|
||||||
|
}
|
||||||
|
ToolButton {
|
||||||
|
id: autoscroll
|
||||||
|
text: MaterialIcons.vertical_align_bottom
|
||||||
|
ToolTip.text: "Scroll to Bottom"
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
font.family: MaterialIcons.fontFamily
|
||||||
|
onClicked: textView.positionViewAtEnd()
|
||||||
|
checkable: false
|
||||||
|
checked: textView.atYEnd
|
||||||
|
}
|
||||||
|
ToolButton {
|
||||||
|
text: MaterialIcons.assignment
|
||||||
|
ToolTip.text: "Copy"
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
font.family: MaterialIcons.fontFamily
|
||||||
|
onClicked: copySubMenu.open()
|
||||||
|
Menu {
|
||||||
|
id: copySubMenu
|
||||||
|
x: parent.width
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Copy Visible Text"
|
||||||
|
onTriggered: {
|
||||||
|
var t = "";
|
||||||
|
for(var i = textView.firstVisibleIndex(); i < textView.lastVisibleIndex(); ++i)
|
||||||
|
t += textView.model[i] + "\n";
|
||||||
|
Clipboard.setText(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MenuItem {
|
||||||
|
text: "Copy All"
|
||||||
|
onTriggered: {
|
||||||
|
Clipboard.setText(textView.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolButton {
|
||||||
|
text: MaterialIcons.open_in_new
|
||||||
|
ToolTip.text: "Open Externally"
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
font.family: MaterialIcons.fontFamily
|
||||||
|
enabled: root.source !== ""
|
||||||
|
onClicked: Qt.openUrlExternally(root.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.margins: 4
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: textView
|
||||||
|
|
||||||
|
property string text
|
||||||
|
|
||||||
|
// model consists in text split by line
|
||||||
|
model: textView.text.split("\n")
|
||||||
|
visible: text != ""
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
// custom key navigation handling
|
||||||
|
keyNavigationEnabled: false
|
||||||
|
highlightFollowsCurrentItem: true
|
||||||
|
highlightMoveDuration: 0
|
||||||
|
Keys.onPressed: {
|
||||||
|
switch(event.key)
|
||||||
|
{
|
||||||
|
case Qt.Key_Home:
|
||||||
|
textView.positionViewAtBeginning();
|
||||||
|
break;
|
||||||
|
case Qt.Key_End:
|
||||||
|
textView.positionViewAtEnd();
|
||||||
|
break;
|
||||||
|
case Qt.Key_Up:
|
||||||
|
currentIndex = firstVisibleIndex();
|
||||||
|
decrementCurrentIndex();
|
||||||
|
break;
|
||||||
|
case Qt.Key_Down:
|
||||||
|
currentIndex = lastVisibleIndex();
|
||||||
|
incrementCurrentIndex();
|
||||||
|
break;
|
||||||
|
case Qt.Key_PageUp:
|
||||||
|
textView.positionViewAtIndex(firstVisibleIndex(), ListView.End);
|
||||||
|
break;
|
||||||
|
case Qt.Key_PageDown:
|
||||||
|
textView.positionViewAtIndex(lastVisibleIndex(), ListView.Beginning);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setText(value) {
|
||||||
|
// store current first index
|
||||||
|
var topIndex = firstVisibleIndex();
|
||||||
|
// store whether autoscroll to bottom is active
|
||||||
|
var scrollToBottom = atYEnd && autoscroll.checked;
|
||||||
|
// replace text
|
||||||
|
text = value;
|
||||||
|
|
||||||
|
// restore content position by either:
|
||||||
|
// - autoscrolling to bottom
|
||||||
|
if(scrollToBottom)
|
||||||
|
positionViewAtEnd();
|
||||||
|
// - setting first visible index back (when possible)
|
||||||
|
else if(topIndex !== firstVisibleIndex())
|
||||||
|
positionViewAtIndex(Math.min(topIndex, count-1), ListView.Beginning);
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstVisibleIndex() {
|
||||||
|
return indexAt(contentX, contentY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lastVisibleIndex() {
|
||||||
|
return indexAt(contentX, contentY + height - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
id: vScrollBar
|
||||||
|
minimumSize: 0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.horizontal: ScrollBar { minimumSize: 0.1 }
|
||||||
|
|
||||||
|
// TextMetrics for line numbers column
|
||||||
|
TextMetrics {
|
||||||
|
id: lineMetrics
|
||||||
|
font.family: "Monospace, Consolas, Monaco"
|
||||||
|
text: textView.count * 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextMetrics for textual progress bar
|
||||||
|
TextMetrics {
|
||||||
|
id: progressMetrics
|
||||||
|
// total number of character in textual progress bar
|
||||||
|
property int count: 51
|
||||||
|
property string character: '*'
|
||||||
|
text: character.repeat(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: RowLayout {
|
||||||
|
width: textView.width
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
// Line number
|
||||||
|
Label {
|
||||||
|
text: index + 1
|
||||||
|
Layout.minimumWidth: lineMetrics.width
|
||||||
|
rightPadding: 2
|
||||||
|
enabled: false
|
||||||
|
Layout.fillHeight: true
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: delegateLoader
|
||||||
|
Layout.fillWidth: true
|
||||||
|
// default line delegate
|
||||||
|
sourceComponent: line_component
|
||||||
|
|
||||||
|
// line delegate selector based on content
|
||||||
|
StateGroup {
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "progressBar"
|
||||||
|
// detect textual progressbar (non empty line with only progressbar character)
|
||||||
|
when: modelData.trim().length
|
||||||
|
&& modelData.split(progressMetrics.character).length - 1 === modelData.trim().length
|
||||||
|
PropertyChanges {
|
||||||
|
target: delegateLoader
|
||||||
|
sourceComponent: progressBar_component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgressBar delegate
|
||||||
|
Component {
|
||||||
|
id: progressBar_component
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: progressMetrics.height
|
||||||
|
ProgressBar {
|
||||||
|
width: progressMetrics.width
|
||||||
|
height: parent.height - 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
from: 0
|
||||||
|
to: progressMetrics.count
|
||||||
|
value: modelData.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default line delegate
|
||||||
|
Component {
|
||||||
|
id: line_component
|
||||||
|
TextInput {
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
text: modelData
|
||||||
|
font.family: "Monospace, Consolas, Monaco"
|
||||||
|
padding: 0
|
||||||
|
selectByMouse: true
|
||||||
|
readOnly: true
|
||||||
|
selectionColor: Colors.sysPalette.highlight
|
||||||
|
persistentSelection: false
|
||||||
|
Keys.forwardTo: [textView]
|
||||||
|
|
||||||
|
color: {
|
||||||
|
// color line according to log level
|
||||||
|
if(text.indexOf("[warning]") >= 0)
|
||||||
|
return Colors.orange;
|
||||||
|
else if(text.indexOf("[error]") >= 0)
|
||||||
|
return Colors.red;
|
||||||
|
return palette.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: vScrollBar.width
|
||||||
|
z: -1
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: lineMetrics.width
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IBeamCursor shape overlay
|
||||||
|
MouseArea {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File loading indicator
|
||||||
|
BusyIndicator {
|
||||||
|
Component.onCompleted: running = Qt.binding(function() { return root.loading })
|
||||||
|
padding: 0
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
implicitWidth: 16
|
||||||
|
implicitHeight: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-reload current file timer
|
||||||
|
Timer {
|
||||||
|
running: root.autoReload
|
||||||
|
interval: root.autoReloadInterval
|
||||||
|
repeat: true
|
||||||
|
// reload file on start and stop
|
||||||
|
onRunningChanged: loadSource()
|
||||||
|
onTriggered: loadSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Load current source file and update ListView's model
|
||||||
|
function loadSource()
|
||||||
|
{
|
||||||
|
if(!visible)
|
||||||
|
return;
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
var xhr = new XMLHttpRequest;
|
||||||
|
|
||||||
|
xhr.open("GET", root.source);
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
// - can't rely on 'Last-Modified' header response to verify
|
||||||
|
// that file has changed on disk (not always up-to-date)
|
||||||
|
// - instead, let QML engine evaluate whether 'text' property value has changed
|
||||||
|
if(xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
textView.setText(xhr.status === 200 ? xhr.responseText : "");
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,3 +5,4 @@ Group 1.0 Group.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
|
||||||
|
TextFileViewer 1.0 TextFileViewer.qml
|
||||||
|
|
|
@ -96,7 +96,7 @@ Panel {
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
color: Qt.lighter(palette.mid, 1.2)
|
color: Qt.lighter(palette.mid, 1.2)
|
||||||
text: "Select a Node to edit its Attributes"
|
text: "Select a Node to access its Details"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,12 +128,17 @@ Panel {
|
||||||
node: root.node
|
node: root.node
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TabBar {
|
TabBar {
|
||||||
id: tabBar
|
id: tabBar
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
width: childrenRect.width
|
width: childrenRect.width
|
||||||
position: TabBar.Footer
|
position: TabBar.Footer
|
||||||
|
currentIndex: 1
|
||||||
TabButton {
|
TabButton {
|
||||||
text: "Attributes"
|
text: "Attributes"
|
||||||
width: implicitWidth
|
width: implicitWidth
|
||||||
|
@ -150,6 +155,3 @@ Panel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import QtQuick 2.9
|
import QtQuick 2.11
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Controls 1.4 as Controls1 // SplitView
|
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 Utils 1.0
|
import Controls 1.0
|
||||||
|
|
||||||
import "common.js" as Common
|
import "common.js" as Common
|
||||||
|
|
||||||
|
@ -85,17 +85,20 @@ FocusScope {
|
||||||
id: fileSelector
|
id: fileSelector
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk[currentItem.fileProperty] : ""
|
property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk[currentItem.fileProperty] : ""
|
||||||
property string lastLoadedFile
|
onCurrentFileChanged: {
|
||||||
property date lastModTime
|
// only set text file viewer source when ListView is fully ready
|
||||||
onCurrentFileChanged: if(visible) loadCurrentFile()
|
// (either empty or fully populated with a valid currentChunk)
|
||||||
onVisibleChanged: if(visible) loadCurrentFile()
|
// to avoid going through an empty url when switching between two nodes
|
||||||
|
if(!chunksLV.count || chunksLV.currentChunk)
|
||||||
|
textFileViewer.source = Filepath.stringToUrl(currentFile);
|
||||||
|
}
|
||||||
|
|
||||||
TabButton {
|
TabButton {
|
||||||
property string fileProperty: "logFile"
|
property string fileProperty: "logFile"
|
||||||
text: "Log"
|
text: "Output"
|
||||||
padding: 4
|
padding: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
TabButton {
|
TabButton {
|
||||||
property string fileProperty: "statisticsFile"
|
property string fileProperty: "statisticsFile"
|
||||||
text: "Statistics"
|
text: "Statistics"
|
||||||
|
@ -108,152 +111,13 @@ FocusScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
TextFileViewer {
|
||||||
Layout.fillHeight: true
|
id: textFileViewer
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 0
|
|
||||||
Pane {
|
|
||||||
id: tb
|
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
Layout.fillHeight: true
|
|
||||||
padding: 0
|
|
||||||
background: Rectangle { color: Qt.darker(activePalette.window, 1.2) }
|
|
||||||
Column {
|
|
||||||
height: parent.height
|
|
||||||
ToolButton {
|
|
||||||
text: MaterialIcons.refresh
|
|
||||||
ToolTip.text: "Refresh"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
font.family: MaterialIcons.fontFamily
|
|
||||||
onClicked: loadCurrentFile(false)
|
|
||||||
}
|
|
||||||
ToolButton {
|
|
||||||
id: autoRefresh
|
|
||||||
text: MaterialIcons.timer
|
|
||||||
ToolTip.text: "Auto-Refresh when Running"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
font.family: MaterialIcons.fontFamily
|
|
||||||
checked: true
|
|
||||||
checkable: true
|
|
||||||
}
|
|
||||||
ToolButton {
|
|
||||||
text: MaterialIcons.vertical_align_top
|
|
||||||
ToolTip.text: "Scroll to Top"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
font.family: MaterialIcons.fontFamily
|
|
||||||
onClicked: logArea.cursorPosition = 0
|
|
||||||
}
|
|
||||||
ToolButton {
|
|
||||||
text: MaterialIcons.vertical_align_bottom
|
|
||||||
ToolTip.text: "Scroll to Bottom"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
font.family: MaterialIcons.fontFamily
|
|
||||||
onClicked: logArea.cursorPosition = logArea.length
|
|
||||||
}
|
|
||||||
ToolButton {
|
|
||||||
id: autoScroll
|
|
||||||
text: MaterialIcons.system_update_alt
|
|
||||||
ToolTip.text: "Auto-Scroll to Bottom"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
font.family: MaterialIcons.fontFamily
|
|
||||||
checkable: true
|
|
||||||
checked: true
|
|
||||||
}
|
|
||||||
ToolButton {
|
|
||||||
text: MaterialIcons.open_in_new
|
|
||||||
ToolTip.text: "Open Externally"
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
font.family: MaterialIcons.fontFamily
|
|
||||||
enabled: fileSelector.currentFile != ""
|
|
||||||
onClicked: Qt.openUrlExternally(Filepath.stringToUrl(fileSelector.currentFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Log display
|
|
||||||
ScrollView {
|
|
||||||
id: logScrollView
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
TextArea {
|
autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING"
|
||||||
id: logArea
|
// source is set in fileSelector
|
||||||
selectByMouse: true
|
|
||||||
selectByKeyboard: true
|
|
||||||
persistentSelection: true
|
|
||||||
font.family: "Monospace, Consolas, Monaco"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-reload current file if NodeChunk is being computed
|
|
||||||
Timer {
|
|
||||||
running: autoRefresh.checked && chunksLV.currentChunk != undefined && chunksLV.currentChunk.statusName === "RUNNING"
|
|
||||||
interval: 2000
|
|
||||||
repeat: true
|
|
||||||
// reload file on start and stop
|
|
||||||
onRunningChanged: loadCurrentFile(true)
|
|
||||||
onTriggered: loadCurrentFile(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadCurrentFile(keepCursorPosition)
|
|
||||||
{
|
|
||||||
if(keepCursorPosition == undefined)
|
|
||||||
keepCursorPosition = false
|
|
||||||
var xhr = new XMLHttpRequest;
|
|
||||||
xhr.open("GET", Filepath.stringToUrl(fileSelector.currentFile));
|
|
||||||
xhr.onreadystatechange = function() {
|
|
||||||
if(xhr.readyState == XMLHttpRequest.HEADERS_RECEIVED)
|
|
||||||
{
|
|
||||||
// if the file is already open
|
|
||||||
// check last modification date
|
|
||||||
var lastMod = new Date(xhr.getResponseHeader("Last-Modified"));
|
|
||||||
if(fileSelector.lastLoadedFile == fileSelector.currentFile
|
|
||||||
&& lastMod.getTime() == fileSelector.lastModTime.getTime() )
|
|
||||||
{
|
|
||||||
// file has not changed, don't reload it
|
|
||||||
xhr.doLoad = false;
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// file is different or last modification time has changed
|
|
||||||
fileSelector.lastModTime = lastMod
|
|
||||||
xhr.doLoad = true
|
|
||||||
}
|
|
||||||
if (xhr.readyState == XMLHttpRequest.DONE) {
|
|
||||||
// store lastLoadedFile url
|
|
||||||
fileSelector.lastLoadedFile = fileSelector.currentFile
|
|
||||||
// if responseText should not be loaded
|
|
||||||
if(!xhr.doLoad)
|
|
||||||
{
|
|
||||||
// file could not be opened, reset text and lastModTime
|
|
||||||
if(xhr.status == 0)
|
|
||||||
{
|
|
||||||
fileSelector.lastModTime = new Date()
|
|
||||||
logArea.text = ''
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// store cursor position and content position
|
|
||||||
var cursorPosition = logArea.cursorPosition;
|
|
||||||
var contentY = logScrollView.ScrollBar.vertical.position;
|
|
||||||
|
|
||||||
// replace text
|
|
||||||
logArea.text = xhr.responseText;
|
|
||||||
|
|
||||||
if(autoScroll.checked)
|
|
||||||
{
|
|
||||||
// Reset cursor position to trigger scroll to bottom
|
|
||||||
logArea.cursorPosition = 0;
|
|
||||||
logArea.cursorPosition = logArea.length;
|
|
||||||
}
|
|
||||||
else if(keepCursorPosition)
|
|
||||||
{
|
|
||||||
if(cursorPosition)
|
|
||||||
logArea.cursorPosition = cursorPosition;
|
|
||||||
logScrollView.ScrollBar.vertical.position = contentY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
9
meshroom/ui/qml/Utils/Clipboard.qml
Normal file
9
meshroom/ui/qml/Utils/Clipboard.qml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
pragma Singleton
|
||||||
|
import Meshroom.Helpers 1.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clipboard singleton object to copy values to paste buffer.
|
||||||
|
*/
|
||||||
|
ClipboardHelper {
|
||||||
|
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ singleton Colors 1.0 Colors.qml
|
||||||
SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml
|
SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml
|
||||||
Request 1.0 request.js
|
Request 1.0 request.js
|
||||||
Format 1.0 format.js
|
Format 1.0 format.js
|
||||||
# causes random crash at application exit
|
# using singleton here causes random crash at application exit
|
||||||
|
# singleton Clipboard 1.0 Clipboard.qml
|
||||||
# singleton Filepath 1.0 Filepath.qml
|
# singleton Filepath 1.0 Filepath.qml
|
||||||
# singleton Scene3DHelper 1.0 Scene3DHelper.qml
|
# singleton Scene3DHelper 1.0 Scene3DHelper.qml
|
||||||
|
|
|
@ -289,9 +289,7 @@ FloatingPane {
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "Copy Path"
|
text: "Copy Path"
|
||||||
// hidden TextEdit to copy to clipboard
|
onTriggered: Clipboard.setText(Filepath.normpath(model.source))
|
||||||
TextEdit { id: fullpath; visible: false; text: Filepath.normpath(model.source) }
|
|
||||||
onTriggered: { fullpath.selectAll(); fullpath.copy(); }
|
|
||||||
}
|
}
|
||||||
MenuSeparator {}
|
MenuSeparator {}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
|
|
|
@ -3,7 +3,10 @@ import time
|
||||||
|
|
||||||
from PySide2.QtCore import QFileSystemWatcher, QUrl, Slot, QTimer, Property, QObject
|
from PySide2.QtCore import QFileSystemWatcher, QUrl, Slot, QTimer, Property, QObject
|
||||||
from PySide2.QtQml import QQmlApplicationEngine
|
from PySide2.QtQml import QQmlApplicationEngine
|
||||||
|
try:
|
||||||
from PySide2 import shiboken2
|
from PySide2 import shiboken2
|
||||||
|
except:
|
||||||
|
import shiboken2
|
||||||
|
|
||||||
|
|
||||||
class QmlInstantEngine(QQmlApplicationEngine):
|
class QmlInstantEngine(QQmlApplicationEngine):
|
||||||
|
|
Loading…
Add table
Reference in a new issue