From acc85127c502d52a3b665df8954622807c99e58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 18 Nov 2015 17:13:41 +0100 Subject: [PATCH 1/2] Track browser tab visibility, only activate webcam/gcode viewer when visible This might help with #1065 if indeed is related to background tab suspending behaviours in browsers, but is a completely blind guess at this point since I still have not been able to reproduce that issue myself. Backported. (cherry picked from commit 720cdad) --- .../static/gcodeviewer/js/renderer.js | 2 +- src/octoprint/static/js/app/main.js | 105 +++++++++++++++++- .../static/js/app/viewmodels/control.js | 69 ++++++++---- .../static/js/app/viewmodels/gcode.js | 12 +- src/octoprint/templates/initscript.jinja2 | 8 ++ 5 files changed, 165 insertions(+), 31 deletions(-) diff --git a/src/octoprint/static/gcodeviewer/js/renderer.js b/src/octoprint/static/gcodeviewer/js/renderer.js index 2f046d90..32f2195a 100644 --- a/src/octoprint/static/gcodeviewer/js/renderer.js +++ b/src/octoprint/static/gcodeviewer/js/renderer.js @@ -379,7 +379,7 @@ GCODE.renderer = (function(){ }; var drawLayer = function(layerNum, fromProgress, toProgress, isNotCurrentLayer){ - console.log("Drawing layer " + layerNum + " from " + fromProgress + " to " + toProgress + " (current: " + !isNotCurrentLayer + ")"); + log.trace("Drawing layer " + layerNum + " from " + fromProgress + " to " + toProgress + " (current: " + !isNotCurrentLayer + ")"); var i; diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index bab804bb..02930e08 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -7,6 +7,72 @@ $(function() { log.setLevel(CONFIG_DEBUG ? "debug" : "info"); + //~~ setup browser and internal tab tracking (in 1.3.0 that will be + // much nicer with the global OctoPrint object...) + + var tabTracking = (function() { + var exports = { + browserTabVisibility: undefined, + selectedTab: undefined + }; + + var browserVisibilityCallbacks = []; + + var getHiddenProp = function() { + var prefixes = ["webkit", "moz", "ms", "o"]; + + // if "hidden" is natively supported just return it + if ("hidden" in document) { + return "hidden" + } + + // otherwise loop over all the known prefixes until we find one + var vendorPrefix = _.find(prefixes, function(prefix) { + return (prefix + "Hidden" in document); + }); + if (vendorPrefix !== undefined) { + return vendorPrefix + "Hidden"; + } + + // nothing found + return undefined; + }; + + var isHidden = function() { + var prop = getHiddenProp(); + if (!prop) return false; + + return document[prop]; + }; + + var updateBrowserVisibility = function() { + var visible = !isHidden(); + exports.browserTabVisible = visible; + _.each(browserVisibilityCallbacks, function(callback) { + callback(visible); + }) + }; + + // register for browser visibility tracking + + var prop = getHiddenProp(); + if (!prop) return undefined; + + var eventName = prop.replace(/[H|h]idden/, "") + "visibilitychange"; + document.addEventListener(eventName, updateBrowserVisibility); + + updateBrowserVisibility(); + + // exports + + exports.isVisible = function() { return !isHidden() }; + exports.onBrowserVisibilityChange = function(callback) { + browserVisibilityCallbacks.push(callback); + }; + + return exports; + })(); + //~~ AJAX setup // work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at @@ -67,6 +133,18 @@ $(function() { // the view model map is our basic look up table for dependencies that may be injected into other view models var viewModelMap = {}; + // We put our tabTracking into the viewModelMap as a workaround until + // our global OctoPrint object becomes available in 1.3.0. This way + // we'll still be able to access it in our view models. + // + // NOTE TO DEVELOPERS: Do NOT depend on this dependency in your custom + // view models. It is ONLY provided for the core application to be able + // to backport a fix from the 1.3.0 development branch and WILL BE + // REMOVED once 1.3.0 gets released without any fallback! + // + // TODO: Remove with release of 1.3.0 + viewModelMap.tabTracking = tabTracking; + // Fix Function#name on browsers that do not support it (IE): // see: http://stackoverflow.com/questions/6903762/function-name-not-supported-in-ie if (!(function f() {}).name) { @@ -371,16 +449,22 @@ $(function() { $('.nav-pills, .nav-tabs').tabdrop(); // Allow components to react to tab change - var tabs = $('#tabs a[data-toggle="tab"]'); - tabs.on('show', function (e) { - var current = e.target.hash; - var previous = e.relatedTarget.hash; + var onTabChange = function(current, previous) { + log.debug("Selected OctoPrint tab changed: previous = " + previous + ", current = " + current); + tabTracking.selectedTab = current; _.each(allViewModels, function(viewModel) { if (viewModel.hasOwnProperty("onTabChange")) { viewModel.onTabChange(current, previous); } }); + }; + + var tabs = $('#tabs a[data-toggle="tab"]'); + tabs.on('show', function (e) { + var current = e.target.hash; + var previous = e.relatedTarget.hash; + onTabChange(current, previous); }); tabs.on('shown', function (e) { @@ -394,6 +478,8 @@ $(function() { }); }); + onTabChange(OCTOPRINT_INITIAL_TAB); + // Fix input element click problems on dropdowns $(".dropdown input, .dropdown label").click(function(e) { e.stopPropagation(); @@ -485,11 +571,22 @@ $(function() { }); log.info("... binding done"); + // startup complete _.each(allViewModels, function(viewModel) { if (viewModel.hasOwnProperty("onStartupComplete")) { viewModel.onStartupComplete(); } }); + + // make sure we can track the browser tab visibility + tabTracking.onBrowserVisibilityChange(function(status) { + log.debug("Browser tab is now " + (status ? "visible" : "hidden")); + _.each(allViewModels, function(viewModel) { + if (viewModel.hasOwnProperty("onBrowserTabVisibilityChange")) { + viewModel.onBrowserTabVisibilityChange(status); + } + }); + }); }; if (!_.has(viewModelMap, "settingsViewModel")) { diff --git a/src/octoprint/static/js/app/viewmodels/control.js b/src/octoprint/static/js/app/viewmodels/control.js index 7b76895f..e1a50344 100644 --- a/src/octoprint/static/js/app/viewmodels/control.js +++ b/src/octoprint/static/js/app/viewmodels/control.js @@ -5,6 +5,9 @@ $(function() { self.loginState = parameters[0]; self.settings = parameters[1]; + // TODO remove with release of 1.3.0 and switch to OctoPrint.coreui usage + self.tabTracking = parameters[2]; + self._createToolEntry = function () { return { name: ko.observable(), @@ -415,31 +418,51 @@ $(function() { self.onSettingsBeforeSave = self.updateRotatorWidth; + self._disableWebcam = function() { + // only disable webcam stream if tab is out of focus for more than 5s, otherwise we might cause + // more load by the constant connection creation than by the actual webcam stream + self.webcamDisableTimeout = setTimeout(function () { + $("#webcam_image").attr("src", ""); + }, 5000); + }; + + self._enableWebcam = function() { + if (self.tabTracking.selectedTab != "#control" || !self.tabTracking.browserTabVisible) { + return; + } + + if (self.webcamDisableTimeout != undefined) { + clearTimeout(self.webcamDisableTimeout); + } + var webcamImage = $("#webcam_image"); + var currentSrc = webcamImage.attr("src"); + if (currentSrc === undefined || currentSrc.trim() == "") { + var newSrc = CONFIG_WEBCAM_STREAM; + if (CONFIG_WEBCAM_STREAM.lastIndexOf("?") > -1) { + newSrc += "&"; + } else { + newSrc += "?"; + } + newSrc += new Date().getTime(); + + self.updateRotatorWidth(); + webcamImage.attr("src", newSrc); + } + }; + self.onTabChange = function (current, previous) { if (current == "#control") { - if (self.webcamDisableTimeout != undefined) { - clearTimeout(self.webcamDisableTimeout); - } - var webcamImage = $("#webcam_image"); - var currentSrc = webcamImage.attr("src"); - if (currentSrc === undefined || currentSrc.trim() == "") { - var newSrc = CONFIG_WEBCAM_STREAM; - if (CONFIG_WEBCAM_STREAM.lastIndexOf("?") > -1) { - newSrc += "&"; - } else { - newSrc += "?"; - } - newSrc += new Date().getTime(); - - self.updateRotatorWidth(); - webcamImage.attr("src", newSrc); - } + self._enableWebcam(); } else if (previous == "#control") { - // only disable webcam stream if tab is out of focus for more than 5s, otherwise we might cause - // more load by the constant connection creation than by the actual webcam stream - self.webcamDisableTimeout = setTimeout(function () { - $("#webcam_image").attr("src", ""); - }, 5000); + self._disableWebcam(); + } + }; + + self.onBrowserTabVisibilityChange = function(status) { + if (status) { + self._enableWebcam(); + } else { + self._disableWebcam(); } }; @@ -565,7 +588,7 @@ $(function() { OCTOPRINT_VIEWMODELS.push([ ControlViewModel, - ["loginStateViewModel", "settingsViewModel"], + ["loginStateViewModel", "settingsViewModel", "tabTracking"], "#control" ]); }); diff --git a/src/octoprint/static/js/app/viewmodels/gcode.js b/src/octoprint/static/js/app/viewmodels/gcode.js index eb8c358d..d82c0801 100644 --- a/src/octoprint/static/js/app/viewmodels/gcode.js +++ b/src/octoprint/static/js/app/viewmodels/gcode.js @@ -5,6 +5,9 @@ $(function() { self.loginState = parameters[0]; self.settings = parameters[1]; + // TODO remove with release of 1.3.0 and switch to OctoPrint.coreui usage + self.tabTracking = parameters[2]; + self.ui_progress_percentage = ko.observable(); self.ui_progress_type = ko.observable(); self.ui_progress_text = ko.computed(function() { @@ -358,7 +361,7 @@ $(function() { if(self.loadedFilename && self.loadedFilename == data.job.file.name && self.loadedFileDate == data.job.file.date) { - if (self.currentlyPrinting && self.renderer_syncProgress() && !self.waitForApproval()) { + if (self.tabTracking.browserTabVisible && self.tabActive && self.currentlyPrinting && self.renderer_syncProgress() && !self.waitForApproval()) { var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(data.progress.completion); if(cmdIndex){ GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd); @@ -504,13 +507,16 @@ $(function() { self.onBeforeBinding = function() { self.initialize(); - } + }; + self.onTabChange = function(current, previous) { + self.tabActive = current == "#gcode"; + }; } OCTOPRINT_VIEWMODELS.push([ GcodeViewModel, - ["loginStateViewModel", "settingsViewModel"], + ["loginStateViewModel", "settingsViewModel", "tabTracking"], "#gcode" ]); }); diff --git a/src/octoprint/templates/initscript.jinja2 b/src/octoprint/templates/initscript.jinja2 index 8d212c62..5763a7ee 100644 --- a/src/octoprint/templates/initscript.jinja2 +++ b/src/octoprint/templates/initscript.jinja2 @@ -33,4 +33,12 @@ var OCTOPRINT_VIEWMODELS = []; var ADDITIONAL_VIEWMODELS = []; var OCTOPRINT_ADDITIONAL_BINDINGS = []; + + {% if templates.tab.order %} + {% set first_tab = templates.tab.order[0] %} + {% set entry, data = templates.tab.entries[first_tab] %} + var OCTOPRINT_INITIAL_TAB = "#{{ data._div }}"; + {% else %} + var OCTOPRINT_INITIAL_TAB = undefined; + {% endif %} From a52a5ca3384e5d782108ddb7c4709dda02d28667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 20 Nov 2015 11:40:06 +0100 Subject: [PATCH 2/2] GCodeViewer: Even if off screen render 100% view on PrintDone event Otherwise the GCODE Viewer will get stuck "half way through" if "sync with progress" is enabled but the tab/browser tab loses focus during printing and only regains it after the print has finished. --- .../static/js/app/viewmodels/gcode.js | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/octoprint/static/js/app/viewmodels/gcode.js b/src/octoprint/static/js/app/viewmodels/gcode.js index d82c0801..4f82f6f5 100644 --- a/src/octoprint/static/js/app/viewmodels/gcode.js +++ b/src/octoprint/static/js/app/viewmodels/gcode.js @@ -342,6 +342,21 @@ $(function() { self._processData(data); }; + self._renderPercentage = function(percentage) { + var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(percentage); + if (!cmdIndex) return; + + GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd); + GCODE.ui.updateLayerInfo(cmdIndex.layer); + + if (self.layerSlider != undefined) { + self.layerSlider.slider("setValue", cmdIndex.layer); + } + if (self.layerCommandSlider != undefined) { + self.layerCommandSlider.slider("setValue", [0, cmdIndex.cmd]); + } + }; + self._processData = function(data) { if (!data.job.file || !data.job.file.name && (self.loadedFilename || self.loadedFileDate)) { self.waitForApproval(false); @@ -362,18 +377,7 @@ $(function() { && self.loadedFilename == data.job.file.name && self.loadedFileDate == data.job.file.date) { if (self.tabTracking.browserTabVisible && self.tabActive && self.currentlyPrinting && self.renderer_syncProgress() && !self.waitForApproval()) { - var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(data.progress.completion); - if(cmdIndex){ - GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd); - GCODE.ui.updateLayerInfo(cmdIndex.layer); - - if (self.layerSlider != undefined) { - self.layerSlider.slider("setValue", cmdIndex.layer); - } - if (self.layerCommandSlider != undefined) { - self.layerCommandSlider.slider("setValue", [0, cmdIndex.cmd]); - } - } + self._renderPercentage(data.progress.completion); } self.errorCount = 0 } else { @@ -397,6 +401,12 @@ $(function() { } }; + self.onEventPrintDone = function() { + if (self.renderer_syncProgress() && !self.waitForApproval()) { + self._renderPercentage(100.0); + } + }; + self.approveLargeFile = function() { self.waitForApproval(false); self.loadFile(self.selectedFile.name(), self.selectedFile.date());