www

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

commit d550ac92b4362b91d317beb0b3d6e88a481cec9f
parent 755db116cc1a5fda27cb3233dde9fdc49457934f
Author: Simon Kornblith <simon@simonster.com>
Date:   Mon, 16 Jul 2012 21:50:14 -0400

Q-ize integration.js

Adds a new function, Zotero.promiseGenerator, that returns a promise that is fulfilled by the last thing yielded by a generator, or rejected with an error.

Diffstat:
Mchrome/content/zotero/integration/addCitationDialog.js | 8+++-----
Mchrome/content/zotero/integration/quickFormat.js | 4++--
Mchrome/content/zotero/xpcom/integration.js | 1885+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mchrome/content/zotero/xpcom/zotero.js | 27+++++++++++++++++++++++----
4 files changed, 955 insertions(+), 969 deletions(-)

diff --git a/chrome/content/zotero/integration/addCitationDialog.js b/chrome/content/zotero/integration/addCitationDialog.js @@ -565,10 +565,10 @@ var Zotero_Citation_Dialog = new function () { if(_previewShown) { document.documentElement.getButton("extra2").label = Zotero.getString("citation.hideEditor"); if(text) { - io.preview(function(preview) { + io.preview().then(function(preview) { _originalHTML = preview; editor.value = text; - }); + }).end(); } else { _updatePreview(); } @@ -581,9 +581,7 @@ var Zotero_Citation_Dialog = new function () { * called when accept button is clicked */ function accept() { - Zotero.debug("Trying to accept"); _getCitation(); - Zotero.debug("got citation"); var isCustom = _previewShown && io.citation.citationItems.length // if a citation is selected && _originalHTML && document.getElementById('editor').value != _originalHTML // and citation has been edited @@ -623,7 +621,7 @@ var Zotero_Citation_Dialog = new function () { editor.readonly = !io.citation.citationItems.length; if(io.citation.citationItems.length) { - io.preview(function(preview) { + io.preview().then(function(preview) { editor.value = preview; if(editor.initialized) { diff --git a/chrome/content/zotero/integration/quickFormat.js b/chrome/content/zotero/integration/quickFormat.js @@ -278,7 +278,7 @@ var Zotero_QuickFormat = new function () { // Save current search so that when we get items, we know whether it's too late to // process them or not var lastSearchTime = currentSearchTime = Date.now(); - io.getItems(function(citedItems) { + io.getItems().then(function(citedItems) { // Don't do anything if panel is already closed if(isAsync && ((referencePanel.state !== "open" && referencePanel.state !== "showing") @@ -314,7 +314,7 @@ var Zotero_QuickFormat = new function () { } _updateItemList(citedItems, citedItemsMatchingSearch, searchResultIDs, isAsync); - }); + }).end(); if(!completed) { // We are going to have to wait until items have been retrieved from the document. diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js @@ -24,8 +24,6 @@ ***** END LICENSE BLOCK ***** */ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - const RESELECT_KEY_URI = 1; const RESELECT_KEY_ITEM_KEY = 2; const RESELECT_KEY_ITEM_ID = 3; @@ -44,11 +42,13 @@ const INTEGRATION_PLUGINS = ["zoteroMacWordIntegration@zotero.org", "zoteroOpenOfficeIntegration@zotero.org", "zoteroWinWordIntegration@zotero.org"]; Zotero.Integration = new function() { + Components.utils.import("resource://gre/modules/Services.jsm"); + Components.utils.import("resource://gre/modules/AddonManager.jsm"); + const INTEGRATION_MIN_VERSIONS = ["3.1.7.SOURCE", "3.5b2.SOURCE", "3.1.3.SOURCE"]; var _tmpFile = null; var _osascriptFile; - var _integrationVersionsOK = null; // these need to be global because of GC var _updateTimer; @@ -59,7 +59,6 @@ Zotero.Integration = new function() { XOpenDisplay, XCloseDisplay, XFlush, XDefaultRootWindow, XInternAtom, XSendEvent, XMapRaised, XGetWindowProperty, X11Atom, X11Bool, X11Display, X11Window, X11Status; - var _inProgress = false; this.currentWindow = false; this.sessions = {}; @@ -110,15 +109,25 @@ Zotero.Integration = new function() { // try to initialize pipe try { - Zotero.IPC.Pipe.initPipeListener(pipe, _parseIntegrationPipeCommand); + Zotero.IPC.Pipe.initPipeListener(pipe, function(string) { + if(string != "") { + // exec command if possible + var parts = string.match(/^([^ \n]*) ([^ \n]*)(?: ([^\n]*))?\n?$/); + if(parts) { + var agent = parts[1].toString(); + var cmd = parts[2].toString(); + var document = parts[3] ? parts[3].toString() : null; + Zotero.Integration.execCommand(agent, cmd, document); + } else { + Components.utils.reportError("Zotero: Invalid integration input received: "+string); + } + } + }); } catch(e) { Zotero.logError(e); } - _updateTimer = Components.classes["@mozilla.org/timer;1"]. - createInstance(Components.interfaces.nsITimer); - _updateTimer.initWithCallback({"notify":function() { _checkPluginVersions() }}, 1000, - Components.interfaces.nsITimer.TYPE_ONE_SHOT); + Q.delay(1000).then(_checkPluginVersions); } /** @@ -157,121 +166,144 @@ Zotero.Integration = new function() { } } - function _checkPluginVersions(callback) { - if(_updateTimer) _updateTimer = undefined; - - if(_integrationVersionsOK !== null) { - if(callback) callback(_integrationVersionsOK); - return; - } + /** + * Checks to see that plugin versions are up to date. + * @return {Promise} Promise that is resolved with true if versions are up to date + * or with false if they are not. + */ + var _checkPluginVersions = new function () { + var integrationVersionsOK; - var verComp = Components.classes["@mozilla.org/xpcom/version-comparator;1"] - .getService(Components.interfaces.nsIVersionComparator); - var addonsChecked = false; - var success = true; - function _checkAddons(addons) { - addonsChecked = true; - for(var i in addons) { - var addon = addons[i]; - if(!addon) continue; - if(addon.userDisabled) continue; - - if(verComp.compare(INTEGRATION_MIN_VERSIONS[i], addon.version) > 0) { - _integrationVersionsOK = false; - Zotero.Integration.activate(); - var msg = Zotero.getString( - "integration.error.incompatibleVersion2", - [Zotero.version, addon.name, INTEGRATION_MIN_VERSIONS[i]] - ); - Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService) - .alert(null, Zotero.getString("integration.error.title"), msg); - throw msg; + return function _checkPluginVersions() { + if(integrationVersionsOK) { + if(integrationVersionsOK === true) { + return Q.resolve(integrationVersionsOK); + } else { + return Q.reject(integrationVersionsOK); } } - _integrationVersionsOK = true; - if(callback) callback(_integrationVersionsOK); - } - - Components.utils.import("resource://gre/modules/AddonManager.jsm"); - AddonManager.getAddonsByIDs(INTEGRATION_PLUGINS, _checkAddons); + var deferred = Q.defer(); + AddonManager.getAddonsByIDs(INTEGRATION_PLUGINS, function(addons) { + for(var i in addons) { + var addon = addons[i]; + if(!addon || addon.userDisabled) continue; + + if(Services.vc.compare(INTEGRATION_MIN_VERSIONS[i], addon.version) > 0) { + deferred.reject(integrationVersionsOK = new Zotero.Exception.Alert( + "integration.error.incompatibleVersion2", + [Zotero.version, addon.name, INTEGRATION_MIN_VERSIONS[i]], + "integration.error.title")); + } + } + deferred.resolve(integrationVersionsOK = true); + }); + return deferred.promise; + }; } /** * Executes an integration command, first checking to make sure that versions are compatible */ - this.execCommand = function execCommand(agent, command, docId) { - if(_inProgress) { - Zotero.Integration.activate(); - if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { - Zotero.Integration.currentWindow.focus(); - } - Zotero.debug("Integration: Request already in progress; not executing "+agent+" "+command); - return; - } - _inProgress = true; + this.execCommand = new function() { + var inProgress; - // Check integration component versions - _checkPluginVersions(function(success) { - if(success) { - _callIntegration(agent, command, docId); - } else { - _inProgress = false; - } - }); - } - - /** - * Parses a command received from the integration pipe - */ - function _parseIntegrationPipeCommand(string) { - if(string != "") { - // exec command if possible - var parts = string.match(/^([^ \n]*) ([^ \n]*)(?: ([^\n]*))?\n?$/); - if(parts) { - var agent = parts[1].toString(); - var cmd = parts[2].toString(); - var document = parts[3] ? parts[3].toString() : null; - Zotero.Integration.execCommand(agent, cmd, document); - } else { - Components.utils.reportError("Zotero: Invalid integration input received: "+string); + return function execCommand(agent, command, docId) { + var document; + + if(inProgress) { + Zotero.Integration.activate(); + if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { + Zotero.Integration.currentWindow.focus(); + } + Zotero.debug("Integration: Request already in progress; not executing "+agent+" "+command); + return; } - } - } - - /** - * Calls the Integration applicatoon - */ - function _callIntegration(agent, command, docId) { - // Try to load the appropriate Zotero component; otherwise display an error using the alert - // service - try { - var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1"; - Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : "")); - var application = Components.classes[componentClass] - .getService(Components.interfaces.zoteroIntegrationApplication); - } catch(e) { - _inProgress = false; - Zotero.Integration.activate(); - Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService) - .alert(null, Zotero.getString("integration.error.title"), - Zotero.getString("integration.error.notInstalled")); - throw e; - } - - // Try to execute the command; otherwise display an error in alert service or word processor - // (depending on what is possible) - var integration, document; - try { - document = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument()); - integration = new Zotero.Integration.Document(application, document); - integration[command](); - } catch(e) { - Zotero.Integration.handleError(e, document); - } - } + inProgress = true; + + // Check integration component versions + _checkPluginVersions().then(function() { + // Try to load the appropriate Zotero component; otherwise display an error + try { + var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1"; + Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : "")); + var application = Components.classes[componentClass] + .getService(Components.interfaces.zoteroIntegrationApplication); + } catch(e) { + throw new Zotero.Exception.Alert("integration.error.notInstalled", + [], "integration.error.title"); + } + + // Try to execute the command; otherwise display an error in alert service or word processor + // (depending on what is possible) + document = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument()); + return Q.resolve((new Zotero.Integration.Document(application, document))[command]()); + }).fail(function(e) { + if(!(e instanceof Zotero.Exception.UserCancelled)) { + try { + var displayError = null; + if(e instanceof Zotero.Exception.Alert) { + displayError = e.message; + } else { + if(e.toString().indexOf("ExceptionAlreadyDisplayed") === -1) { + displayError = Zotero.getString("integration.error.generic")+"\n\n"+(e.message || e.toString()); + } + if(e.stack) { + Zotero.debug(e.stack); + } + } + + if(displayError) { + var showErrorInFirefox = !document; + + if(document) { + try { + document.activate(); + document.displayAlert(displayError, + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); + } catch(e) { + showErrorInFirefox = true; + } + } + + if(showErrorInFirefox) { + Zotero.Integration.activate(); + Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService) + .alert(null, Zotero.getString("integration.error.title"), displayError); + } + } + } finally { + Zotero.logError(e); + } + } + }).fin(function() { + if(document) { + try { + document.cleanup(); + document.activate(); + + // Call complete function if one exists + if(document.wrappedJSObject && document.wrappedJSObject.complete) { + document.wrappedJSObject.complete(); + } + } catch(e) { + Zotero.logError(e); + } + } + + if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { + var oldWindow = Zotero.Integration.currentWindow; + Q.delay(100).then(function() { + oldWindow.close(); + }); + } + + inProgress = Zotero.Integration.currentWindow = false; + }).end(); + }; + }; /** * Activates Firefox @@ -583,7 +615,7 @@ Zotero.Integration = new function() { if(XSendEvent(_x11Display, _x11RootWindow, 0, mask, event.address())) { XMapRaised(_x11Display, x11Window); XFlush(_x11Display); - Zotero.debug("Activated successfully"); + Zotero.debug("Integration: Activated successfully"); } else { Zotero.debug("Integration: An error occurred activating the window"); } @@ -619,103 +651,6 @@ Zotero.Integration = new function() { } /** - * Show appropriate dialogs for an integration error - */ - this.handleError = function(e, document) { - if(!(e instanceof Zotero.Exception.UserCancelled)) { - try { - var displayError = null; - if(e instanceof Zotero.Integration.DisplayException) { - displayError = e.toString(); - } else { - // check to see whether there's a pyxpcom error in the console, since it doesn't - // get thrown directly - var message = ""; - - var consoleService = Components.classes["@mozilla.org/consoleservice;1"] - .getService(Components.interfaces.nsIConsoleService); - - var messages = {}; - consoleService.getMessageArray(messages, {}); - messages = messages.value; - if(messages && messages.length) { - var lastMessage = messages[messages.length-1]; - try { - var error = lastMessage.QueryInterface(Components.interfaces.nsIScriptError); - } catch(e2) { - if(lastMessage.message && lastMessage.message.substr(0, 12) == "ERROR:xpcom:") { - // print just the last line of the message, but re-throw the rest - message = lastMessage.message.substr(0, lastMessage.message.length-1); - message = "\n"+message.substr(message.lastIndexOf("\n")) - } - } - } - - if(!message && typeof(e) == "object") message = "\n\n"+e.toString(); - - if(message.indexOf("ExceptionAlreadyDisplayed") === -1) { - displayError = Zotero.getString("integration.error.generic")+message; - } - Zotero.debug(e); - } - - if(displayError) { - var showErrorInFirefox = !document; - - if(document) { - try { - document.activate(); - document.displayAlert(displayError, - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); - } catch(e) { - showErrorInFirefox = true; - } - } - - if(showErrorInFirefox) { - Zotero.Integration.activate(); - Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService) - .alert(null, Zotero.getString("integration.error.title"), displayError); - } - } - } finally { - Zotero.logError(e); - } - } - - this.complete(document); - } - - /** - * Called when integration is complete - */ - this.complete = function(doc) { - if(doc) { - try { - doc.cleanup(); - doc.activate(); - - // Call complete function if one exists - if(doc.wrappedJSObject && doc.wrappedJSObject.complete) { - doc.wrappedJSObject.complete(); - } - } catch(e) { - Zotero.logError(e); - } - } - - if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { - var oldWindow = Zotero.Integration.currentWindow; - Zotero.setTimeout(function() { - oldWindow.close(); - }, 100, true); - } - _inProgress = Zotero.Integration.currentWindow = false; - } - - /** * Runs an AppleScript on OS X * * @param script {String} @@ -743,18 +678,15 @@ Zotero.Integration = new function() { * @param {String} url The chrome:// URI of the window * @param {String} [options] Options to pass to the window * @param {String} [io] Data to pass to the window - * @param {Function|Boolean} [async] Function to call when window is closed. If not specified, - * function waits to return until the window has been closed. If "true", the function returns - * immediately. + * @return {Promise} Promise resolved when the window is closed */ - this.displayDialog = function(doc, url, options, io, async) { + this.displayDialog = function displayDialog(doc, url, options, io) { doc.cleanup(); var allOptions = 'chrome,centerscreen'; // without this, Firefox gets raised with our windows under Compiz if(Zotero.isLinux) allOptions += ',dialog=no'; if(options) allOptions += ','+options; - if(!async) allOptions += ',modal=yes'; var window = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] .getService(Components.interfaces.nsIWindowWatcher) @@ -762,6 +694,7 @@ Zotero.Integration = new function() { Zotero.Integration.currentWindow = window; Zotero.Integration.activate(window); + var deferred = Q.defer(); var listener = function() { if(window.location.toString() === "about:blank") return; @@ -773,15 +706,22 @@ Zotero.Integration = new function() { } Zotero.Integration.currentWindow = false; - if(async instanceof Function) { - try { - async(); - } catch(e) { - Zotero.Integration.handleError(e, doc); - } - } + deferred.resolve(); } window.addEventListener("unload", listener, false); + + return deferred.promise; + }; + + /** + * Default callback for field-related errors. All functions that do not define their + * own handlers for field-related errors should use this one. + */ + this.onFieldError = function onFieldError(err) { + if(err.attemptToResolve) { + return err.attemptToResolve(); + } + throw err; } } @@ -799,22 +739,159 @@ Zotero.Integration.MissingItemException = function(reselectKeys, reselectKeyType this.citationIndex = citationIndex; this.citationLength = citationLength; } -Zotero.Integration.MissingItemException.prototype.name = "MissingItemException"; -Zotero.Integration.MissingItemException.prototype.message = "An item in this document is missing from your Zotero library."; -Zotero.Integration.MissingItemException.prototype.toString = function() { return this.message; }; +Zotero.Integration.MissingItemException.prototype = { + "name":"MissingItemException", + "message":"An item in this document is missing from your Zotero library.", + "toString":function() { return this.message }, + "setContext":function(fieldGetter, fieldIndex) { + this.fieldGetter = fieldGetter; + this.fieldIndex = fieldIndex; + }, + + "attemptToResolve":function() { + Zotero.logError(this); + if(!this.fieldGetter) { + throw new Error("Could not resolve "+this.name+": setContext not called"); + } + + // Ask user what to do with this item + if(this.citationLength == 1) { + var msg = Zotero.getString("integration.missingItem.single"); + } else { + var msg = Zotero.getString("integration.missingItem.multiple", (this.citationIndex+1).toString()); + } + msg += '\n\n'+Zotero.getString('integration.missingItem.description'); + this.fieldGetter._fields[this.fieldIndex].select(); + this.fieldGetter._doc.activate(); + var result = this.fieldGetter._doc.displayAlert(msg, 1, 3); + if(result == 0) { // Cancel + return Q.reject(new Zotero.Exception.UserCancelled("document update")); + } else if(result == 1) { // No + for each(var reselectKey in this.reselectKeys) { + this.fieldGetter._removeCodeKeys[reselectKey] = true; + } + this.fieldGetter._removeCodeFields[this.fieldIndex] = true; + return this.fieldGetter._processFields(this.fieldIndex+1); + } else { // Yes + // Display reselect item dialog + var fieldGetter = this.fieldGetter, + fieldIndex = this.fieldIndex, + oldCurrentWindow = Zotero.Integration.currentWindow; + return fieldGetter._session.reselectItem(fieldGetter._doc, this) + .then(function() { + // Now try again + Zotero.Integration.currentWindow = oldCurrentWindow; + fieldGetter._doc.activate(); + try { + fieldGetter._processFields(fieldIndex); + } catch(e) { + return Zotero.Integration.onFieldError(e); + } + }); + return false; + } + } +} -Zotero.Integration.DisplayException = function(name, params) { - this.name = name; - this.params = params ? params : []; +Zotero.Integration.CorruptFieldException = function(code, cause) { + this.code = code; + this.cause = cause; +}; +Zotero.Integration.CorruptFieldException.prototype = { + "name":"CorruptFieldException", + "message":"A field code in this document is corrupted.", + "toString":function() { return this.cause.toString()+"\n\n"+this.code.toSource(); }, + "setContext":function(fieldGetter, fieldIndex, field) { + this.fieldGetter = fieldGetter; + this.fieldIndex = fieldIndex; + }, + + /** + * Tries to resolve the CorruptFieldException + * @return {Promise} A promise that is either resolved with true or rejected with + * Zotero.Exception.UserCancelled + */ + "attemptToResolve":function() { + Zotero.logError(this.cause); + if(!this.fieldGetter) { + throw new Error("Could not resolve "+this.name+": setContext not called"); + } + + var msg = Zotero.getString("integration.corruptField")+'\n\n'+ + Zotero.getString('integration.corruptField.description'), + field = this.fieldGetter._fields[this.fieldIndex]; + field.select(); + this.fieldGetter._doc.activate(); + var result = this.fieldGetter._doc.displayAlert(msg, + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO_CANCEL); + + if(result == 0) { + return Q.reject(new Zotero.Exception.UserCancelled("document update")); + } else if(result == 1) { // No + this.fieldGetter._removeCodeFields[this.fieldIndex] = true; + return this.fieldGetter._processFields(this.fieldIndex+1); + } else { + // Display reselect edit citation dialog + var fieldGetter = this.fieldGetter, + oldWindow = Zotero.Integration.currentWindow, + oldProgressCallback = this.progressCallback; + return fieldGetter.addEditCitation(field).then(function() { + if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { + Zotero.Integration.currentWindow.close(); + } + Zotero.Integration.currentWindow = oldWindow; + fieldGetter.progressCallback = oldProgressCallback; + return fieldGetter.updateSession().fail(Zotero.Integration.onFieldError); + }); + } + } }; -Zotero.Integration.DisplayException.prototype.toString = function() { return Zotero.getString("integration.error."+this.name, this.params); }; -Zotero.Integration.CorruptFieldException = function(corruptFieldString) { - this.corruptFieldString = corruptFieldString; +/** + * An exception to encapsulate the case where bibliography data is invalid. + * @class + */ +Zotero.Integration.CorruptBibliographyException = function(code, cause) { + this.code = code; + this.cause = cause; } -Zotero.Integration.CorruptFieldException.prototype.name = "CorruptFieldException"; -Zotero.Integration.CorruptFieldException.prototype.message = "A field code in this document is corrupted."; -Zotero.Integration.CorruptFieldException.prototype.toString = function() { return this.message+" "+this.corruptFieldString.toSource(); } +Zotero.Integration.CorruptBibliographyException.prototype = { + "name":"CorruptBibliographyException", + "message":"A bibliography in this document is corrupted.", + "toString":function() { return this.cause.toString()+"\n\n"+this.code }, + + "setContext":function(fieldGetter) { + this.fieldGetter = fieldGetter; + }, + + /** + * Tries to resolve the CorruptBibliographyException + * @return {Promise} A promise that is either resolved with true or rejected with + * Zotero.Exception.UserCancelled + */ + "attemptToResolve":function() { + Zotero.debug("Attempting to resolve") + Zotero.logError(this.cause); + if(!this.fieldGetter) { + throw new Error("Could not resolve "+this.name+": setContext not called"); + } + + var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+ + Zotero.getString('integration.corruptBibliography.description'); + var result = this.fieldGetter._doc.displayAlert(msg, + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); + if(result == 0) { + return Q.reject(new Zotero.Exception.UserCancelled("clearing corrupted bibliography")); + } else { + this.fieldGetter._bibliographyData = ""; + this.fieldGetter._session.bibliographyHasChanged = true; + this.fieldGetter._session.bibliographyDataHasChanged = true; + return Q.resolve(true); + } + } +}; const INTEGRATION_TYPE_ITEM = 1; const INTEGRATION_TYPE_BIBLIOGRAPHY = 2; @@ -831,25 +908,28 @@ Zotero.Integration.Document = function(app, doc) { this._app = app; this._doc = doc; } - /** * Creates a new session * @param data {Zotero.Integration.DocumentData} Document data for new session + * @return {Zotero.Integration.Session} */ -Zotero.Integration.Document.prototype._createNewSession = function(data) { +Zotero.Integration.Document.prototype._createNewSession = function _createNewSession(data) { data.sessionID = Zotero.randomString(); var session = Zotero.Integration.sessions[data.sessionID] = new Zotero.Integration.Session(this._doc); return session; -} +}; /** * Gets preferences for a document - * @param require {Boolean} Whether an error should be thrown if no preferences or fields exist - * (otherwise, the set doc prefs dialog is shown) - * @param dontRunSetDocPrefs {Boolean} Whether to show the Set Document Preferences window if no - * preferences exist - */ -Zotero.Integration.Document.prototype._getSession = function(require, dontRunSetDocPrefs, callback) { + * @param require {Boolean} Whether an error should be thrown if no preferences or fields + * exist (otherwise, the set doc prefs dialog is shown) + * @param dontRunSetDocPrefs {Boolean} Whether to show the Set Document Preferences + * window if no preferences exist + * @return {Promise} Promise resolved with true if a session was found or false if + * dontRunSetDocPrefs is true and no session was found, or rejected with + * Zotero.Exception.UserCancelled if the document preferences window was cancelled. + */ +Zotero.Integration.Document.prototype._getSession = function _getSession(require, dontRunSetDocPrefs) { var dataString = this._doc.getDocumentData(), data, me = this; @@ -877,7 +957,9 @@ Zotero.Integration.Document.prototype._getSession = function(require, dontRunSet // if no fields, throw an error if(!haveFields) { - throw new Zotero.Integration.DisplayException("mustInsertCitation"); + return Q.reject(new Zotero.Exception.Alert( + "integration.error.mustInsertCitation", + [], "integration.error.title")); } else { Zotero.debug("Integration: No document preferences found, but found "+data.prefs.fieldType+" fields"); } @@ -886,23 +968,18 @@ Zotero.Integration.Document.prototype._getSession = function(require, dontRunSet // Set doc prefs if no data string yet this._session = this._createNewSession(data); this._session.setData(data); - if(dontRunSetDocPrefs) { - callback(false); - return; - } + if(dontRunSetDocPrefs) return Q.resolve(false); - this._session.setDocPrefs(this._doc, this._app.primaryFieldType, this._app.secondaryFieldType, function(status) { - if(status === false) { - throw new Zotero.Exception.UserCancelled("document preferences update"); - } - + return this._session.setDocPrefs(this._doc, this._app.primaryFieldType, + this._app.secondaryFieldType).then(function(status) { // save doc prefs in doc me._doc.setDocumentData(me._session.data.serializeXML()); if(haveFields) { me._session.reload = true; } - callback(true); + + return me._session; }); } else { if(data.dataVersion < DATA_VERSION) { @@ -916,14 +993,18 @@ Zotero.Integration.Document.prototype._getSession = function(require, dontRunSet var warning = this._doc.displayAlert(Zotero.getString("integration.upgradeWarning"), Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); - if(!warning) throw new Zotero.Exception.UserCancelled("document upgrade"); + if(!warning) { + return Q.reject(new Zotero.Exception.UserCancelled("document upgrade")); + } } else if(data.dataVersion > DATA_VERSION) { - throw new Zotero.Integration.DisplayException("newerDocumentVersion", [data.zoteroVersion, Zotero.version]); + return Q.reject(new Zotero.Exception.Alert("integration.error.newerDocumentVersion", + [data.zoteroVersion, Zotero.version], "integration.error.title")); } if(data.prefs.fieldType !== this._app.primaryFieldType && data.prefs.fieldType !== this._app.secondaryFieldType) { - throw new Zotero.Integration.DisplayException("fieldTypeMismatch"); + return Q.reject(new Zotero.Exception.Alert("integration.error.fieldTypeMismatch", + [], "integration.error.title")); } if(Zotero.Integration.sessions[data.sessionID]) { @@ -935,230 +1016,227 @@ Zotero.Integration.Document.prototype._getSession = function(require, dontRunSet } catch(e) { // make sure style is defined if(e instanceof Zotero.Integration.DisplayException && e.name === "invalidStyle") { - this._session.setDocPrefs(this._doc, this._app.primaryFieldType, - this._app.secondaryFieldType, function(status) { - if(status === false) { - throw new Zotero.Exception.UserCancelled("document preferences update"); - } - + return this._session.setDocPrefs(this._doc, this._app.primaryFieldType, + this._app.secondaryFieldType).then(function(status) { me._doc.setDocumentData(me._session.data.serializeXML()); me._session.reload = true; - callback(true); + return me._session; }); - return; } else { - throw e; + return Q.reject(e); } } this._doc.setDocumentData(this._session.data.serializeXML()); this._session.reload = true; } - callback(true); + return Q.resolve(this._session); } -} +}; /** * Adds a citation to the current document. + * @return {Promise} */ Zotero.Integration.Document.prototype.addCitation = function() { var me = this; - this._getSession(false, false, function() { - var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); - fieldGetter.addEditCitation(null, function() { - Zotero.Integration.complete(me._doc); - }); + return this._getSession(false, false).then(function() { + return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(null); }); } /** * Edits the citation at the cursor position. + * @return {Promise} */ Zotero.Integration.Document.prototype.editCitation = function() { var me = this; - this._getSession(true, false, function() { + return this._getSession(true, false).then(function() { var field = me._doc.cursorInField(me._session.data.prefs['fieldType']) if(!field) { - throw new Zotero.Integration.DisplayException("notInCitation"); + throw new Zotero.Exception.Alert("integration.error.notInCitation", [], + "integration.error.title"); } - var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); - fieldGetter.addEditCitation(field, function() { - Zotero.Integration.complete(me._doc); - }); + return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(field); }); } /** * Adds a bibliography to the current document. + * @return {Promise} */ Zotero.Integration.Document.prototype.addBibliography = function() { var me = this; - this._getSession(true, false, function() { + return this._getSession(true, false).then(function() { // Make sure we can have a bibliography if(!me._session.data.style.hasBibliography) { - throw new Zotero.Integration.DisplayException("noBibliography"); + throw new Zotero.Exception.Alert("integration.error.noBibliography", [], + "integration.error.title"); } var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc), field = fieldGetter.addField(); field.setCode("BIBL"); - fieldGetter.updateSession(function() { - fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false, function() { - Zotero.Integration.complete(me._doc); - }); - }); + return fieldGetter.updateSession().fail(Zotero.Integration.onFieldError) + .then(function() { + return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); + }) }); } /** * Edits bibliography metadata. + * @return {Promise} */ -Zotero.Integration.Document.prototype.editBibliography = function(callback) { +Zotero.Integration.Document.prototype.editBibliography = function() { // Make sure we have a bibliography - var me = this; - this._getSession(true, false, function() { - var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); - fieldGetter.get(function(fields) { - var haveBibliography = false; - for(var i=fields.length-1; i>=0; i--) { - var code = fields[i].getCode(); - var [type, content] = fieldGetter.getCodeTypeAndContent(code); - if(type == INTEGRATION_TYPE_BIBLIOGRAPHY) { - haveBibliography = true; - break; - } - } - - if(!haveBibliography) { - throw new Zotero.Integration.DisplayException("mustInsertBibliography"); + var me = this, fieldGetter; + return this._getSession(true, false).then(function() { + fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); + return fieldGetter.get(); + }).then(function(fields) { + var haveBibliography = false; + for(var i=fields.length-1; i>=0; i--) { + var code = fields[i].getCode(); + var [type, content] = fieldGetter.getCodeTypeAndContent(code); + if(type == INTEGRATION_TYPE_BIBLIOGRAPHY) { + haveBibliography = true; + break; } - - fieldGetter.updateSession(function() { - me._session.editBibliography(me._doc, function() { - me._doc.activate(); - fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false, function() { - Zotero.Integration.complete(me._doc); - }); - }); - }); - }); + } + + if(!haveBibliography) { + throw new Zotero.Exception.Alert("integration.error.mustInsertBibliography", + [], "integration.error.title"); + } + + return fieldGetter.updateSession().fail(Zotero.Integration.onFieldError); + }).then(function() { + return me._session.editBibliography(me._doc); + }).then(function() { + return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); }); } /** * Updates the citation data for all citations and bibliography entries. + * @return {Promise} */ Zotero.Integration.Document.prototype.refresh = function() { var me = this; - this._getSession(true, false, function() { + return this._getSession(true, false).then(function() { // Send request, forcing update of citations and bibliography var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); - fieldGetter.updateSession(function() { - fieldGetter.updateDocument(FORCE_CITATIONS_REGENERATE, true, false, function() { - Zotero.Integration.complete(me._doc); - }); + return fieldGetter.updateSession().fail(Zotero.Integration.onFieldError) + .then(function() { + return fieldGetter.updateDocument(FORCE_CITATIONS_REGENERATE, true, false); }); }); } /** * Deletes field codes. + * @return {Promise} */ Zotero.Integration.Document.prototype.removeCodes = function() { var me = this; - this._getSession(true, false, function() { + return this._getSession(true, false).then(function() { var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); - fieldGetter.get(function(fields) { - var result = me._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); - if(result) { - for(var i=fields.length-1; i>=0; i--) { - fields[i].removeCode(); - } + return fieldGetter.get() + }).then(function(fields) { + var result = me._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); + if(result) { + for(var i=fields.length-1; i>=0; i--) { + fields[i].removeCode(); } - - Zotero.Integration.complete(me._doc); - }); + } }); } /** * Displays a dialog to set document preferences (style, footnotes/endnotes, etc.) + * @return {Promise} */ Zotero.Integration.Document.prototype.setDocPrefs = function() { - var me = this; - this._getSession(false, true, function(haveSession) { - var setDocPrefs = function() { - me._session.setDocPrefs(me._doc, me._app.primaryFieldType, me._app.secondaryFieldType, - function(oldData) { - if(oldData || oldData === null) { - me._doc.setDocumentData(me._session.data.serializeXML()); - if(oldData === null) return; - - fieldGetter.get(function(fields) { - if(fields && fields.length) { - // if there are fields, we will have to convert some things; get a list of what we need to deal with - var convertBibliographies = oldData === true || oldData.prefs.fieldType != me._session.data.prefs.fieldType; - var convertItems = convertBibliographies || oldData.prefs.noteType != me._session.data.prefs.noteType; - var fieldsToConvert = new Array(); - var fieldNoteTypes = new Array(); - for(var i=0, n=fields.length; i<n; i++) { - var field = fields[i], - fieldCode = field.getCode(), - [type, content] = fieldGetter.getCodeTypeAndContent(fieldCode); - - if(convertItems && type === INTEGRATION_TYPE_ITEM) { - var citation = me._session.unserializeCitation(fieldCode); - if(!citation.properties.dontUpdate) { - fieldsToConvert.push(field); - fieldNoteTypes.push(me._session.data.prefs.noteType); - } - } else if(convertBibliographies && type === INTEGRATION_TYPE_BIBLIOGRAPHY) { - fieldsToConvert.push(field); - fieldNoteTypes.push(0); - } - } - - if(fieldsToConvert.length) { - // pass to conversion function - me._doc.convert(new Zotero.Integration.Document.JSEnumerator(fieldsToConvert), - me._session.data.prefs.fieldType, fieldNoteTypes, fieldNoteTypes.length); - } - - // refresh contents - fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); - fieldGetter.updateSession(function() { - fieldGetter.updateDocument(FORCE_CITATIONS_RESET_TEXT, true, true, - function() { - Zotero.Integration.complete(me._doc); - }); - }); - } else { - Zotero.Integration.complete(me._doc); - } - }); - } else { - Zotero.Integration.complete(me._doc); - } - }); - }; - - var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); - + var me = this, + fieldGetter, + oldData; + return this._getSession(false, true).then(function(haveSession) { + fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); + var setDocPrefs = me._session.setDocPrefs.bind(me._session, me._doc, + me._app.primaryFieldType, me._app.secondaryFieldType); if(!haveSession) { // This is a brand new document; don't try to get fields - setDocPrefs(); + return setDocPrefs(); } else if(me._session.reload) { // Always reload before setDocPrefs so we can permit/deny unchecking storeReferences as // appropriate - fieldGetter.updateSession(setDocPrefs); + return fieldGetter.updateSession().fail(Zotero.Integration.onFieldError) + .then(setDocPrefs); } else { // Can get fields while dialog is open - fieldGetter.get(); - setDocPrefs(); + return Q.all([ + fieldGetter.get(), + setDocPrefs() + ]).spread(function (fields, setDocPrefs) { + // Only return value from setDocPrefs + return setDocPrefs; + }); + } + }).then(function(aOldData) { // After setDocPrefs call + oldData = aOldData; + + // Write document data to document + me._doc.setDocumentData(me._session.data.serializeXML()); + + // If oldData is null, then there was no document data, so we don't need to update + // fields + if(!oldData) return false; + return fieldGetter.get(); + }).then(function(fields) { + if(!fields || !fields.length) return; + + // If there are fields, we will have to convert some things; get a list of what + // we need to deal with + var convertBibliographies = oldData === true + || oldData.prefs.fieldType != me._session.data.prefs.fieldType; + var convertItems = convertBibliographies + || oldData.prefs.noteType != me._session.data.prefs.noteType; + var fieldsToConvert = new Array(); + var fieldNoteTypes = new Array(); + for(var i=0, n=fields.length; i<n; i++) { + var field = fields[i], + fieldCode = field.getCode(), + [type, content] = fieldGetter.getCodeTypeAndContent(fieldCode); + + if(convertItems && type === INTEGRATION_TYPE_ITEM) { + var citation = me._session.unserializeCitation(fieldCode); + if(!citation.properties.dontUpdate) { + fieldsToConvert.push(field); + fieldNoteTypes.push(me._session.data.prefs.noteType); + } + } else if(convertBibliographies + && type === INTEGRATION_TYPE_BIBLIOGRAPHY) { + fieldsToConvert.push(field); + fieldNoteTypes.push(0); + } } + + if(fieldsToConvert.length) { + // Pass to conversion function + me._doc.convert(new Zotero.Integration.Document.JSEnumerator(fieldsToConvert), + me._session.data.prefs.fieldType, fieldNoteTypes, + fieldNoteTypes.length); + } + + // Refresh contents + fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); + return fieldGetter.updateSession().fail(Zotero.Integration.onFieldError) + .then(fieldGetter.updateDocument.bind( + fieldGetter, FORCE_CITATIONS_RESET_TEXT, true, true)); }); } @@ -1182,7 +1260,6 @@ Zotero.Integration.Document.JSEnumerator.prototype.getNext = function() { Zotero.Integration.Fields = function(session, doc) { this._session = session; this._doc = doc; - this._callbacks = []; } /** @@ -1192,8 +1269,8 @@ Zotero.Integration.Fields = function(session, doc) { Zotero.Integration.Fields.prototype.addField = function(note) { // Get citation types if necessary if(!this._doc.canInsertField(this._session.data.prefs['fieldType'])) { - throw new Zotero.Integration.DisplayException("cannotInsertHere"); - return false; + return Q.reject(new Zotero.Exception.Alert("integration.error.cannotInsertHere", + [], "integration.error.title")); } var field = this._doc.cursorInField(this._session.data.prefs['fieldType']); @@ -1201,7 +1278,7 @@ Zotero.Integration.Fields.prototype.addField = function(note) { if(!this._doc.displayAlert(Zotero.getString("integration.replace"), Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL)) { - throw new Zotero.Exception.UserCancelled("inserting citation"); + return Q.reject(new Zotero.Exception.UserCancelled("inserting citation")); } } @@ -1236,262 +1313,155 @@ Zotero.Integration.Fields.prototype.getCodeTypeAndContent = function(rawCode) { /** * Gets all fields for a document + * @return {Promise} Promise resolved with field list. */ -Zotero.Integration.Fields.prototype.get = function(callback) { +Zotero.Integration.Fields.prototype.get = function get() { + // If we already have fields, just return them if(this._fields) { - try { - if(callback) { - callback(this._fields); - } - } catch(e) { - Zotero.Integration.handleError(e, this._doc); - } - return; + return Q.resolve(this._fields); } - if(callback) { - this._callbacks.push(callback); + // Create a new promise and add it to promise list + var deferred = Q.defer(); + + // If already getting fields, just return the promise + if(this._deferreds) { + this._deferreds.push(deferred); + return deferred; + } else { + this._deferreds = [deferred]; } - this._retrieveFields(); -} - -/** - * Actually do the work of retrieving fields - */ -Zotero.Integration.Fields.prototype._retrieveFields = function() { - if(this._retrievingFields) return; - this._retrievingFields = true; - var getFieldsTime = (new Date()).getTime(); - var me = this; - this._doc.getFieldsAsync(this._session.data.prefs['fieldType'], {"observe":function(subject, topic, data) { + // Otherwise, start getting fields + var getFieldsTime = (new Date()).getTime(), + me = this; + this._doc.getFieldsAsync(this._session.data.prefs['fieldType'], + {"observe":function(subject, topic, data) { if(topic === "fields-available") { - if(me.progressCallback) me.progressCallback(75); - - // Add fields to fields array - var fieldsEnumerator = subject.QueryInterface(Components.interfaces.nsISimpleEnumerator); - var fields = me._fields = []; - while(fieldsEnumerator.hasMoreElements()) { - fields.push(fieldsEnumerator.getNext().QueryInterface(Components.interfaces.zoteroIntegrationField)); - } - - if(Zotero.Debug.enabled) { - var endTime = (new Date()).getTime(); - Zotero.debug("Integration: Retrieved "+fields.length+" fields in "+ - (endTime-getFieldsTime)/1000+"; "+ - 1000/((endTime-getFieldsTime)/fields.length)+" fields/second"); + if(me.progressCallback) { + try { + me.progressCallback(75); + } catch(e) { + Zotero.logError(e); + }; } - // Run callbacks try { - for(var i=0, n=me._callbacks.length; i<n; i++) { - me._callbacks[i](fields); + // Add fields to fields array + var fieldsEnumerator = subject.QueryInterface(Components.interfaces.nsISimpleEnumerator); + var fields = me._fields = []; + while(fieldsEnumerator.hasMoreElements()) { + fields.push(fieldsEnumerator.getNext().QueryInterface(Components.interfaces.zoteroIntegrationField)); + } + + if(Zotero.Debug.enabled) { + var endTime = (new Date()).getTime(); + Zotero.debug("Integration: Retrieved "+fields.length+" fields in "+ + (endTime-getFieldsTime)/1000+"; "+ + 1000/((endTime-getFieldsTime)/fields.length)+" fields/second"); } } catch(e) { - Zotero.Integration.handleError(e, me._doc); + // Reject promises + for(var i=0, n=me._deferreds.length; i<n; i++) { + me._deferreds[i].reject(e); + } + me._deferreds = []; + return; + } + + // Resolve promises + for(var i=0, n=me._deferreds.length; i<n; i++) { + me._deferreds[i].resolve(fields); + } + me._deferreds = []; + } else if(topic === "fields-progress") { + if(me.progressCallback) { + try { + me.progressCallback((data ? parseInt(data, 10)*(3/4) : null)); + } catch(e) { + Zotero.logError(e); + }; } - } else if(topic === "fields-progress" && me.progressCallback) { - me.progressCallback((data ? parseInt(data, 10)*(3/4) : null)); } else if(topic === "fields-error") { - Zotero.Integration.handleError(data, me._doc); + for(var i=0, n=me._deferreds.length; i<n; i++) { + me._deferreds[i].reject(data); + } + me._deferreds = []; } }, QueryInterface:XPCOMUtils.generateQI([Components.interfaces.nsIObserver, Components.interfaces.nsISupports])}); -} - -/** - * Shows an error if a field code is corrupted - * @param {Exception} e The exception thrown - * @param {Field} field The Zotero field object - * @param {Function} callback The callback passed to updateSession - * @param {Function} errorCallback The error callback passed to updateSession - * @param {Integer} i The field index - * @return {Boolean} Whether to continue updating the session - */ -Zotero.Integration.Fields.prototype._showCorruptFieldError = function(e, field, callback, errorCallback, i) { - Zotero.logError(e); - - var msg = Zotero.getString("integration.corruptField")+'\n\n'+ - Zotero.getString('integration.corruptField.description'); - field.select(); - this._doc.activate(); - var result = this._doc.displayAlert(msg, - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO_CANCEL); - - if(result == 0) { - throw new Zotero.Exception.UserCancelled("document update"); - } else if(result == 1) { // No - this._removeCodeFields.push(i); - return true; - } else { - // Display reselect edit citation dialog - var me = this; - var oldWindow = Zotero.Integration.currentWindow; - var oldProgressCallback = me.progressCallback; - this.addEditCitation(field, function() { - if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { - Zotero.Integration.currentWindow.close(); - } - Zotero.Integration.currentWindow = oldWindow; - me.progressCallback = oldProgressCallback; - me.updateSession(callback, errorCallback); - }); - return false; - } -} - -/** - * Shows an error if a field code is missing - * @param {Exception} e The exception thrown - * @param {Exception} e The exception thrown - * @param {Field} field The Zotero field object - * @param {Function} callback The callback passed to updateSession - * @param {Function} errorCallback The error callback passed to updateSession - * @param {Integer} i The field index - * @return {Boolean} Whether to continue updating the session - */ -Zotero.Integration.Fields.prototype._showMissingItemError = function(e, field, callback, errorCallback, i) { - // First, check if we've already decided to remove field codes from these - var reselect = true; - for each(var reselectKey in e.reselectKeys) { - if(this._deleteKeys[reselectKey]) { - this._removeCodeFields.push(i); - return true; - } - } - - // Ask user what to do with this item - if(e.citationLength == 1) { - var msg = Zotero.getString("integration.missingItem.single"); - } else { - var msg = Zotero.getString("integration.missingItem.multiple", (e.citationIndex+1).toString()); - } - msg += '\n\n'+Zotero.getString('integration.missingItem.description'); - field.select(); - this._doc.activate(); - var result = this._doc.displayAlert(msg, 1, 3); - if(result == 0) { // Cancel - throw new Zotero.Exception.UserCancelled("document update"); - } else if(result == 1) { // No - for each(var reselectKey in e.reselectKeys) { - this._deleteKeys[reselectKey] = true; - } - this._removeCodeFields.push(i); - return true; - } else { // Yes - // Display reselect item dialog - var me = this; - var oldCurrentWindow = Zotero.Integration.currentWindow; - this._session.reselectItem(this._doc, e, function() { - // Now try again - Zotero.Integration.currentWindow = oldCurrentWindow; - me._doc.activate(); - me._processFields(me._fields, callback, errorCallback, i); - }); - return false; - } + return deferred.promise; } /** * Updates Zotero.Integration.Session attached to Zotero.Integration.Fields in line with document */ -Zotero.Integration.Fields.prototype.updateSession = function(callback, errorCallback) { - var me = this; - this.get(function(fields) { +Zotero.Integration.Fields.prototype.updateSession = function() { + var me = this, collectFieldsTime; + return this.get().then(function() { me._session.resetRequest(me._doc); - me._deleteKeys = {}; - me._deleteFields = []; - me._removeCodeFields = []; + me._removeCodeKeys = {}; + me._removeCodeFields = {}; me._bibliographyFields = []; me._bibliographyData = ""; - var collectFieldsTime = (new Date()).getTime(); - me._processFields(fields, function() { - var endTime = (new Date()).getTime(); - if(Zotero.Debug.enabled) { - Zotero.debug("Integration: Updated session data for "+fields.length+" fields in "+ - (endTime-collectFieldsTime)/1000+"; "+ - 1000/((endTime-collectFieldsTime)/fields.length)+" fields/second"); - } - - // load uncited items from bibliography - if(me._bibliographyData && !me._session.bibliographyData) { - try { - me._session.loadBibliographyData(me._bibliographyData); - } catch(e) { - var defaultHandler = function() { - if(e instanceof Zotero.Integration.CorruptFieldException) { - var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+ - Zotero.getString('integration.corruptBibliography.description'); - var result = me._doc.displayAlert(msg, - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); - if(result == 0) { - throw e; - } else { - me._bibliographyData = ""; - me._session.bibliographyHasChanged = true; - me._session.bibliographyDataHasChanged = true; - } - } else { - throw e; - } - }; - if(errorCallback) { - if(!errorCallback(e, defaultHandler)) return; - } else if(!defaultHandler()) { - return; - } - } - } - - // if we are reloading this session, assume no item IDs to be updated except for edited items - if(me._session.reload) { - //this._session.restoreProcessorState(); TODO doesn't appear to be working properly - me._session.updateUpdateIndices(); - Zotero.pumpGenerator(me._session.updateCitations(function(deleteCitations) { - me._deleteFields = me._deleteFields.concat([i for(i in deleteCitations)]); - me._session.updateIndices = {}; - me._session.updateItemIDs = {}; - me._session.citationText = {}; - me._session.bibliographyHasChanged = false; - delete me._session.reload; - if(callback) callback(me._session); - })); - } else { - if(callback) callback(me._session); - } - }, errorCallback); + collectFieldsTime = (new Date()).getTime(); + return me._processFields(); + }).then(function() { + var endTime = (new Date()).getTime(); + if(Zotero.Debug.enabled) { + Zotero.debug("Integration: Updated session data for "+me._fields.length+" fields in "+ + (endTime-collectFieldsTime)/1000+"; "+ + 1000/((endTime-collectFieldsTime)/me._fields.length)+" fields/second"); + } + + // Load uncited items from bibliography + if(me._bibliographyData && !me._session.bibliographyData) { + try { + me._session.loadBibliographyData(me._bibliographyData); + } catch(e) { + var exception = new Zotero.Integration.CorruptBibliographyException(me, e); + exception.setContext(me); + throw exception; + } + } + + // if we are reloading this session, assume no item IDs to be updated except for + // edited items + if(me._session.reload) { + //this._session.restoreProcessorState(); TODO doesn't appear to be working properly + me._session.updateUpdateIndices(); + return Zotero.promiseGenerator(me._session._updateCitations()) + .then(function() { + me._session.updateIndices = {}; + me._session.updateItemIDs = {}; + me._session.citationText = {}; + me._session.bibliographyHasChanged = false; + delete me._session.reload; + }); + } else { + return; + } }); } /** * Keep processing fields until all have been processed */ -Zotero.Integration.Fields.prototype._processFields = function(fields, callback, errorCallback, i) { +Zotero.Integration.Fields.prototype._processFields = function(i) { if(!i) i = 0; var me = this; - for(var n = fields.length; i<n; i++) { - var field = fields[i]; + for(var n = this._fields.length; i<n; i++) { + var field = this._fields[i]; try { var fieldCode = field.getCode(); } catch(e) { - var defaultHandler = function() { - return me._showCorruptFieldError(e, field, callback, errorCallback, i); - }; - - if(errorCallback) { - if(errorCallback(e, defaultHandler)) { - continue; - } else { - return; - } - } else if(!defaultHandler()) { - return; - } + var corruptFieldException = new Zotero.Integration.CorruptFieldException( + "Field code not retrievable", e); + corruptFieldException.setContext(this, i); + throw corruptFieldException; } var [type, content] = this.getCodeTypeAndContent(fieldCode); @@ -1500,24 +1470,23 @@ Zotero.Integration.Fields.prototype._processFields = function(fields, callback, try { this._session.addCitation(i, noteIndex, content); } catch(e) { - var defaultHandler = function() { - if(e instanceof Zotero.Integration.MissingItemException) { - return me._showMissingItemError(e, field, callback, errorCallback, i); - } else if(e instanceof Zotero.Integration.CorruptFieldException) { - return me._showCorruptFieldError(e, field, callback, errorCallback, i); - } else { - throw e; - } - }; + var removeCode = false; - if(errorCallback) { - if(errorCallback(e, defaultHandler)) { - continue; - } else { - return; + if(e instanceof Zotero.Integration.CorruptFieldException) { + e.setContext(this, i) + } else if(e instanceof Zotero.Integration.MissingItemException) { + // Check if we've already decided to remove this field code + for each(var reselectKey in e.reselectKeys) { + if(this._removeCodeKeys[reselectKey]) { + this._removeCodeFields[i] = true; + removeCode = true; + } } - } if(!defaultHandler()) { - return; + if(!removeCode) e.setContext(this, i); + } + + if(!removeCode) { + throw e; } } } else if(type === INTEGRATION_TYPE_BIBLIOGRAPHY) { @@ -1527,29 +1496,24 @@ Zotero.Integration.Fields.prototype._processFields = function(fields, callback, } } } - - if(callback) callback(); } /** * Updates bibliographies and fields within a document * @param {Boolean} forceCitations Whether to regenerate all citations * @param {Boolean} forceBibliography Whether to regenerate all bibliography entries * @param {Boolean} [ignoreCitationChanges] Whether to ignore changes to citations that have been - * modified since they were created, instead of showing a warning + * modified since they were created, instead of showing a warning + * @return {Promise} A promise resolved when the document is updated */ Zotero.Integration.Fields.prototype.updateDocument = function(forceCitations, forceBibliography, - ignoreCitationChanges, callback) { - // update citations - try { - this._session.updateUpdateIndices(forceCitations); - var me = this; - var deleteCitations = Zotero.pumpGenerator(this._session.updateCitations(function(deleteCitations) { - Zotero.pumpGenerator(me._updateDocument(forceCitations, forceBibliography, - ignoreCitationChanges, deleteCitations, callback)); - })); - } catch(e) { - Zotero.Integration.handleError(e, this._doc); - } + ignoreCitationChanges) { + // Update citations + this._session.updateUpdateIndices(forceCitations); + var me = this; + return Zotero.promiseGenerator(this._session._updateCitations()).then(function() { + return Zotero.promiseGenerator(me._updateDocument(forceCitations, forceBibliography, + ignoreCitationChanges)); + }); } /** @@ -1560,161 +1524,160 @@ Zotero.Integration.Fields.prototype.updateDocument = function(forceCitations, fo * modified since they were created, instead of showing a warning */ Zotero.Integration.Fields.prototype._updateDocument = function(forceCitations, forceBibliography, - ignoreCitationChanges, deleteCitations, callback) { - try { - // update citations - this._deleteFields = this._deleteFields.concat([i for(i in deleteCitations)]); - - if(this.progressCallback) { - var nFieldUpdates = [i for(i in this._session.updateIndices)].length; - if(this._session.bibliographyHasChanged || forceBibliography) { - nFieldUpdates += this._bibliographyFields.length*5; - } + ignoreCitationChanges) { + if(this.progressCallback) { + var nFieldUpdates = [i for(i in this._session.updateIndices)].length; + if(this._session.bibliographyHasChanged || forceBibliography) { + nFieldUpdates += this._bibliographyFields.length*5; } - - var nUpdated=0; - for(var i in this._session.updateIndices) { - if(this.progressCallback && nUpdated % 10 == 0) { + } + + var nUpdated=0; + for(var i in this._session.updateIndices) { + if(this.progressCallback && nUpdated % 10 == 0) { + try { this.progressCallback(75+(nUpdated/nFieldUpdates)*25); - yield true; + } catch(e) { + Zotero.logError(e); } + yield; + } + + var citation = this._session.citationsByIndex[i]; + var field = this._fields[i]; + + // If there is no citation, we're deleting it, or we shouldn't update it, ignore + // it + if(!citation || citation.properties.delete) continue; + var isRich = false; + + if(!citation.properties.dontUpdate) { + var formattedCitation = citation.properties.custom + ? citation.properties.custom : this._session.citationText[i]; - var citation = this._session.citationsByIndex[i]; - var field = this._fields[i]; - - // If there is no citation, we're deleting it, or we shouldn't update it, ignore it - if(!citation || deleteCitations[i]) continue; - var isRich = false; + if(formattedCitation.indexOf("\\") !== -1) { + // need to set text as RTF + formattedCitation = "{\\rtf "+formattedCitation+"}" + isRich = true; + } - if(!citation.properties.dontUpdate) { - var formattedCitation = citation.properties.custom - ? citation.properties.custom : this._session.citationText[i]; - - if(formattedCitation.indexOf("\\") !== -1) { - // need to set text as RTF - formattedCitation = "{\\rtf "+formattedCitation+"}" - isRich = true; - } - - if(forceCitations === FORCE_CITATIONS_RESET_TEXT - || citation.properties.formattedCitation !== formattedCitation) { - // Check if citation has been manually modified - if(!ignoreCitationChanges && citation.properties.plainCitation) { - var plainCitation = field.getText(); - if(plainCitation !== citation.properties.plainCitation) { - // Citation manually modified; ask user if they want to save changes - field.select(); - var result = this._doc.displayAlert( - Zotero.getString("integration.citationChanged")+"\n\n"+Zotero.getString("integration.citationChanged.description"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO); - if(result) { - citation.properties.dontUpdate = true; - } + if(forceCitations === FORCE_CITATIONS_RESET_TEXT + || citation.properties.formattedCitation !== formattedCitation) { + // Check if citation has been manually modified + if(!ignoreCitationChanges && citation.properties.plainCitation) { + var plainCitation = field.getText(); + if(plainCitation !== citation.properties.plainCitation) { + // Citation manually modified; ask user if they want to save changes + field.select(); + var result = this._doc.displayAlert( + Zotero.getString("integration.citationChanged")+"\n\n"+Zotero.getString("integration.citationChanged.description"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO); + if(result) { + citation.properties.dontUpdate = true; } } - - if(!citation.properties.dontUpdate) { - field.setText(formattedCitation, isRich); - - citation.properties.formattedCitation = formattedCitation; - citation.properties.plainCitation = field.getText(); - } } - } - - var fieldCode = this._session.getCitationField(citation); - if(fieldCode != citation.properties.field) { - field.setCode( - (this._session.data.prefs.storeReferences ? "ITEM CSL_CITATION" : "ITEM") - +" "+fieldCode); - if(this._session.data.prefs.fieldType === "ReferenceMark" && isRich - && !citation.properties.dontUpdate) { - // For ReferenceMarks with formatting, we need to set the text again, because - // setting the field code removes formatting from the mark. I don't like this. + if(!citation.properties.dontUpdate) { field.setText(formattedCitation, isRich); + + citation.properties.formattedCitation = formattedCitation; + citation.properties.plainCitation = field.getText(); } } - nUpdated++; } - // update bibliographies - if(this._bibliographyFields.length // if bibliography exists - && (this._session.bibliographyHasChanged // and bibliography changed - || forceBibliography)) { // or if we should generate regardless of - // changes - var bibliographyFields = this._bibliographyFields; + var fieldCode = this._session.getCitationField(citation); + if(fieldCode != citation.properties.field) { + field.setCode( + (this._session.data.prefs.storeReferences ? "ITEM CSL_CITATION" : "ITEM") + +" "+fieldCode); - if(forceBibliography || this._session.bibliographyDataHasChanged) { - var bibliographyData = this._session.getBibliographyData(); - for each(var field in bibliographyFields) { - field.setCode("BIBL "+bibliographyData - +(this._session.data.prefs.storeReferences ? " CSL_BIBLIOGRAPHY" : "")); - } + if(this._session.data.prefs.fieldType === "ReferenceMark" && isRich + && !citation.properties.dontUpdate) { + // For ReferenceMarks with formatting, we need to set the text again, because + // setting the field code removes formatting from the mark. I don't like this. + field.setText(formattedCitation, isRich); } - - // get bibliography and format as RTF - var bib = this._session.getBibliography(); - - var bibliographyText = ""; - if(bib) { - bibliographyText = bib[0].bibstart+bib[1].join("\\\r\n")+"\\\r\n"+bib[0].bibend; - - // if bibliography style not set, set it - if(!this._session.data.style.bibliographyStyleHasBeenSet) { - var bibStyle = Zotero.Cite.getBibliographyFormatParameters(bib); - - // set bibliography style - this._doc.setBibliographyStyle(bibStyle.firstLineIndent, bibStyle.indent, - bibStyle.lineSpacing, bibStyle.entrySpacing, bibStyle.tabStops, bibStyle.tabStops.length); - - // set bibliographyStyleHasBeenSet parameter to prevent further changes - this._session.data.style.bibliographyStyleHasBeenSet = true; - this._doc.setDocumentData(this._session.data.serializeXML()); - } + } + nUpdated++; + } + + // update bibliographies + if(this._bibliographyFields.length // if bibliography exists + && (this._session.bibliographyHasChanged // and bibliography changed + || forceBibliography)) { // or if we should generate regardless of + // changes + var bibliographyFields = this._bibliographyFields; + + if(forceBibliography || this._session.bibliographyDataHasChanged) { + var bibliographyData = this._session.getBibliographyData(); + for each(var field in bibliographyFields) { + field.setCode("BIBL "+bibliographyData + +(this._session.data.prefs.storeReferences ? " CSL_BIBLIOGRAPHY" : "")); } + } + + // get bibliography and format as RTF + var bib = this._session.getBibliography(); + + var bibliographyText = ""; + if(bib) { + bibliographyText = bib[0].bibstart+bib[1].join("\\\r\n")+"\\\r\n"+bib[0].bibend; - // set bibliography text - for each(var field in bibliographyFields) { - if(this.progressCallback) { - this.progressCallback(75+(nUpdated/nFieldUpdates)*25); - yield true; - } + // if bibliography style not set, set it + if(!this._session.data.style.bibliographyStyleHasBeenSet) { + var bibStyle = Zotero.Cite.getBibliographyFormatParameters(bib); - if(bibliographyText) { - field.setText(bibliographyText, true); - } else { - field.setText("{Bibliography}", false); - } - nUpdated += 5; + // set bibliography style + this._doc.setBibliographyStyle(bibStyle.firstLineIndent, bibStyle.indent, + bibStyle.lineSpacing, bibStyle.entrySpacing, bibStyle.tabStops, bibStyle.tabStops.length); + + // set bibliographyStyleHasBeenSet parameter to prevent further changes + this._session.data.style.bibliographyStyleHasBeenSet = true; + this._doc.setDocumentData(this._session.data.serializeXML()); } } - // do this operations in reverse in case plug-ins care about order - var sortClosure = function(a, b) { return a-b; }; - this._deleteFields.sort(sortClosure); - for(var i=(this._deleteFields.length-1); i>=0; i--) { - this._fields[this._deleteFields[i]].delete(); - } - this._removeCodeFields.sort(sortClosure); - for(var i=(this._removeCodeFields.length-1); i>=0; i--) { - this._fields[this._removeCodeFields[i]].removeCode(); + // set bibliography text + for each(var field in bibliographyFields) { + if(this.progressCallback) { + try { + this.progressCallback(75+(nUpdated/nFieldUpdates)*25); + } catch(e) { + Zotero.logError(e); + } + yield; + } + + if(bibliographyText) { + field.setText(bibliographyText, true); + } else { + field.setText("{Bibliography}", false); + } + nUpdated += 5; } - - if(callback) { - callback(); + } + + // Do these operations in reverse in case plug-ins care about order + for(var i=this._session.citationsByIndex.length-1; i>=0; i--) { + if(this._session.citationsByIndex[i] && + this._session.citationsByIndex[i].properties.delete) { + this._fields[i].delete(); } - } catch(e) { - Zotero.Integration.handleError(e, this._doc); + } + var removeCodeFields = Object.keys(this._removeCodeFields).sort(); + for(var i=(this._removeCodeFields.length-1); i>=0; i--) { + this._fields[this._removeCodeFields[i]].removeCode(); } } /** * Brings up the addCitationDialog, prepopulated if a citation is provided */ -Zotero.Integration.Fields.prototype.addEditCitation = function(field, callback) { - var newField, citation, fieldIndex, session = this._session, me = this, loadFirst; +Zotero.Integration.Fields.prototype.addEditCitation = function(field) { + var newField, citation, fieldIndex, session = this._session; // if there's already a citation, make sure we have item IDs in addition to keys if(field) { @@ -1723,7 +1686,7 @@ Zotero.Integration.Fields.prototype.addEditCitation = function(field, callback) } catch(e) {} if(code) { - [type, content] = this.getCodeTypeAndContent(code); + var [type, content] = this.getCodeTypeAndContent(code); if(type != INTEGRATION_TYPE_ITEM) { throw new Zotero.Integration.DisplayException("notInCitation"); } @@ -1770,33 +1733,44 @@ Zotero.Integration.Fields.prototype.addEditCitation = function(field, callback) citation = {"citationItems":[], "properties":{}}; } - var io = new Zotero.Integration.CitationEditInterface(citation, field, this, session, newField, callback); + var io = new Zotero.Integration.CitationEditInterface(citation, field, this, session); if(Zotero.Prefs.get("integration.useClassicAddCitationDialog")) { Zotero.Integration.displayDialog(this._doc, - 'chrome://zotero/content/integration/addCitationDialog.xul', 'alwaysRaised,resizable', - io, true); + 'chrome://zotero/content/integration/addCitationDialog.xul', 'alwaysRaised,resizable', + io); } else { var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised') ? 'popup' : 'alwaysRaised') Zotero.Integration.displayDialog(this._doc, - 'chrome://zotero/content/integration/quickFormat.xul', mode, io, true); + 'chrome://zotero/content/integration/quickFormat.xul', mode, io); + } + + if(newField) { + var me = this; + return io.promise.fail(function(e) { + // Try to delete new field on failure + try { + field.delete(); + } catch(e) {} + throw e; + }); + } else { + return io.promise; } } /** * Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js */ -Zotero.Integration.CitationEditInterface = function(citation, field, fields, session, deleteOnCancel, doneCallback) { +Zotero.Integration.CitationEditInterface = function(citation, field, fieldGetter, session) { this.citation = citation; this._field = field; - this._fields = fields; + this._fieldGetter = fieldGetter; this._session = session; - this._deleteOnCancel = deleteOnCancel; - this._doneCallback = doneCallback; - this._sessionUpdated = false; - this._sessionCallbackQueue = false; + this._sessionUpdateResolveErrors = false; + this._sessionUpdateDeferreds = []; // Needed to make this work across boundaries this.wrappedJSObject = this; @@ -1808,73 +1782,119 @@ Zotero.Integration.CitationEditInterface = function(citation, field, fields, ses this.style = session.style; // Start getting citation data - var me = this; - fields.get(function(fields) { + this._acceptDeferred = Q.defer(); + this._fieldIndexPromise = fieldGetter.get().then(function(fields) { for(var i=0, n=fields.length; i<n; i++) { if(fields[i].equals(field)) { - me._fieldIndex = i; - return; + return i; + } + } + }); + + var me = this; + this.promise = this._fieldIndexPromise.then(function(fieldIndex) { + me._fieldIndex = fieldIndex; + return me._acceptDeferred.promise; + }).then(function(progressCallback) { + me._fieldGetter.progressCallback = progressCallback; + return me._updateSession(true); + }).then(function() { + // Add new citation + me._session.addCitation(me._fieldIndex, me._field.getNoteIndex(), me.citation); + me._session.updateIndices[me._fieldIndex] = true; + + // Check if bibliography changed + if(!me._session.bibliographyHasChanged) { + var citationItems = me.citation.citationItems; + for(var i=0, n=citationItems.length; i<n; i++) { + if(me._session.citationsByItemID[citationItems[i].itemID] && + me._session.citationsByItemID[citationItems[i].itemID].length == 1) { + me._session.bibliographyHasChanged = true; + break; + } } } + + // Update document + return me._fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, false, false); }); } Zotero.Integration.CitationEditInterface.prototype = { /** - * Handles an error in updateSession + * Run a function when the session information has been updated + * @param {Boolean} [resolveErrors] Whether to attempt to resolve errors that occur + * while session information is being updated, e.g. by showing a dialog to the + * user. + * @return {Promise} A promise resolved when session information has been updated */ - "_errorHandler":function(e, defaultHandler) { - Zotero.debug('Integration.CitationEditInterface: Error "'+e.toString()+'" caught by handler'); - if(this._haveAccepted) { - try { - return defaultHandler(); - } catch(e) { - if(e instanceof Zotero.Exception.UserCancelled) { - this._field.delete(); + "_updateSession":function _updateSession(resolveErrors) { + var me = this; + if(this._sessionUpdatePromise && this._sessionUpdatePromise.isResolved()) { + // Session has already been updated. If we were deferring resolving an error, + // and we are supposed to resolve it now, then do that + if(this._sessionUpdateError) { + if(resolveErrors && this._sessionUpdateError.attemptToResolve) { + return this._sessionUpdateError.attemptToResolve().then(function() { + delete me._sessionUpdateError; + }); + } else { + return Q.reject(this._sessionUpdateError); } - throw e; + } else { + return Q.resolve(true); } } else { - this._errorOccurred = true; - return true; - } - }, - - /** - * Run a function when the session information has been updated - * @param {Function} sessionUpdatedCallback - */ - "_runWhenSessionUpdated":function runWhenSessionUpdated(sessionUpdatedCallback) { - if(this._sessionUpdated) { - // session has been updated; run callback - sessionUpdatedCallback(); - } else if(this._sessionCallbackQueue) { - // session is being updated; add to queue - this._sessionCallbackQueue.push(sessionUpdatedCallback); - } else { - // session is not yet updated; start update - this._sessionCallbackQueue = [sessionUpdatedCallback]; - var me = this; - me._fields.updateSession(function() { - for(var i=0, n=me._sessionCallbackQueue.length; i<n; i++) { - me._sessionCallbackQueue[i](); - } - me._sessionUpdated = true; - delete me._sessionCallbackQueue; - }, function(e, defaultHandler) { return me._errorHandler(e, defaultHandler) }); + var deferred = Q.defer(); + + this._sessionUpdateResolveErrors = this._sessionUpdateResolveErrors || resolveErrors; + this._sessionUpdateDeferreds.push(deferred); + + if(!this._sessionUpdatePromise) { + // Add deferred to queue + + var me = this; + this._sessionUpdatePromise = this._fieldGetter.updateSession().fail(function(err) { + // If an error occurred, either try to resolve it or reject it + // depending on whether anyone has called _updateSession with + // resolveErrors set to true. This is necessary to prevent field code + // errors from appearing while the user interacts with the QuickFormat + // dialog, since some people find this very confusing. + if(me._sessionUpdateResolveErrors && err.attemptToResolve) { + return err.attemptToResolve(); + } else { + throw err; + } + }).then(function() { + // If no errors occurred, or errors were resolved, resolve promises + for(var i=0; i<me._sessionUpdateDeferreds.length; i++) { + me._sessionUpdateDeferreds[i].resolve(true); + } + }, function(err) { + // Error propagates if attemptToResolve failed or wasn't called to + // begin with + me._sessionUpdateError = err; + for(var i=0; i<me._sessionUpdateDeferreds.length; i++) { + me._sessionUpdateDeferreds[i].reject(err); + } + throw err; + }).end(); + } + + return deferred.promise; } }, /** * Execute a callback with a preview of the given citation - * @param {Function} previewCallback + * @return {Promise} A promise resolved with the previewed citation string */ - "preview":function preview(previewCallback) { + "preview":function preview() { var me = this; - this._runWhenSessionUpdated(function() { + return this._updateSession().then(function() { me.citation.properties.zoteroIndex = me._fieldIndex; me.citation.properties.noteIndex = me._field.getNoteIndex(); - previewCallback(me._session.previewCitation(me.citation)); + return me._session.previewCitation(me.citation); }); }, @@ -1882,8 +1902,8 @@ Zotero.Integration.CitationEditInterface.prototype = { * Sort the citation */ "sort":function() { - // Unlike above, we can do the previewing here without waiting for all the fields to load, - // since they won't change the sorting (I don't think) + // Unlike above, we can do the previewing here without waiting for all the fields + // to load, since they won't change the sorting (I don't think) this._session.previewCitation(this.citation); }, @@ -1891,70 +1911,32 @@ Zotero.Integration.CitationEditInterface.prototype = { * Accept changes to the citation * @param {Function} [progressCallback] A callback to be run when progress has changed. * Receives a number from 0 to 100 indicating current status. - * @param {Boolean} [force] Whether to run accept even if it has been run previously. */ - "accept":function(progressCallback, force) { - var me = this; - - // Don't allow accept to be called multiple times - if(!force && this._haveAccepted) return; - this._haveAccepted = true; - - this._fields.progressCallback = progressCallback; - - if(this._errorOccurred) { - // If an error occurred updating the session, update it again, this time letting the - // error get displayed - Zotero.setTimeout(function() { - me._fields.updateSession(function() { - me._errorOccurred = false; - me._sessionUpdated = true; - me.accept(progressCallback, true); - }, function(e, defaultHandler) { return me._errorHandler(e, defaultHandler) }); - }, 0); - return; - } - - if(this.citation.citationItems.length) { - // Citation - this._runWhenSessionUpdated(function() { - me._session.addCitation(me._fieldIndex, me._field.getNoteIndex(), me.citation); - me._session.updateIndices[me._fieldIndex] = true; - - if(!me._session.bibliographyHasChanged) { - var citationItems = me.citation.citationItems; - for(var i=0, n=citationItems.length; i<n; i++) { - if(me._session.citationsByItemID[citationItems[i].itemID] && - me._session.citationsByItemID[citationItems[i].itemID].length == 1) { - me._session.bibliographyHasChanged = true; - break; - } - } - } - - me._fields.updateDocument(FORCE_CITATIONS_FALSE, false, false, me._doneCallback); - }); - } else { - if(this._deleteOnCancel) this._field.delete(); - if(this._doneCallback) this._doneCallback(); + "accept":function(progressCallback) { + if(!this._acceptDeferred.promise.isResolved()) { + this._acceptDeferred.resolve(progressCallback); } }, /** * Get a list of items used in the current document - * @param {Function} [itemsCallback] A callback to be run with item objects when items have been - * retrieved. + * @return {Promise} A promise resolved by the items */ - "getItems":function(itemsCallback) { - if(this._fieldIndex || Zotero.Utilities.isEmpty(this._session.citationsByItemID)) { + "getItems":function() { + if(this._fieldIndexPromise.isFulfilled() + || Zotero.Utilities.isEmpty(this._session.citationsByItemID)) { // Either we already have field data for this run or we have no item data at all. // Update session before continuing. var me = this; - this._runWhenSessionUpdated(function() { me._getItems(itemsCallback); }); + return this._updateSession().then(function() { + return me._getItems(); + }, function() { + return []; + }); } else { // We have item data left over from a previous run with this document, so we don't need // to wait. - this._getItems(itemsCallback); + return Q.resolve(this._getItems()); } }, @@ -1962,11 +1944,11 @@ Zotero.Integration.CitationEditInterface.prototype = { * Helper function for getItems. Does the same thing, but this can assume that the session data * has already been updated if it should be. */ - "_getItems":function(itemsCallback) { + "_getItems":function() { var citationsByItemID = this._session.citationsByItemID; var ids = [itemID for(itemID in citationsByItemID) if(citationsByItemID[itemID] && citationsByItemID[itemID].length - // Exclude this item + // Exclude the present item && (citationsByItemID[itemID].length > 1 || citationsByItemID[itemID][0].properties.zoteroIndex !== this._fieldIndex))]; @@ -1985,7 +1967,7 @@ Zotero.Integration.CitationEditInterface.prototype = { return indexB - indexA; }); - itemsCallback(Zotero.Cite.getItem(ids)); + return Zotero.Cite.getItem(ids); } } @@ -2059,9 +2041,11 @@ Zotero.Integration.Session.prototype.setData = function(data) { /** * Displays a dialog to set document preferences - * @return {oldData|null|false} Old document data, if there was any; null, if there wasn't; false if cancelled + * @return {Promise} A promise resolved with old document data, if there was any or null, + * if there wasn't, or rejected with Zotero.Exception.UserCancelled if the dialog was + * cancelled. */ -Zotero.Integration.Session.prototype.setDocPrefs = function(doc, primaryFieldType, secondaryFieldType, callback) { +Zotero.Integration.Session.prototype.setDocPrefs = function(doc, primaryFieldType, secondaryFieldType) { var io = new function() { this.wrappedJSObject = this; }; @@ -2077,11 +2061,11 @@ Zotero.Integration.Session.prototype.setDocPrefs = function(doc, primaryFieldTyp } var me = this; - Zotero.Integration.displayDialog(doc, - 'chrome://zotero/content/integration/integrationDocPrefs.xul', '', io, function() { + return Zotero.Integration.displayDialog(doc, + 'chrome://zotero/content/integration/integrationDocPrefs.xul', '', io) + .then(function() { if(!io.style) { - callback(false); - return; + throw Zotero.Exception.UserCancelled("document preferences window"); } // set data @@ -2102,7 +2086,7 @@ Zotero.Integration.Session.prototype.setDocPrefs = function(doc, primaryFieldTyp me.oldCitationIDs = {}; } - callback(oldData ? oldData : null); + return oldData || null; }); } @@ -2110,16 +2094,14 @@ Zotero.Integration.Session.prototype.setDocPrefs = function(doc, primaryFieldTyp * Reselects an item to replace a deleted item * @param exception {Zotero.Integration.MissingItemException} */ -Zotero.Integration.Session.prototype.reselectItem = function(doc, exception, callback) { - var io = new function() { - this.wrappedJSObject = this; - }, +Zotero.Integration.Session.prototype.reselectItem = function(doc, exception) { + var io = new function() { this.wrappedJSObject = this; }, me = this; io.addBorder = Zotero.isWin; io.singleSelection = true; - Zotero.Integration.displayDialog(doc, 'chrome://zotero/content/selectItemsDialog.xul', - 'resizable', io, function() { + return Zotero.Integration.displayDialog(doc, 'chrome://zotero/content/selectItemsDialog.xul', + 'resizable', io).then(function() { if(io.dataOut && io.dataOut.length) { var itemID = io.dataOut[0]; @@ -2134,8 +2116,6 @@ Zotero.Integration.Session.prototype.reselectItem = function(doc, exception, cal // flag for update me.updateItemIDs[itemID] = true; } - - callback(); }); } @@ -2397,7 +2377,7 @@ Zotero.Integration.Session.prototype.unserializeCitation = function(arg, index) try { var citation = JSON.parse(arg.replace(/{{((?:\s*,?"unsorted":(?:true|false)|\s*,?"custom":"(?:(?:\\")?[^"]*\s*)*")*)}}/, "{$1}")); } catch(e) { - throw new Zotero.Integration.CorruptFieldException(arg); + throw new Zotero.Integration.CorruptFieldException(arg, e); } } } @@ -2492,7 +2472,7 @@ Zotero.Integration.Session.prototype.unserializeCitation = function(arg, index) } /** - * marks a citation for removal + * Marks a citation for removal */ Zotero.Integration.Session.prototype.deleteCitation = function(index) { var oldCitation = (this.citationsByIndex[index] ? this.citationsByIndex[index] : false); @@ -2618,68 +2598,58 @@ Zotero.Integration.Session.prototype.formatCitation = function(index, citation) /** * Updates the list of citations to be serialized to the document */ -Zotero.Integration.Session.prototype.updateCitations = function(callback) { - try { - /*var allUpdatesForced = false; - var forcedUpdates = {}; - if(force) { - allUpdatesForced = true; - // make sure at least one citation gets updated - updateLoop: for each(var indexList in [this.newIndices, this.updateIndices]) { - for(var i in indexList) { - if(!this.citationsByIndex[i].properties.delete) { - allUpdatesForced = false; - break updateLoop; - } +Zotero.Integration.Session.prototype._updateCitations = function() { + /*var allUpdatesForced = false; + var forcedUpdates = {}; + if(force) { + allUpdatesForced = true; + // make sure at least one citation gets updated + updateLoop: for each(var indexList in [this.newIndices, this.updateIndices]) { + for(var i in indexList) { + if(!this.citationsByIndex[i].properties.delete) { + allUpdatesForced = false; + break updateLoop; } } - - if(allUpdatesForced) { - for(i in this.citationsByIndex) { - if(this.citationsByIndex[i] && !this.citationsByIndex[i].properties.delete) { - forcedUpdates[i] = true; - break; - } - } - } - }*/ - - if(Zotero.Debug.enabled) { - Zotero.debug("Integration: Indices of new citations"); - Zotero.debug([key for(key in this.newIndices)]); - Zotero.debug("Integration: Indices of updated citations"); - Zotero.debug([key for(key in this.updateIndices)]); } - var deleteCitations = {}; - for each(var indexList in [this.newIndices, this.updateIndices]) { - for(var index in indexList) { - index = parseInt(index); - - var citation = this.citationsByIndex[index]; - if(!citation) continue; - if(citation.properties.delete) { - deleteCitations[index] = true; - continue; - } - if(this.formatCitation(index, citation)) { - this.bibliographyHasChanged = true; + if(allUpdatesForced) { + for(i in this.citationsByIndex) { + if(this.citationsByIndex[i] && !this.citationsByIndex[i].properties.delete) { + forcedUpdates[i] = true; + break; } - this.citeprocCitationIDs[citation.citationID] = true; - delete this.newIndices[index]; - yield true; } } - - /*if(allUpdatesForced) { - this.newIndices = {}; - this.updateIndices = {}; - }*/ - - callback(deleteCitations); - } catch(e) { - Zotero.Integration.handleError(e, this._doc); + }*/ + + if(Zotero.Debug.enabled) { + Zotero.debug("Integration: Indices of new citations"); + Zotero.debug([key for(key in this.newIndices)]); + Zotero.debug("Integration: Indices of updated citations"); + Zotero.debug([key for(key in this.updateIndices)]); } + + + for each(var indexList in [this.newIndices, this.updateIndices]) { + for(var index in indexList) { + index = parseInt(index); + + var citation = this.citationsByIndex[index]; + if(!citation) continue; + if(this.formatCitation(index, citation)) { + this.bibliographyHasChanged = true; + } + this.citeprocCitationIDs[citation.citationID] = true; + delete this.newIndices[index]; + yield; + } + } + + /*if(allUpdatesForced) { + this.newIndices = {}; + this.updateIndices = {}; + }*/ } /** @@ -2838,15 +2808,14 @@ Zotero.Integration.Session.prototype.previewCitation = function(citation) { /** * Edits integration bibliography */ -Zotero.Integration.Session.prototype.editBibliography = function(doc, callback) { +Zotero.Integration.Session.prototype.editBibliography = function(doc) { var bibliographyEditor = new Zotero.Integration.Session.BibliographyEditInterface(this); var io = new function() { this.wrappedJSObject = bibliographyEditor; } this.bibliographyDataHasChanged = this.bibliographyHasChanged = true; - Zotero.Integration.displayDialog(doc, - 'chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io, - callback); + return Zotero.Integration.displayDialog(doc, + 'chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io); } /** diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js @@ -38,6 +38,11 @@ const ZOTERO_CONFIG = { VERSION: "3.0.8.SOURCE" }; +// Commonly used imports accessible anywhere +Components.utils.import("resource://zotero/q.js"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + /* * Core functions */ @@ -1465,16 +1470,17 @@ const ZOTERO_CONFIG = { * If errorHandler is specified, exceptions in the generator will be caught * and passed to the callback */ - this.pumpGenerator = function(generator, ms, errorHandler) { + this.pumpGenerator = function(generator, ms, errorHandler, doneHandler) { _waiting++; var timer = Components.classes["@mozilla.org/timer;1"]. - createInstance(Components.interfaces.nsITimer); + createInstance(Components.interfaces.nsITimer), + yielded; var timerCallback = {"notify":function() { var err = false; _waiting--; try { - if(generator.next()) { + if((yielded = generator.next()) !== false) { _waiting++; return; } @@ -1500,12 +1506,25 @@ const ZOTERO_CONFIG = { } else { throw err; } + } else if(doneHandler) { + doneHandler(yielded); } }} timer.initWithCallback(timerCallback, ms ? ms : 0, Components.interfaces.nsITimer.TYPE_REPEATING_SLACK); // add timer to global scope so that it doesn't get garbage collected before it completes _runningTimers.push(timer); - } + }; + + /** + * Pumps a generator until it yields false. Unlike the above, this returns a promise. + */ + this.promiseGenerator = function(generator, ms) { + var deferred = Q.defer(); + this.pumpGenerator(generator, ms, + function(e) { deferred.reject(e); }, + function(data) { deferred.resolve(data) }); + return deferred.promise; + }; /** * Emulates the behavior of window.setTimeout, but ensures that callbacks do not get called