mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-07-20 01:57:16 +02:00
[ui] New TextFileViewer for displaying log files
Introduce a new TextFileViewer component with auto-reload feature based on a ListView instead of a TextArea for performance reasons. Uses the text content split on line breaks as ListView's model. Features: * auto-scroll to bottom * display line numbers * customizable line delegates (e.g.: display a real progress bar) * color lines according to log level
This commit is contained in:
parent
438622a14b
commit
55dba55d19
4 changed files with 363 additions and 173 deletions
331
meshroom/ui/qml/Controls/TextFileViewer.qml
Normal file
331
meshroom/ui/qml/Controls/TextFileViewer.qml
Normal file
|
@ -0,0 +1,331 @@
|
||||||
|
import QtQuick 2.11
|
||||||
|
import QtQml.Models 2.11
|
||||||
|
import QtQuick.Controls 2.5
|
||||||
|
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(false)
|
||||||
|
}
|
||||||
|
ToolButton {
|
||||||
|
text: MaterialIcons.vertical_align_top
|
||||||
|
ToolTip.text: "Scroll to Top"
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
font.family: MaterialIcons.fontFamily
|
||||||
|
onClicked: textView.positionViewAtBeginning()
|
||||||
|
}
|
||||||
|
ToolButton {
|
||||||
|
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, keepPosition) {
|
||||||
|
// store cursor position and content position
|
||||||
|
var topIndex = firstVisibleIndex();
|
||||||
|
var scrollToBottom = atYEnd;
|
||||||
|
// replace text
|
||||||
|
text = value;
|
||||||
|
|
||||||
|
if(scrollToBottom)
|
||||||
|
positionViewAtEnd();
|
||||||
|
else if(topIndex !== firstVisibleIndex() && keepPosition)
|
||||||
|
positionViewAtIndex(topIndex, 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(true)
|
||||||
|
onTriggered: loadSource(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current source file and update ListView's model
|
||||||
|
function loadSource(keepPosition = false)
|
||||||
|
{
|
||||||
|
if(!visible)
|
||||||
|
return;
|
||||||
|
loading = true;
|
||||||
|
var xhr = new XMLHttpRequest;
|
||||||
|
xhr.open("GET", root.source);
|
||||||
|
xhr.onload = 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
|
||||||
|
textView.setText(xhr.status === 200 ? xhr.responseText : "", keepPosition);
|
||||||
|
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.12
|
||||||
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,13 @@ 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
|
|
||||||
property date lastModTime
|
|
||||||
onCurrentFileChanged: if(visible) loadCurrentFile()
|
|
||||||
onVisibleChanged: if(visible) loadCurrentFile()
|
|
||||||
|
|
||||||
|
|
||||||
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 +104,12 @@ FocusScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
TextFileViewer {
|
||||||
Layout.fillHeight: true
|
|
||||||
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: Filepath.stringToUrl(fileSelector.currentFile)
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue