Merge pull request #466 from alicevision/dev_logsAndStatus

Logs display and status update improvements
This commit is contained in:
Fabien Castan 2019-06-05 23:15:30 +02:00 committed by GitHub
commit e26b3dee30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 541 additions and 213 deletions

View file

@ -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,

View file

@ -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")

View 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()

View file

@ -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)

View file

@ -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)

View 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();
}
}

View file

@ -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

View file

@ -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
}
}
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,9 @@
pragma Singleton
import Meshroom.Helpers 1.0
/**
* Clipboard singleton object to copy values to paste buffer.
*/
ClipboardHelper {
}

View file

@ -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

View file

@ -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 {

View file

@ -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):