diff --git a/scripts/developer/debugging/debugWindow.js b/scripts/developer/debugging/debugWindow.js index c0474b97c5c..6c5e0206371 100644 --- a/scripts/developer/debugging/debugWindow.js +++ b/scripts/developer/debugging/debugWindow.js @@ -9,20 +9,20 @@ // (function() { // BEGIN LOCAL_SCOPE - +"use strict" //check if script already running. -var scriptData = ScriptDiscoveryService.getRunning(); -var scripts = scriptData.filter(function (datum) { return datum.name === 'debugWindow.js'; }); +const scriptData = ScriptDiscoveryService.getRunning(); +const scripts = scriptData.filter(function (datum) { return datum.name === 'debugWindow.js'; }); if (scripts.length >= 2) { //2nd instance of the script is too much ScriptDiscoveryService.stopScript(scripts[1].url); return; } -var SUPPRESS_DEFAULT_SCRIPTS_MENU_NAME = "Developer" -var SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME = "Suppress messages from default scripts in Debug Window"; -var DEBUG_WINDOW_SUPPRESS_DEFAULTS_SCRIPTS = 'debugWindowSuppressDefaultScripts'; -var suppressDefaultScripts = Settings.getValue(DEBUG_WINDOW_SUPPRESS_DEFAULTS_SCRIPTS, false) +const SUPPRESS_DEFAULT_SCRIPTS_MENU_NAME = "Developer" +const SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME = "Suppress messages from default scripts in Debug Window"; +const DEBUG_WINDOW_SUPPRESS_DEFAULTS_SCRIPTS = 'debugWindowSuppressDefaultScripts'; +let suppressDefaultScripts = Settings.getValue(DEBUG_WINDOW_SUPPRESS_DEFAULTS_SCRIPTS, false) Menu.addMenuItem({ menuName: SUPPRESS_DEFAULT_SCRIPTS_MENU_NAME, menuItemName: SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME, @@ -37,30 +37,36 @@ Menu.menuItemEvent.connect(function(menuItem) { }); // Set up the qml ui -var qml = Script.resolvePath('debugWindow.qml'); +const qml = Script.resolvePath('debugWindow.qml'); -var HMD_DEBUG_WINDOW_GEOMETRY_KEY = 'hmdDebugWindowGeometry'; -var hmdDebugWindowGeometryValue = Settings.getValue(HMD_DEBUG_WINDOW_GEOMETRY_KEY); +const HMD_DEBUG_WINDOW_GEOMETRY_KEY = 'hmdDebugWindowGeometry'; +const hmdDebugWindowGeometryValue = Settings.getValue(HMD_DEBUG_WINDOW_GEOMETRY_KEY); -var windowWidth = 400; -var windowHeight = 900; +let windowWidth = 400; +let windowHeight = 900; -var hasPosition = false; -var windowX = 0; -var windowY = 0; +let hasPosition = false; +let windowX = 0; +let windowY = 0; if (hmdDebugWindowGeometryValue !== '') { - var geometry = JSON.parse(hmdDebugWindowGeometryValue); + const geometry = JSON.parse(hmdDebugWindowGeometryValue); if ((geometry.x !== 0) && (geometry.y !== 0)) { windowWidth = geometry.width; windowHeight = geometry.height; windowX = geometry.x; windowY = geometry.y; + + // Constrain window position to within the viewport + const viewportDimensions = Controller.getViewportDimensions(); + if (windowX > viewportDimensions.x) windowX = viewportDimensions.x - windowWidth; + if (windowY > viewportDimensions.y) windowX = viewportDimensions.y - windowHeight; + hasPosition = true; } } -var window = new OverlayWindow({ +const window = new OverlayWindow({ title: 'Debug Window', source: qml, width: windowWidth, height: windowHeight, @@ -92,18 +98,23 @@ function shouldLogMessage(scriptFileName) { || (scriptFileName !== "defaultScripts.js" && scriptFileName != "controllerScripts.js"); } -var getFormattedDate = function() { - var date = new Date(); +const getFormattedDate = function() { + const date = new Date(); return date.getMonth() + "/" + date.getDate() + " " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); }; -var sendToLogWindow = function(type, message, scriptFileName) { +const sendToLogWindow = function(type, message, scriptFileName) { if (shouldLogMessage(scriptFileName)) { - var typeFormatted = ""; + let typeFormatted = ""; if (type) { typeFormatted = type + " - "; } - window.sendToQml("[" + getFormattedDate() + "] " + "[" + scriptFileName + "] " + typeFormatted + message); + window.sendToQml({ + date: getFormattedDate(), + scriptFileName: scriptFileName, + type: type, + message: message, + }); } }; @@ -132,7 +143,7 @@ Script.scriptEnding.connect(function () { Menu.isOptionChecked(SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME)); Menu.removeMenuItem(SUPPRESS_DEFAULT_SCRIPTS_MENU_NAME, SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME); - var geometry = JSON.stringify({ + const geometry = JSON.stringify({ x: window.position.x, y: window.position.y, width: window.size.x, diff --git a/scripts/developer/debugging/debugWindow.qml b/scripts/developer/debugging/debugWindow.qml index 23708033351..3619d3078e8 100644 --- a/scripts/developer/debugging/debugWindow.qml +++ b/scripts/developer/debugging/debugWindow.qml @@ -8,13 +8,18 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.0 import Hifi 1.0 as Hifi +import TabletScriptingInterface 1.0 +import stylesUit 1.0 -Rectangle { + +ColumnLayout { id: root + HifiConstants { id: hifi } width: parent ? parent.width : 100 height: parent ? parent.height : 100 @@ -23,22 +28,246 @@ Rectangle { property var channel; - TextArea { - id: textArea - width: parent.width - height: parent.height - text:"" + Rectangle { + Layout.fillHeight: true + Layout.preferredWidth: parent.width + color: hifi.colors.darkGray + + ScrollView { + id: logView + anchors.fill: parent + clip: true + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + + TextArea { + id: textArea + width: root.width + height: parent.height - bottomRow.height + wrapMode: TextArea.Wrap + placeholderText: qsTr("Debug messages from your running scripts will appear here...") + placeholderTextColor: hifi.colors.lightGrayText + readOnly: true + selectByMouse: true + textFormat: TextEdit.RichText + + background: Rectangle { + color: "transparent" + } + text:"" + + onTextChanged: { + if (logView.scrollDelay) { + // Scroll to the bottom, but only once within a short period of time + // rather than for every message. + scrollTimer.restart(); + } else { + // Scroll to the bottom, immediately! + logView.scrollToBottom(); + logView.scrollDelay = true; + } + } + } + + // Do we want to use the timer for the next scroll (true), + // or scroll immediately (false); + property bool scrollDelay: true + + Timer { + id: scrollTimer + interval: 1000; running: false; repeat: false; + onTriggered: { + logView.scrollToBottom(); + } + } + + function scrollToBottom() { // This is not strictly necessary now, but is kept as a fail-safe + logView.ScrollBar.vertical.position = 1.0 - logView.ScrollBar.vertical.size; + } + } + + } + + RowLayout { + id: bottomRow + Layout.preferredWidth: parent.width + + // Helpful text + Text { + Layout.fillWidth: true + Layout.preferredWidth: parent.width + Layout.maximumHeight: selectScriptName.height + text: "Clear view and filter logs by script name" + color: hifi.colors.lightGrayText + horizontalAlignment: Text.AlignRight + wrapMode: Text.WordWrap + clip: true + + + } + + Text { + text: "-->" + color: hifi.colors.lightGrayText + horizontalAlignment: Text.AlignHCenter + clip:true + } + + // Drop down menu with all seen script names + ComboBox { + id: selectScriptName + Layout.preferredWidth: 150 + Layout.preferredHeight: 35; + padding: 0 + model: scriptNames + + background: Rectangle { + id: comboBoxBackground; + color: "#333"; + radius: 10; + } + + contentItem: Text { + anchors.centerIn: parent; + width: parent.width; + text: selectScriptName.displayText; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + elide: Text.ElideRight; + color: "white"; + } + + popup: Popup { + width: selectScriptName.width + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: selectScriptName.popup.visible ? selectScriptName.delegateModel : null + currentIndex: selectScriptName.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator { } + } + + background: Rectangle { + color: Qt.rgba(0,0,0,0.9) + radius: 10 + } + + onVisibleChanged: { + Tablet.playSound(TabletEnums.ButtonClicked); + } + } + + indicator: Canvas { + id: canvas + x: selectScriptName.width - width - selectScriptName.rightPadding + y: selectScriptName.topPadding + (selectScriptName.availableHeight - height) / 2 + width: 12 + height: 8 + contextType: "2d" + + Connections { + target: selectScriptName + function onPressedChanged() { canvas.requestPaint(); } + } + + onPaint: { + context.reset(); + context.moveTo(0, 0); + context.lineTo(width, 0); + context.lineTo(width / 2, height); + context.closePath(); + context.fillStyle = "white"; + context.fill(); + } + } + + delegate: ItemDelegate { + contentItem: Text { + text: model.name + color: "white" + font: selectScriptName.font + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + background: Rectangle { + color: highlighted ? "gray" : "transparent"; + radius: 10 + } + highlighted: scriptNames.highlightedIndex == index + } + + onActivated: { + console.log("index: ", index); + console.log("currentText:", currentText); + if (index == 0) { + currentFilter = null; + } else { + currentFilter = currentText; + } + textArea.clear() + } + + } } - function fromScript(message) { - var MAX_LINE_COUNT = 2000; - var TRIM_LINES = 500; - if (textArea.lineCount > MAX_LINE_COUNT) { - var lines = textArea.text.split('\n'); - lines.splice(0, TRIM_LINES); - textArea.text = lines.join('\n'); + ListModel { + id: scriptNames + ListElement { + name: "No Filter" + } + } + + property var scriptNamesSet: new Set() + + property var currentFilter: null; + + function fromScript(line) { + const MAX_LINE_COUNT = 2000; // post-wrap lines + const TRIM_LINES = 50; // pre-wrap lines + if (textArea.lineCount > MAX_LINE_COUNT) { // textArea.lineCount represents the number of visual line breaks after text wrap, so is not an accurate count of the number of log lines. + + let charIndex = -1; + for (let i = 0; i < TRIM_LINES; i++) { + charIndex = textArea.text.indexOf('', charIndex + 1); // This seems to result in removing ~3x the number of log lines then we requested. + } + // Here we use .remove, as editing the .text property directly results in breaking the built-in automatic scrolling when new lines are added. + textArea.remove(0, charIndex + 7); // + the length of + } + switch(line.type) { + case "WARNING": + line.color = hifi.colors.orangeHighlight + break; + case "ERROR": + line.color = hifi.colors.redHighlight + break; + case "INFO": + line.color = hifi.colors.indigoHighlight + break + default: + line.color = hifi.colors.faintGray + } + + // Preserved formatting of text when used in html + const message = line.message + .replace(//g, ">") + .replace(/\n/g, "
"); + + if (!currentFilter || currentFilter == line.scriptFileName) { + // `white-space: pre-wrap` preserves white space at the start of lines + textArea.append(`[${line.date}] [${line.scriptFileName}] ${line.type.length > 0 ? line.type+' - ' : ""}${message}`); + } + + if (!scriptNamesSet.has(line.scriptFileName)) { + const scriptNameItem = {"name": line.scriptFileName}; + scriptNamesSet.add(line.scriptFileName); + scriptNames.append(scriptNameItem); } - textArea.append(message); } function clearWindow() {