mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-29 10:17:27 +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
|
||||
from meshroom.core import nodesDesc
|
||||
from meshroom.ui import components
|
||||
from meshroom.ui.components.clipboard import ClipboardHelper
|
||||
from meshroom.ui.components.filepath import FilepathHelper
|
||||
from meshroom.ui.components.scene3D import Scene3DHelper
|
||||
from meshroom.ui.palette import PaletteManager
|
||||
|
@ -84,17 +85,25 @@ class MeshroomApp(QApplication):
|
|||
|
||||
# expose available node types that can be instantiated
|
||||
self.engine.rootContext().setContextProperty("_nodeTypes", sorted(nodesDesc.keys()))
|
||||
|
||||
# instantiate Reconstruction object
|
||||
r = Reconstruction(parent=self)
|
||||
self.engine.rootContext().setContextProperty("_reconstruction", r)
|
||||
pm = PaletteManager(self.engine, parent=self)
|
||||
self.engine.rootContext().setContextProperty("_PaletteManager", pm)
|
||||
fpHelper = FilepathHelper(parent=self)
|
||||
self.engine.rootContext().setContextProperty("Filepath", fpHelper)
|
||||
scene3DHelper = Scene3DHelper(parent=self)
|
||||
self.engine.rootContext().setContextProperty("Scene3DHelper", scene3DHelper)
|
||||
|
||||
# those helpers should be available from QML Utils module as singletons, but:
|
||||
# - qmlRegisterUncreatableType is not yet available in PySide2
|
||||
# - declaring them as singleton in qmldir file causes random crash at exit
|
||||
# => expose them as context properties instead
|
||||
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)
|
||||
# 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.add_argument('--project', metavar='MESHROOM_FILE', type=str, required=False,
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
|
||||
def registerTypes():
|
||||
from PySide2.QtQml import qmlRegisterType
|
||||
from meshroom.ui.components.clipboard import ClipboardHelper
|
||||
from meshroom.ui.components.edge import EdgeMouseArea
|
||||
from meshroom.ui.components.filepath import FilepathHelper
|
||||
from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController
|
||||
|
||||
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(Scene3DHelper, "Meshroom.Helpers", 1, 0, "Scene3DHelper") # TODO: uncreatable
|
||||
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)
|
||||
acceptedButtons = Property(int,
|
||||
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)
|
||||
released = Signal(MouseEvent)
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
# coding:utf-8
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
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
|
||||
|
||||
|
@ -16,6 +18,61 @@ from meshroom.ui import commands
|
|||
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):
|
||||
"""
|
||||
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):
|
||||
super(ChunksMonitor, self).__init__(parent)
|
||||
self.lastModificationRecords = dict()
|
||||
self._filesTimePoller = FilesModTimePollerThread(parent=self)
|
||||
self._filesTimePoller.timesAvailable.connect(self.compareFilesTimes)
|
||||
self._pollerOutdated = False
|
||||
self.setChunks(chunks)
|
||||
# Check status files every x seconds
|
||||
# TODO: adapt frequency according to graph compute status
|
||||
self.startTimer(5000)
|
||||
|
||||
def setChunks(self, chunks):
|
||||
""" Set the list of chunks to monitor. """
|
||||
self._filesTimePoller.stop()
|
||||
self.clear()
|
||||
for chunk in chunks:
|
||||
f = chunk.statusFile
|
||||
# Store a record of {chunk: status file last modification}
|
||||
self.lastModificationRecords[chunk] = self.getFileLastModTime(f)
|
||||
# initialize last modification times to current time for all chunks
|
||||
self.lastModificationRecords[chunk] = time.time()
|
||||
# For local use, handle statusChanged emitted directly from the node chunk
|
||||
chunk.statusChanged.connect(self.onChunkStatusChanged)
|
||||
self._pollerOutdated = True
|
||||
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):
|
||||
""" Clear the list of monitored chunks """
|
||||
for ch in self.lastModificationRecords:
|
||||
ch.statusChanged.disconnect(self.onChunkStatusChanged)
|
||||
""" Clear the list of monitored chunks. """
|
||||
for chunk in self.lastModificationRecords:
|
||||
chunk.statusChanged.disconnect(self.onChunkStatusChanged)
|
||||
self.lastModificationRecords.clear()
|
||||
|
||||
def timerEvent(self, evt):
|
||||
self.checkFileTimes()
|
||||
|
||||
def onChunkStatusChanged(self):
|
||||
""" React to change of status coming from the NodeChunk itself. """
|
||||
chunk = self.sender()
|
||||
assert chunk in self.lastModificationRecords
|
||||
# Update record entry for this file so that it's up-to-date on next timerEvent
|
||||
self.lastModificationRecords[chunk] = self.getFileLastModTime(chunk.statusFile)
|
||||
# update record entry for this file so that it's up-to-date on next timerEvent
|
||||
# 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)
|
||||
|
||||
@staticmethod
|
||||
def getFileLastModTime(f):
|
||||
""" Return 'mtime' of the file if it exists, -1 otherwise. """
|
||||
return os.path.getmtime(f) if os.path.exists(f) else -1
|
||||
@property
|
||||
def statusFiles(self):
|
||||
""" Get status file paths from current chunks. """
|
||||
return [c.statusFile for c in self.lastModificationRecords.keys()]
|
||||
|
||||
def checkFileTimes(self):
|
||||
""" Check status files last modification time and compare with stored value """
|
||||
for chunk, t in self.lastModificationRecords.items():
|
||||
lastMod = self.getFileLastModTime(chunk.statusFile)
|
||||
if lastMod != t:
|
||||
self.lastModificationRecords[chunk] = lastMod
|
||||
def compareFilesTimes(self, times):
|
||||
"""
|
||||
Compare previous file modification times with results from last poll.
|
||||
Trigger chunk status update if file was modified since.
|
||||
|
||||
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()
|
||||
logging.debug("Status for node {} changed: {}".format(chunk.node, chunk.status.status))
|
||||
|
||||
chunkStatusChanged = Signal(NodeChunk, int)
|
||||
|
||||
|
@ -244,6 +318,11 @@ class UIGraph(QObject):
|
|||
self._sortedDFSChunks.clear()
|
||||
self._undoStack.clear()
|
||||
|
||||
def stopChildThreads(self):
|
||||
""" Stop all child threads. """
|
||||
self.stopExecution()
|
||||
self._chunksMonitor.stop()
|
||||
|
||||
def load(self, filepath):
|
||||
g = Graph('')
|
||||
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
|
||||
Panel 1.0 Panel.qml
|
||||
SearchBar 1.0 SearchBar.qml
|
||||
TextFileViewer 1.0 TextFileViewer.qml
|
||||
|
|
|
@ -96,7 +96,7 @@ Panel {
|
|||
}
|
||||
Label {
|
||||
color: Qt.lighter(palette.mid, 1.2)
|
||||
text: "Select a Node to edit its Attributes"
|
||||
text: "Select a Node to access its Details"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -128,28 +128,30 @@ Panel {
|
|||
node: root.node
|
||||
}
|
||||
}
|
||||
TabBar {
|
||||
id: tabBar
|
||||
|
||||
Layout.fillWidth: true
|
||||
width: childrenRect.width
|
||||
position: TabBar.Footer
|
||||
TabButton {
|
||||
text: "Attributes"
|
||||
width: implicitWidth
|
||||
padding: 4
|
||||
leftPadding: 8
|
||||
rightPadding: leftPadding
|
||||
}
|
||||
TabButton {
|
||||
text: "Log"
|
||||
width: implicitWidth
|
||||
leftPadding: 8
|
||||
rightPadding: leftPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
|
||||
Layout.fillWidth: true
|
||||
width: childrenRect.width
|
||||
position: TabBar.Footer
|
||||
currentIndex: 1
|
||||
TabButton {
|
||||
text: "Attributes"
|
||||
width: implicitWidth
|
||||
padding: 4
|
||||
leftPadding: 8
|
||||
rightPadding: leftPadding
|
||||
}
|
||||
TabButton {
|
||||
text: "Log"
|
||||
width: implicitWidth
|
||||
leftPadding: 8
|
||||
rightPadding: leftPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import QtQuick 2.9
|
||||
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 Utils 1.0
|
||||
import Controls 1.0
|
||||
|
||||
import "common.js" as Common
|
||||
|
||||
|
@ -85,17 +85,20 @@ FocusScope {
|
|||
id: fileSelector
|
||||
Layout.fillWidth: true
|
||||
property string currentFile: chunksLV.currentChunk ? chunksLV.currentChunk[currentItem.fileProperty] : ""
|
||||
property string lastLoadedFile
|
||||
property date lastModTime
|
||||
onCurrentFileChanged: if(visible) loadCurrentFile()
|
||||
onVisibleChanged: if(visible) loadCurrentFile()
|
||||
|
||||
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)
|
||||
textFileViewer.source = Filepath.stringToUrl(currentFile);
|
||||
}
|
||||
|
||||
TabButton {
|
||||
property string fileProperty: "logFile"
|
||||
text: "Log"
|
||||
text: "Output"
|
||||
padding: 4
|
||||
}
|
||||
|
||||
TabButton {
|
||||
property string fileProperty: "statisticsFile"
|
||||
text: "Statistics"
|
||||
|
@ -108,152 +111,13 @@ FocusScope {
|
|||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillHeight: true
|
||||
TextFileViewer {
|
||||
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.fillHeight: true
|
||||
TextArea {
|
||||
id: logArea
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
persistentSelection: true
|
||||
font.family: "Monospace, Consolas, Monaco"
|
||||
}
|
||||
}
|
||||
Layout.fillHeight: true
|
||||
autoReload: chunksLV.currentChunk !== undefined && chunksLV.currentChunk.statusName === "RUNNING"
|
||||
// source is set in fileSelector
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
Request 1.0 request.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 Scene3DHelper 1.0 Scene3DHelper.qml
|
||||
|
|
|
@ -289,9 +289,7 @@ FloatingPane {
|
|||
}
|
||||
MenuItem {
|
||||
text: "Copy Path"
|
||||
// hidden TextEdit to copy to clipboard
|
||||
TextEdit { id: fullpath; visible: false; text: Filepath.normpath(model.source) }
|
||||
onTriggered: { fullpath.selectAll(); fullpath.copy(); }
|
||||
onTriggered: Clipboard.setText(Filepath.normpath(model.source))
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem {
|
||||
|
|
|
@ -3,7 +3,10 @@ import time
|
|||
|
||||
from PySide2.QtCore import QFileSystemWatcher, QUrl, Slot, QTimer, Property, QObject
|
||||
from PySide2.QtQml import QQmlApplicationEngine
|
||||
from PySide2 import shiboken2
|
||||
try:
|
||||
from PySide2 import shiboken2
|
||||
except:
|
||||
import shiboken2
|
||||
|
||||
|
||||
class QmlInstantEngine(QQmlApplicationEngine):
|
||||
|
|
Loading…
Add table
Reference in a new issue