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
This commit is contained in:
Adomas Venčkauskas 2018-03-30 15:31:53 +03:00 committed by Dan Stillman
parent 5e5b567782
commit 593153eebe
8 changed files with 339 additions and 29 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,21 +2718,101 @@ 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();
}
}
Zotero.Integration.LegacyPluginWrapper = function(application) {
function wrapField(field) {
var wrapped = {rawField: field};

View file

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

View file

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