www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 593153eebe3ce14a3f88ea995149d49c46ed4e72
parent 5e5b5677822e3247703bf5f509b71f511d709871
Author: Adomas Venčkauskas <adomas.ven@gmail.com>
Date:   Fri, 30 Mar 2018 15:31:53 +0300

Adds a progress bar for non quick-format integration actions

The progress percentage is based on the most recent transaction
(or undeterminate if this is the first session transaction)

Fix undefined function call error

Diffstat:
Mchrome/content/zotero-platform/mac/integration.css | 4++++
Achrome/content/zotero/integration/progressBar.js | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/integration/progressBar.xul | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/connector/httpIntegrationClient.js | 6++----
Mchrome/content/zotero/xpcom/connector/server_connector.js | 12+++++++-----
Mchrome/content/zotero/xpcom/integration.js | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mchrome/content/zotero/xpcom/utilities_internal.js | 12++++++++++++
Mtest/tests/integrationTest.js | 3+++
8 files changed, 339 insertions(+), 29 deletions(-)

diff --git a/chrome/content/zotero-platform/mac/integration.css b/chrome/content/zotero-platform/mac/integration.css @@ -14,6 +14,10 @@ body[multiline="true"] { width: 800px; } +#quick-format-dialog.progress-bar #quick-format-deck { + height: 37px; +} + #quick-format-search { background: white; -moz-appearance: searchfield; diff --git a/chrome/content/zotero/integration/progressBar.js b/chrome/content/zotero/integration/progressBar.js @@ -0,0 +1,129 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2018 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see <http://www.gnu.org/licenses/>. + + ***** END LICENSE BLOCK ***** +*/ +Components.utils.import("resource://gre/modules/Services.jsm"); + +var Zotero_ProgressBar = new function () { + var initialized, io; + + /** + * Pre-initialization, when the dialog has loaded but has not yet appeared + */ + this.onDOMContentLoaded = function(event) { + if(event.target === document) { + initialized = true; + io = window.arguments[0].wrappedJSObject; + if (io.onLoad) { + io.onLoad(_onProgress); + } + + // Only hide chrome on Windows or Mac + if(Zotero.isMac) { + document.documentElement.setAttribute("drawintitlebar", true); + } else if(Zotero.isWin) { + document.documentElement.setAttribute("hidechrome", true); + } + + new WindowDraggingElement(document.getElementById("quick-format-dialog"), window); + + } + }; + + /** + * Initialize add citation dialog + */ + this.onLoad = function(event) { + if(event.target !== document) return; + // make sure we are visible + window.setTimeout(function() { + // window.resizeTo(window.outerWidth, window.outerHeight); + var screenX = window.screenX; + var screenY = window.screenY; + var xRange = [window.screen.availLeft, window.screen.width-window.outerWidth]; + var yRange = [window.screen.availTop, window.screen.height-window.outerHeight]; + if (screenX < xRange[0] || screenX > xRange[1] || screenY < yRange[0] || screenY > yRange[1]) { + var targetX = Math.max(Math.min(screenX, xRange[1]), xRange[0]); + var targetY = Math.max(Math.min(screenY, yRange[1]), yRange[0]); + Zotero.debug("Moving window to "+targetX+", "+targetY); + window.moveTo(targetX, targetY); + } + }, 0); + + window.focus(); + }; + + /** + * Called when progress changes + */ + function _onProgress(percent) { + var meter = document.getElementById("quick-format-progress-meter"); + if(percent === null) { + meter.mode = "undetermined"; + } else { + meter.mode = "determined"; + meter.value = Math.round(percent); + } + } + + /** + * Resizes windows + * @constructor + */ + var Resizer = function(panel, targetWidth, targetHeight, pixelsPerStep, stepsPerSecond) { + this.panel = panel; + this.curWidth = panel.clientWidth; + this.curHeight = panel.clientHeight; + this.difX = (targetWidth ? targetWidth - this.curWidth : 0); + this.difY = (targetHeight ? targetHeight - this.curHeight : 0); + this.step = 0; + this.steps = Math.ceil(Math.max(Math.abs(this.difX), Math.abs(this.difY))/pixelsPerStep); + this.timeout = (1000/stepsPerSecond); + + var me = this; + this._animateCallback = function() { me.animate() }; + }; + + /** + * Performs a step of the animation + */ + Resizer.prototype.animate = function() { + if(this.stopped) return; + this.step++; + this.panel.sizeTo(this.curWidth+Math.round(this.step*this.difX/this.steps), + this.curHeight+Math.round(this.step*this.difY/this.steps)); + if(this.step !== this.steps) { + window.setTimeout(this._animateCallback, this.timeout); + } + }; + + /** + * Halts resizing + */ + Resizer.prototype.stop = function() { + this.stopped = true; + }; +} + +window.addEventListener("DOMContentLoaded", Zotero_ProgressBar.onDOMContentLoaded, false); +window.addEventListener("load", Zotero_ProgressBar.onLoad, false); diff --git a/chrome/content/zotero/integration/progressBar.xul b/chrome/content/zotero/integration/progressBar.xul @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<!-- + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2018 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see <http://www.gnu.org/licenses/>. + + ***** END LICENSE BLOCK ***** +--> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://global/skin/browser.css" type="text/css"?> +<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?> +<?xml-stylesheet href="chrome://zotero/skin/integration.css" type="text/css"?> +<?xml-stylesheet href="chrome://zotero-platform/content/integration.css" type="text/css"?> +<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd"> + +<window + id="quick-format-dialog" + class="progress-bar" + orient="vertical" + title="&zotero.progress.title;" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + persist="screenX screenY"> + + <script src="../include.js"/> + <script src="windowDraggingUtils.js" type="text/javascript"/> + <script src="progressBar.js" type="text/javascript"/> + + <box orient="horizontal" id="quick-format-entry"> + <deck id="quick-format-deck" selectedIndex="0" flex="1"> + <progressmeter id="quick-format-progress-meter" mode="undetermined" value="0" flex="1"/> + </deck> + </box> +</window> diff --git a/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js b/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js @@ -85,8 +85,7 @@ for (let method of ["activate", "canInsertField", "displayAlert", "getDocumentDa // @NOTE Currently unused, prompts are done using the connector Zotero.HTTPIntegrationClient.Document.prototype._displayAlert = async function(dialogText, icon, buttons) { - var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService); + var ps = Services.prompt; var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_OK) + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING); @@ -172,8 +171,7 @@ Zotero.HTTPIntegrationClient.Field.prototype.getText = async function() { Zotero.HTTPIntegrationClient.Field.prototype.setText = async function(text, isRich) { // The HTML will be stripped by Google Docs and and since we're // caching this value, we need to strip it ourselves - var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); + var wm = Services.wm; var win = wm.getMostRecentWindow('navigator:browser'); var doc = new win.DOMParser().parseFromString(text, "text/html"); this._text = doc.documentElement.textContent; diff --git a/chrome/content/zotero/xpcom/connector/server_connector.js b/chrome/content/zotero/xpcom/connector/server_connector.js @@ -484,6 +484,12 @@ Zotero.Server.Connector.SavePage.prototype = { * @param {Function} sendResponseCallback function to send HTTP response */ init: function(url, data, sendResponseCallback) { + var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget(); + if (!library.editable) { + Zotero.logError("Can't add item to read-only library " + library.name); + return sendResponseCallback(500, "application/json", JSON.stringify({ libraryEditable: false })); + } + this.sendResponse = sendResponseCallback; Zotero.Server.Connector.Detect.prototype.init.apply(this, [url, data, sendResponseCallback]) }, @@ -533,11 +539,7 @@ Zotero.Server.Connector.SavePage.prototype = { var jsonItems = []; translate.setHandler("select", function(obj, item, callback) { return me._selectItems(obj, item, callback) }); translate.setHandler("itemDone", function(obj, item, jsonItem) { - if(collection) { - collection.addItem(item.id); - } Zotero.Server.Connector.AttachmentProgressManager.add(jsonItem.attachments); - jsonItems.push(jsonItem); }); translate.setHandler("attachmentProgress", function(obj, attachment, progress, error) { @@ -557,7 +559,7 @@ Zotero.Server.Connector.SavePage.prototype = { } else { translate.setTranslator(translators[0]); } - translate.translate(libraryID); + translate.translate({libraryID, collections: collection ? [collection.id] : false}); } } diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js @@ -317,6 +317,18 @@ Zotero.Integration = new function() { oldWindow.close(); }); } + + if (Zotero.Integration.currentSession && Zotero.Integration.currentSession.progressBar) { + Zotero.Promise.delay(5).then(function() { + Zotero.Integration.currentSession.progressBar.hide(); + }); + } + // This technically shouldn't be necessary since we call document.activate(), + // but http integration plugins may not have OS level access to windows to be + // able to activate themselves. E.g. Google Docs on Safari. + if (Zotero.isMac && agent == 'http') { + Zotero.Utilities.Internal.sendToBack(); + } Zotero.Integration.currentDoc = Zotero.Integration.currentWindow = false; } @@ -330,7 +342,9 @@ Zotero.Integration = new function() { * @return {Promise} Promise resolved when the window is closed */ this.displayDialog = async function displayDialog(url, options, io) { + Zotero.debug(`Integration: Displaying dialog ${url}`); await Zotero.Integration.currentDoc.cleanup(); + Zotero.Integration.currentSession && await Zotero.Integration.currentSession.progressBar.hide(true); var allOptions = 'chrome,centerscreen'; // without this, Firefox gets raised with our windows under Compiz @@ -358,8 +372,14 @@ Zotero.Integration = new function() { deferred.resolve(); } window.addEventListener("unload", listener, false); - + await deferred.promise; + // We do not want to redisplay the progress bar if this window close + // was the final close of the integration command + await Zotero.Promise.delay(10); + if (Zotero.Integration.currentDoc) { + Zotero.Integration.currentSession.progressBar.show(); + } }; /** @@ -368,6 +388,9 @@ Zotero.Integration = new function() { * @return {Zotero.Integration.Session} Promise */ this.getSession = async function (app, doc, agent) { + var progressBar = new Zotero.Integration.Progress(4, Zotero.isMac && agent != 'http'); + progressBar.show(); + var dataString = await doc.getDocumentData(), data, session; @@ -439,7 +462,6 @@ Zotero.Integration = new function() { if (installed) { await session.setData(data, true); } - return session; } } await session.setDocPrefs(); @@ -449,6 +471,11 @@ Zotero.Integration = new function() { } session.agent = agent; session._doc = doc; + if (session.progressBar) { + progressBar.reset(); + progressBar.segments = session.progressBar.segments; + } + session.progressBar = progressBar; return session; }; @@ -807,14 +834,16 @@ Zotero.Integration.Fields.prototype.get = new function() { var promise = deferred.promise; // Otherwise, start getting fields - var getFieldsTime = (new Date()).getTime(); + var timer = new Zotero.Integration.Timer(); + timer.start(); + this._session.progressBar.start(); try { var fields = this._fields = Array.from(await this._doc.getFields(this._session.data.prefs['fieldType'])); - - var endTime = (new Date()).getTime(); - Zotero.debug("Integration: Retrieved "+fields.length+" fields in "+ - (endTime-getFieldsTime)/1000+"; "+ - 1000/((endTime-getFieldsTime)/fields.length)+" fields/second"); + + var retrieveTime = timer.stop(); + this._session.progressBar.finishSegment(); + Zotero.debug("Integration: Retrieved " + fields.length + " fields in " + + retrieveTime + "; " + fields.length/retrieveTime + " fields/second"); deferred.resolve(fields); } catch(e) { deferred.reject(e); @@ -829,7 +858,6 @@ Zotero.Integration.Fields.prototype.get = new function() { * Updates Zotero.Integration.Session attached to Zotero.Integration.Fields in line with document */ Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(function* (forceCitations) { - var collectFieldsTime; yield this.get(); this._session.resetRequest(this._doc); @@ -837,19 +865,19 @@ Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(fun this._deleteFields = {}; this._bibliographyFields = []; - collectFieldsTime = (new Date()).getTime(); + var timer = new Zotero.Integration.Timer(); + timer.start(); + this._session.progressBar.start(); if (forceCitations) { this._session.regenAll = true; } yield this._processFields(); this._session.regenAll = false; - - var endTime = (new Date()).getTime(); - if(Zotero.Debug.enabled) { - Zotero.debug("Integration: Updated session data for " + this._fields.length + " fields in " - + (endTime - collectFieldsTime) / 1000 + "; " - + 1000/ ((endTime - collectFieldsTime) / this._fields.length) + " fields/second"); - } + + var updateTime = timer.stop(); + this._session.progressBar.finishSegment(); + Zotero.debug("Integration: Updated session data for " + this._fields.length + " fields in " + + updateTime + "; " + this._fields.length/updateTime + " fields/second"); if (this._session.reload) { this._session.restoreProcessorState(); @@ -905,8 +933,12 @@ Zotero.Integration.Fields.prototype.updateDocument = Zotero.Promise.coroutine(fu this._session.timer = new Zotero.Integration.Timer(); this._session.timer.start(); + this._session.progressBar.start(); yield this._session._updateCitations() + this._session.progressBar.finishSegment(); + this._session.progressBar.start(); yield this._updateDocument(forceCitations, forceBibliography, ignoreCitationChanges) + this._session.progressBar.finishSegment(); var diff = this._session.timer.stop(); this._session.timer = null; @@ -1172,7 +1204,6 @@ Zotero.Integration.Fields.prototype.addEditCitation = async function (field) { fieldIndexPromise, citationsByItemIDPromise, previewFn ); - Zotero.debug('Integration: Displaying citation dialogue'); if (Zotero.Prefs.get("integration.useClassicAddCitationDialog")) { Zotero.Integration.displayDialog('chrome://zotero/content/integration/addCitationDialog.xul', 'alwaysRaised,resizable', io); @@ -2687,18 +2718,98 @@ Zotero.Integration.Timer = class { stop() { this.resume(); - return ((new Date()).getTime() - this.startTime)/1000; + this.finalTime = ((new Date()).getTime() - this.startTime) + return this.finalTime/1000; } pause() { this.pauseTime = (new Date()).getTime(); } + getSplit() { + var pauseTime = 0; + if (this.pauseTime) { + var pauseTime = (new Date()).getTime() - this.pauseTime; + } + return ((new Date()).getTime() - this.startTime - pauseTime); + } + resume() { if (this.pauseTime) { this.startTime += ((new Date()).getTime() - this.pauseTime); - this.pauseTime = null; + this.pauseTime; + } + } +} + +Zotero.Integration.Progress = class { + /** + * @param {Number} segmentCount + * @param {Boolean} dontDisplay + * On macOS closing an application window switches focus to the topmost window of the same application + * instead of the previous window of any application. Since the progress window is opened and closed + * between showing other integration windows, macOS will switch focus to the main Zotero window (and + * move the word processor window to the background). Thus we avoid showing the progress window on macOS + * except for http agents (i.e. google docs), where even opening the citation dialog may potentially take + * a long time and having no indication of progress is worse than bringing the Zotero window to the front + */ + constructor(segmentCount=4, dontDisplay=false) { + this.segments = Array.from({length: segmentCount}, () => undefined); + this.timer = new Zotero.Integration.Timer(); + this.segmentIdx = 0; + this.dontDisplay = dontDisplay; + } + + update() { + if (!this.onProgress) return; + var currentSegment = this.segments[this.segmentIdx]; + if (!currentSegment) return; + var total = this.segments.reduce((acc, val) => acc+val, 0); + var startProgress = 100*this.segments.slice(0, this.segmentIdx).reduce((acc, val) => acc+val, 0)/total; + var maxProgress = 100.0*currentSegment/total; + var split = this.timer.getSplit(); + var curProgress = startProgress + maxProgress*Math.min(1, split/currentSegment); + this.onProgress(curProgress); + setTimeout(this.update.bind(this), 100); + } + + start() { + this.timer.start(); + } + pause() {this.timer.pause();} + resume() {this.timer.resume();} + finishSegment() { + this.timer.stop(); + this.segments[this.segmentIdx++] = this.timer.finalTime; + } + reset() { + this.segmentIdx = 0; + } + show() { + if (this.dontDisplay) return; + var options = 'chrome,centerscreen'; + // without this, Firefox gets raised with our windows under Compiz + if (Zotero.isLinux) options += ',dialog=no'; + if (Zotero.isMac) options += ',resizable=false'; + + var io = {onLoad: function(onProgress) { + this.onProgress = onProgress; + this.update(); + }.bind(this)}; + io.wrappedJSObject = io; + this.window = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Components.interfaces.nsIWindowWatcher) + .openWindow(null, 'chrome://zotero/content/integration/progressBar.xul', '', options, io); + Zotero.Utilities.Internal.activate(this.window); + } + async hide(fast=false) { + if (this.dontDisplay || !this.window) return; + if (!fast) { + this.onProgress && this.onProgress(100); + this.onProgress = null; + await Zotero.Promise.delay(300); } + this.window.close(); } } diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js @@ -1660,6 +1660,18 @@ Zotero.Utilities.Internal.activate = new function() { } }; +Zotero.Utilities.Internal.sendToBack = function() { + if (Zotero.isMac) { + Zotero.Utilities.Internal.executeAppleScript(` + tell application "System Events" + if frontmost of application id "org.zotero.zotero" then + set visible of process "Zotero" to false + end if + end tell + `); + } +} + /** * Base64 encode / decode * From http://www.webtoolkit.info/ diff --git a/test/tests/integrationTest.js b/test/tests/integrationTest.js @@ -305,9 +305,12 @@ describe("Zotero.Integration", function () { }); addEditCitationSpy = sinon.spy(Zotero.Integration.Interface.prototype, 'addEditCitation'); + + sinon.stub(Zotero.Integration.Progress.prototype, 'show'); }); after(function() { + Zotero.Integration.Progress.prototype.show.restore(); Zotero.Integration.getApplication.restore(); displayDialogStub.restore(); addEditCitationSpy.restore();