From d550ac92b4362b91d317beb0b3d6e88a481cec9f Mon Sep 17 00:00:00 2001 From: Simon Kornblith Date: Mon, 16 Jul 2012 21:50:14 -0400 Subject: [PATCH] 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. --- .../zotero/integration/addCitationDialog.js | 8 +- .../content/zotero/integration/quickFormat.js | 4 +- chrome/content/zotero/xpcom/integration.js | 1897 ++++++++--------- chrome/content/zotero/xpcom/zotero.js | 27 +- 4 files changed, 961 insertions(+), 975 deletions(-) diff --git a/chrome/content/zotero/integration/addCitationDialog.js b/chrome/content/zotero/integration/addCitationDialog.js index 32fce10c3..99115a227 100644 --- 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 index 9d73aceef..2b2066955 100644 --- 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 index a4624fc05..449c8793a 100644 --- 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; + /** + * 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; - if(_integrationVersionsOK !== null) { - if(callback) callback(_integrationVersionsOK); - return; - } - - 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; + 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; } - }); - } - - /** - * 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); - } - } - } - - /** - * 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"); } @@ -618,103 +650,6 @@ Zotero.Integration = new function() { return foundWindow; } - /** - * 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 * @@ -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.DisplayException = function(name, params) { - this.name = name; - this.params = params ? params : []; -}; -Zotero.Integration.DisplayException.prototype.toString = function() { return Zotero.getString("integration.error."+this.name, this.params); }; - -Zotero.Integration.CorruptFieldException = function(corruptFieldString) { - this.corruptFieldString = corruptFieldString; +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.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.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); + }); + } + } +}; + +/** + * 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.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 + * @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(require, dontRunSetDocPrefs, callback) { +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; - } + 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; } - - if(!haveBibliography) { - throw new Zotero.Integration.DisplayException("mustInsertBibliography"); - } - - 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=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(); + // 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()); + } } - if(callback) { - callback(); + // 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; } - } catch(e) { - Zotero.Integration.handleError(e, this._doc); + } + + // 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(); + } + } + 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 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; - } - 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(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)]); } + + + 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 index e28089583..8ed06fe1d 100644 --- 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