diff --git a/chrome/content/zotero-platform/mac/integration.css b/chrome/content/zotero-platform/mac/integration.css
index f33b75ad3a..d3f6ee46fb 100644
--- 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
new file mode 100644
index 0000000000..d669cea467
--- /dev/null
+++ 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 .
+
+ ***** 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
new file mode 100644
index 0000000000..abbc0ab644
--- /dev/null
+++ b/chrome/content/zotero/integration/progressBar.xul
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js b/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js
index 2d0981f708..1e200e1d14 100644
--- 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
index 79542465d3..70da872381 100644
--- 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
index 26d9192e0a..909c0b9207 100644
--- 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,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};
diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js
index 305344870e..bd86815592 100644
--- 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
index c1a0ff88fb..681f37f629 100644
--- 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();