www

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

zoteroPane.js (150602B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 /*
     27  * This object contains the various functions for the interface
     28  */
     29 var ZoteroPane = new function()
     30 {
     31 	var _unserialized = false;
     32 	this.collectionsView = false;
     33 	this.itemsView = false;
     34 	this.progressWindow = false;
     35 	this._listeners = {};
     36 	this.__defineGetter__('loaded', function () { return _loaded; });
     37 	var _lastSelectedItems = [];
     38 	
     39 	//Privileged methods
     40 	this.destroy = destroy;
     41 	this.isShowing = isShowing;
     42 	this.isFullScreen = isFullScreen;
     43 	this.handleKeyDown = handleKeyDown;
     44 	this.handleKeyUp = handleKeyUp;
     45 	this.setHighlightedRowsCallback = setHighlightedRowsCallback;
     46 	this.handleKeyPress = handleKeyPress;
     47 	this.getSelectedCollection = getSelectedCollection;
     48 	this.getSelectedSavedSearch = getSelectedSavedSearch;
     49 	this.getSelectedItems = getSelectedItems;
     50 	this.getSortedItems = getSortedItems;
     51 	this.getSortField = getSortField;
     52 	this.getSortDirection = getSortDirection;
     53 	this.setItemsPaneMessage = setItemsPaneMessage;
     54 	this.clearItemsPaneMessage = clearItemsPaneMessage;
     55 	this.contextPopupShowing = contextPopupShowing;
     56 	this.viewSelectedAttachment = viewSelectedAttachment;
     57 	this.reportErrors = reportErrors;
     58 	this.displayErrorMessage = displayErrorMessage;
     59 	
     60 	this.document = document;
     61 	
     62 	const COLLECTIONS_HEIGHT = 32; // minimum height of the collections pane and toolbar
     63 	
     64 	var self = this,
     65 		_loaded = false, _madeVisible = false,
     66 		titlebarcolorState, titleState, observerService,
     67 		_reloadFunctions = [], _beforeReloadFunctions = [];
     68 	
     69 	/**
     70 	 * Called when the window containing Zotero pane is open
     71 	 */
     72 	this.init = function () {
     73 		Zotero.debug("Initializing Zotero pane");
     74 		
     75 		// For now, keep actions menu in the DOM and show it in Firefox for development
     76 		if (!Zotero.isStandalone) {
     77 			document.getElementById('zotero-tb-actions-menu-separator').hidden = false;
     78 			document.getElementById('zotero-tb-actions-menu').hidden = false;
     79 		}
     80 		
     81 		// Set "Report Errors..." label via property rather than DTD entity,
     82 		// since we need to reference it in script elsewhere
     83 		document.getElementById('zotero-tb-actions-reportErrors').setAttribute('label',
     84 			Zotero.getString('errorReport.reportErrors'));
     85 		// Set key down handler
     86 		document.getElementById('appcontent').addEventListener('keydown', ZoteroPane_Local.handleKeyDown, true);
     87 		
     88 		// Hide or show the PDF recognizer button
     89 		Zotero.RecognizePDF.addListener('empty', function (row) {
     90 			document.getElementById('zotero-tb-recognize').hidden = true;
     91 		});
     92 		
     93 		Zotero.RecognizePDF.addListener('nonempty', function (row) {
     94 			document.getElementById('zotero-tb-recognize').hidden = false;
     95 		});
     96 		
     97 		_loaded = true;
     98 		
     99 		var zp = document.getElementById('zotero-pane');
    100 		Zotero.setFontSize(zp);
    101 		ZoteroPane_Local.updateLayout();
    102 		ZoteroPane_Local.updateToolbarPosition();
    103 		this.updateWindow();
    104 		window.addEventListener("resize", () => {
    105 			this.updateWindow();
    106 			this.updateToolbarPosition();
    107 		});
    108 		window.setTimeout(ZoteroPane_Local.updateToolbarPosition, 0);
    109 		
    110 		Zotero.updateQuickSearchBox(document);
    111 		
    112 		if (Zotero.isMac) {
    113 			//document.getElementById('zotero-tb-actions-zeroconf-update').setAttribute('hidden', false);
    114 			document.getElementById('zotero-pane-stack').setAttribute('platform', 'mac');
    115 		} else if(Zotero.isWin) {
    116 			document.getElementById('zotero-pane-stack').setAttribute('platform', 'win');
    117 		}
    118 		
    119 		// Set the sync tooltip label
    120 		Components.utils.import("resource://zotero/config.js");
    121 		document.getElementById('zotero-tb-sync-label').value = Zotero.getString(
    122 			'sync.syncWith', ZOTERO_CONFIG.DOMAIN_NAME
    123 		);
    124 		
    125 		if (Zotero.isStandalone) {
    126 			document.getElementById('zotero-tb-feed-add-fromPage').hidden = true;
    127 			document.getElementById('zotero-tb-feed-add-fromPage-menu').hidden = true;
    128 		}
    129 		
    130 		// register an observer for Zotero reload
    131 		observerService = Components.classes["@mozilla.org/observer-service;1"]
    132 					  .getService(Components.interfaces.nsIObserverService);
    133 		observerService.addObserver(_reloadObserver, "zotero-reloaded", false);
    134 		observerService.addObserver(_reloadObserver, "zotero-before-reload", false);
    135 		this.addBeforeReloadListener(function(newMode) {
    136 			if(newMode == "connector") {
    137 				ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('connector.standaloneOpen'));
    138 			}
    139 			return;
    140 		});
    141 		this.addReloadListener(_loadPane);
    142 		
    143 		// continue loading pane
    144 		_loadPane();
    145 	};
    146 	
    147 	/**
    148 	 * Called on window load or when pane has been reloaded after switching into or out of connector
    149 	 * mode
    150 	 */
    151 	function _loadPane() {
    152 		if(!Zotero || !Zotero.initialized || Zotero.isConnector) return;
    153 		
    154 		// Set flags for hi-res displays
    155 		Zotero.hiDPI = window.devicePixelRatio > 1;
    156 		Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : "";
    157 		
    158 		ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
    159 		
    160 		// Add a default progress window
    161 		ZoteroPane_Local.progressWindow = new Zotero.ProgressWindow({ window });
    162 		
    163 		//Initialize collections view
    164 		ZoteroPane_Local.collectionsView = new Zotero.CollectionTreeView();
    165 		// Handle an error in setTree()/refresh()
    166 		ZoteroPane_Local.collectionsView.onError = function (e) {
    167 			ZoteroPane_Local.displayErrorMessage();
    168 		};
    169 		var collectionsTree = document.getElementById('zotero-collections-tree');
    170 		collectionsTree.view = ZoteroPane_Local.collectionsView;
    171 		collectionsTree.controllers.appendController(new Zotero.CollectionTreeCommandController(collectionsTree));
    172 		collectionsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true);
    173 		collectionsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true);
    174 		
    175 		// Clear items view, so that the load registers as a new selected collection when switching
    176 		// between modes
    177 		ZoteroPane_Local.itemsView = null;
    178 		
    179 		var itemsTree = document.getElementById('zotero-items-tree');
    180 		itemsTree.controllers.appendController(new Zotero.ItemTreeCommandController(itemsTree));
    181 		itemsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true);
    182 		itemsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true);
    183 		
    184 		var menu = document.getElementById("contentAreaContextMenu");
    185 		menu.addEventListener("popupshowing", ZoteroPane_Local.contextPopupShowing, false);
    186 		
    187 		var tagSelector = document.getElementById('zotero-tag-selector');
    188 		tagSelector.onchange = function () {
    189 			return ZoteroPane_Local.updateTagFilter();
    190 		};
    191 		
    192 		Zotero.Keys.windowInit(document);
    193 		
    194 		if (Zotero.restoreFromServer) {
    195 			Zotero.restoreFromServer = false;
    196 			
    197 			setTimeout(function () {
    198 				var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    199 										.getService(Components.interfaces.nsIPromptService);
    200 				var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    201 									+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
    202 				var index = ps.confirmEx(
    203 					null,
    204 					"Zotero Restore",
    205 					"The local Zotero database has been cleared."
    206 						+ " "
    207 						+ "Would you like to restore from the Zotero server now?",
    208 					buttonFlags,
    209 					"Sync Now",
    210 					null, null, null, {}
    211 				);
    212 				
    213 				if (index == 0) {
    214 					Zotero.Sync.Server.sync({
    215 						onSuccess: function () {
    216 							Zotero.Sync.Runner.updateIcons();
    217 							
    218 							ps.alert(
    219 								null,
    220 								"Restore Completed",
    221 								"The local Zotero database has been successfully restored."
    222 							);
    223 						},
    224 						
    225 						onError: function (msg) {
    226 							ps.alert(
    227 								null,
    228 								"Restore Failed",
    229 								"An error occurred while restoring from the server:\n\n"
    230 									+ msg
    231 							);
    232 							
    233 							Zotero.Sync.Runner.error(msg);
    234 						}
    235 					});
    236 				}
    237 			}, 1000);
    238 		}
    239 		// If the database was initialized or there are no sync credentials and
    240 		// Zotero hasn't been run before in this profile, display the start page
    241 		// -- this way the page won't be displayed when they sync their DB to
    242 		// another profile or if the DB is initialized erroneously (e.g. while
    243 		// switching data directory locations)
    244 		else if (Zotero.Prefs.get('firstRun2')) {
    245 			if (Zotero.Schema.dbInitialized || !Zotero.Sync.Server.enabled) {
    246 				setTimeout(function () {
    247 					ZoteroPane_Local.loadURI(ZOTERO_CONFIG.START_URL);
    248 				}, 400);
    249 			}
    250 			Zotero.Prefs.set('firstRun2', false);
    251 			try {
    252 				Zotero.Prefs.clear('firstRun');
    253 			}
    254 			catch (e) {}
    255 		}
    256 		
    257 		if (Zotero.openPane) {
    258 			Zotero.openPane = false;
    259 			setTimeout(function () {
    260 				ZoteroPane_Local.show();
    261 			}, 0);
    262 		}
    263 		
    264 		// TEMP: Clean up extra files from Mendeley imports <5.0.51
    265 		setTimeout(async function () {
    266 			var needsCleanup = await Zotero.DB.valueQueryAsync(
    267 				"SELECT COUNT(*) FROM settings WHERE setting='mImport' AND key='cleanup'"
    268 			)
    269 			if (!needsCleanup) return;
    270 			
    271 			Components.utils.import("chrome://zotero/content/import/mendeley/mendeleyImport.js");
    272 			var importer = new Zotero_Import_Mendeley();
    273 			importer.deleteNonPrimaryFiles();
    274 		}, 10000)
    275 	}
    276 	
    277 	
    278 	/*
    279 	 * Create the New Item (+) submenu with each item type
    280 	 */
    281 	this.buildItemTypeSubMenu = function () {
    282 		var moreMenu = document.getElementById('zotero-tb-add-more');
    283 		
    284 		while (moreMenu.hasChildNodes()) {
    285 			moreMenu.removeChild(moreMenu.firstChild);
    286 		}
    287 		
    288 		// Sort by localized name
    289 		var t = Zotero.ItemTypes.getSecondaryTypes();
    290 		var itemTypes = [];
    291 		for (var i=0; i<t.length; i++) {
    292 			itemTypes.push({
    293 				id: t[i].id,
    294 				name: t[i].name,
    295 				localized: Zotero.ItemTypes.getLocalizedString(t[i].id)
    296 			});
    297 		}
    298 		var collation = Zotero.getLocaleCollation();
    299 		itemTypes.sort(function(a, b) {
    300 			return collation.compareString(1, a.localized, b.localized);
    301 		});
    302 		
    303 		for (var i = 0; i<itemTypes.length; i++) {
    304 			var menuitem = document.createElement("menuitem");
    305 			menuitem.setAttribute("label", itemTypes[i].localized);
    306 			menuitem.setAttribute("tooltiptext", "");
    307 			let type = itemTypes[i].id;
    308 			menuitem.addEventListener("command", function() { ZoteroPane_Local.newItem(type, {}, null, true).done(); }, false);
    309 			moreMenu.appendChild(menuitem);
    310 		}
    311 	}
    312 	
    313 	
    314 	this.updateNewItemTypes = function () {
    315 		var addMenu = document.getElementById('zotero-tb-add').firstChild;
    316 		
    317 		// Remove all nodes so we can regenerate
    318 		var options = addMenu.getElementsByAttribute("class", "zotero-tb-add");
    319 		while (options.length) {
    320 			var p = options[0].parentNode;
    321 			p.removeChild(options[0]);
    322 		}
    323 		
    324 		var separator = addMenu.firstChild;
    325 		
    326 		// Sort by localized name
    327 		var t = Zotero.ItemTypes.getPrimaryTypes();
    328 		var itemTypes = [];
    329 		for (var i=0; i<t.length; i++) {
    330 			itemTypes.push({
    331 				id: t[i].id,
    332 				name: t[i].name,
    333 				localized: Zotero.ItemTypes.getLocalizedString(t[i].id)
    334 			});
    335 		}
    336 		var collation = Zotero.getLocaleCollation();
    337 		itemTypes.sort(function(a, b) {
    338 			return collation.compareString(1, a.localized, b.localized);
    339 		});
    340 		
    341 		for (var i = 0; i<itemTypes.length; i++) {
    342 			var menuitem = document.createElement("menuitem");
    343 			menuitem.setAttribute("label", itemTypes[i].localized);
    344 			menuitem.setAttribute("tooltiptext", "");
    345 			let type = itemTypes[i].id;
    346 			menuitem.addEventListener("command", function() { ZoteroPane_Local.newItem(type, {}, null, true).done(); }, false);
    347 			menuitem.className = "zotero-tb-add";
    348 			addMenu.insertBefore(menuitem, separator);
    349 		}
    350 	}
    351 	
    352 	
    353 	
    354 	/*
    355 	 * Called when the window closes
    356 	 */
    357 	function destroy()
    358 	{
    359 		if (!Zotero || !Zotero.initialized || !_loaded) {
    360 			return;
    361 		}
    362 		
    363 		if(this.isShowing()) {
    364 			this.serializePersist();
    365 		}
    366 		
    367 		var tagSelector = document.getElementById('zotero-tag-selector');
    368 		tagSelector.unregister();
    369 		
    370 		if(this.collectionsView) this.collectionsView.unregister();
    371 		if(this.itemsView) this.itemsView.unregister();
    372 		
    373 		observerService.removeObserver(_reloadObserver, "zotero-reloaded");
    374 	}
    375 	
    376 	/**
    377 	 * Called before Zotero pane is to be made visible
    378 	 * @return {Boolean} True if Zotero pane should be loaded, false otherwise (if an error
    379 	 * 		occurred)
    380 	 */
    381 	this.makeVisible = Zotero.Promise.coroutine(function* () {
    382 		if (Zotero.locked) {
    383 			Zotero.showZoteroPaneProgressMeter();
    384 		}
    385 		
    386 		yield Zotero.unlockPromise;
    387 		
    388 		// The items pane is hidden initially to avoid showing column lines
    389 		document.getElementById('zotero-items-tree').hidden = false;
    390 		Zotero.hideZoteroPaneOverlays();
    391 		
    392 		// If pane not loaded, load it or display an error message
    393 		if (!ZoteroPane_Local.loaded) {
    394 			ZoteroPane_Local.init();
    395 		}
    396 		
    397 		// If Zotero could not be initialized, display an error message and return
    398 		if (!Zotero || Zotero.skipLoading) {
    399 			this.displayStartupError();
    400 			return false;
    401 		}
    402 		
    403 		if(!_madeVisible) {
    404 			this.buildItemTypeSubMenu();
    405 		}
    406 		_madeVisible = true;
    407 		
    408 		this.unserializePersist();
    409 		this.updateLayout();
    410 		this.updateToolbarPosition();
    411 		this.updateTagSelectorSize();
    412 		
    413 		// restore saved row selection (for tab switching)
    414 		// TODO: Remove now that no tab mode?
    415 		var containerWindow = window;
    416 		if(containerWindow.zoteroSavedCollectionSelection) {
    417 			this.collectionsView.onLoad.addListener(Zotero.Promise.coroutine(function* () {
    418 				yield this.collectionsView.selectByID(containerWindow.zoteroSavedCollectionSelection);
    419 				
    420 				if (containerWindow.zoteroSavedItemSelection) {
    421 					this.itemsView.onLoad.addListener(function () {
    422 						this.itemsView.rememberSelection(containerWindow.zoteroSavedItemSelection);
    423 						delete containerWindow.zoteroSavedItemSelection;
    424 					}.bind(this));
    425 				}
    426 				
    427 				delete containerWindow.zoteroSavedCollectionSelection;
    428 			}.bind(this)));
    429 		}
    430 		
    431 		// Focus the quicksearch on pane open
    432 		var searchBar = document.getElementById('zotero-tb-search');
    433 		setTimeout(function () {
    434 			searchBar.inputField.select();
    435 		}, 1);
    436 		
    437 		//
    438 		// TEMP: Remove after people are no longer upgrading from Zotero for Firefox
    439 		//
    440 		var showFxProfileWarning = false;
    441 		var pref = 'firstRun.skipFirefoxProfileAccessCheck';
    442 		if (Zotero.fxProfileAccessError != undefined && Zotero.fxProfileAccessError) {
    443 			showFxProfileWarning = true;
    444 		}
    445 		else if (!Zotero.Prefs.get(pref)) {
    446 			showFxProfileWarning = !(yield Zotero.Profile.checkFirefoxProfileAccess());
    447 		}
    448 		if (showFxProfileWarning) {
    449 			Zotero.uiReadyPromise.delay(2000).then(function () {
    450 				var ps = Services.prompt;
    451 				var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
    452 					+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING;
    453 				var text = "Zotero was unable to access your Firefox profile to check for "
    454 					+ "existing Zotero data.\n\n"
    455 					+ "If you’ve upgraded from Zotero 4.0 for Firefox and don’t see the data "
    456 					+ "you expect, it may be located elsewhere on your computer. "
    457 					+ "Click “More Information” for help restoring your previous data.\n\n"
    458 					+ "If you’re new to Zotero, you can ignore this message.";
    459 				var url = 'https://www.zotero.org/support/kb/data_missing_after_zotero_5_upgrade';
    460 				var dontShowAgain = {};
    461 				let index = ps.confirmEx(null,
    462 					Zotero.getString('general.warning'),
    463 					text,
    464 					buttonFlags,
    465 					Zotero.getString('general.moreInformation'),
    466 					"Ignore",
    467 					null,
    468 					Zotero.getString('general.dontShowAgain'),
    469 					dontShowAgain
    470 				);
    471 				if (dontShowAgain.value) {
    472 					Zotero.Prefs.set(pref, true)
    473 				}
    474 				if (index == 0) {
    475 					this.loadURI(url);
    476 				}
    477 			}.bind(this));
    478 		}
    479 		// Once we successfully find it once, don't bother checking again
    480 		else {
    481 			Zotero.Prefs.set(pref, true);
    482 		}
    483 		
    484 		// Auto-sync on pane open or if new account
    485 		if (Zotero.Prefs.get('sync.autoSync') || Zotero.initAutoSync) {
    486 			yield Zotero.proxyAuthComplete;
    487 			yield Zotero.uiReadyPromise;
    488 			
    489 			if (!Zotero.Sync.Runner.enabled) {
    490 				Zotero.debug('Sync not enabled -- skipping auto-sync', 4);
    491 			}
    492 			else if (Zotero.Sync.Runner.syncInProgress) {
    493 				Zotero.debug('Sync already running -- skipping auto-sync', 4);
    494 			}
    495 			else if (Zotero.Sync.Server.manualSyncRequired) {
    496 				Zotero.debug('Manual sync required -- skipping auto-sync', 4);
    497 			}
    498 			else if (showFxProfileWarning) {
    499 				Zotero.debug('Firefox profile access error -- skipping initial auto-sync', 4);
    500 			}
    501 			else {
    502 				Zotero.Sync.Runner.sync({
    503 					background: true
    504 				}).then(() => Zotero.initAutoSync = false);
    505 			}
    506 		}
    507 		
    508 		// Set sync icon to spinning if there's an existing sync
    509 		//
    510 		// We don't bother setting an existing error state at open
    511 		if (Zotero.Sync.Runner.syncInProgress) {
    512 			Zotero.Sync.Runner.updateIcons('animate');
    513 		}
    514 		
    515 		return true;
    516 	});
    517 	
    518 	/**
    519 	 * Function to be called before ZoteroPane_Local is hidden. Does not actually hide the Zotero pane.
    520 	 */
    521 	this.makeHidden = function() {
    522 		this.serializePersist();
    523 	}
    524 	
    525 	function isShowing() {
    526 		var zoteroPane = document.getElementById('zotero-pane-stack');
    527 		return zoteroPane
    528 			&& zoteroPane.getAttribute('hidden') != 'true'
    529 			&& zoteroPane.getAttribute('collapsed') != 'true';
    530 	}
    531 	
    532 	function isFullScreen() {
    533 		return document.getElementById('zotero-pane-stack').getAttribute('fullscreenmode') == 'true';
    534 	}
    535 	
    536 	
    537 	/*
    538 	 * Trigger actions based on keyboard shortcuts
    539 	 */
    540 	function handleKeyDown(event, from) {
    541 		try {
    542 			// Ignore keystrokes outside of Zotero pane
    543 			if (!(event.originalTarget.ownerDocument instanceof XULDocument)) {
    544 				return;
    545 			}
    546 		}
    547 		catch (e) {
    548 			Zotero.debug(e);
    549 		}
    550 		
    551 		if (Zotero.locked) {
    552 			event.preventDefault();
    553 			return;
    554 		}
    555 		
    556 		if (from == 'zotero-pane') {
    557 			// Highlight collections containing selected items
    558 			//
    559 			// We use Control (17) on Windows because Alt triggers the menubar;
    560 			// 	otherwise we use Alt/Option (18)
    561 			if ((Zotero.isWin && event.keyCode == 17 && !event.altKey) ||
    562 					(!Zotero.isWin && event.keyCode == 18 && !event.ctrlKey)
    563 					&& !event.shiftKey && !event.metaKey) {
    564 				
    565 				this.highlightTimer = Components.classes["@mozilla.org/timer;1"].
    566 					createInstance(Components.interfaces.nsITimer);
    567 				// {} implements nsITimerCallback
    568 				this.highlightTimer.initWithCallback({
    569 					notify: ZoteroPane_Local.setHighlightedRowsCallback
    570 				}, 225, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
    571 			}
    572 			// Unhighlight on key up
    573 			else if ((Zotero.isWin && event.ctrlKey) ||
    574 					(!Zotero.isWin && event.altKey)) {
    575 				if (this.highlightTimer) {
    576 					this.highlightTimer.cancel();
    577 					this.highlightTimer = null;
    578 				}
    579 				ZoteroPane_Local.collectionsView.setHighlightedRows();
    580 			}
    581 		}
    582 	}
    583 	
    584 	function handleKeyUp(event) {
    585 		var from = event.originalTarget.id;
    586 		if (from == 'zotero-items-tree') {
    587 			if ((Zotero.isWin && event.keyCode == 17) ||
    588 					(!Zotero.isWin && event.keyCode == 18)) {
    589 				if (this.highlightTimer) {
    590 					this.highlightTimer.cancel();
    591 					this.highlightTimer = null;
    592 				}
    593 				ZoteroPane_Local.collectionsView.setHighlightedRows();
    594 				return;
    595 			}
    596 		}
    597 	}
    598 	
    599 	
    600 	/*
    601 	 * Highlights collections containing selected items on Ctrl (Win) or
    602 	 * Option/Alt (Mac/Linux) press
    603 	 */
    604 	function setHighlightedRowsCallback() {
    605 		var itemIDs = ZoteroPane_Local.getSelectedItems(true);
    606 		// If no items or an unreasonable number, don't try
    607 		if (!itemIDs || !itemIDs.length || itemIDs.length > 100) return;
    608 		
    609 		Zotero.Promise.coroutine(function* () {
    610 			var collectionIDs = yield Zotero.Collections.getCollectionsContainingItems(itemIDs, true);
    611 			var ids = collectionIDs.map(id => "C" + id);
    612 			var userLibraryID = Zotero.Libraries.userLibraryID;
    613 			var allInPublications = Zotero.Items.get(itemIDs).every((item) => {
    614 				return item.libraryID == userLibraryID && item.inPublications;
    615 			})
    616 			if (allInPublications) {
    617 				ids.push("P" + Zotero.Libraries.userLibraryID);
    618 			}
    619 			if (ids.length) {
    620 				ZoteroPane_Local.collectionsView.setHighlightedRows(ids);
    621 			}
    622 		})();
    623 	}
    624 	
    625 	
    626 	function handleKeyPress(event) {
    627 		var from = event.originalTarget.id;
    628 		
    629 		// Ignore keystrokes if Zotero pane is closed
    630 		var zoteroPane = document.getElementById('zotero-pane-stack');
    631 		if (zoteroPane.getAttribute('hidden') == 'true' ||
    632 				zoteroPane.getAttribute('collapsed') == 'true') {
    633 			return;
    634 		}
    635 		
    636 		if (Zotero.locked) {
    637 			event.preventDefault();
    638 			return;
    639 		}
    640 
    641 		var key = String.fromCharCode(event.which);
    642 		if (key) {
    643 			var command = Zotero.Keys.getCommand(key);
    644 		}
    645 		
    646 		if (from == 'zotero-collections-tree') {
    647 			if ((event.keyCode == event.DOM_VK_BACK_SPACE && Zotero.isMac) ||
    648 					event.keyCode == event.DOM_VK_DELETE) {
    649 				var deleteItems = event.metaKey || (!Zotero.isMac && event.shiftKey);
    650 				ZoteroPane_Local.deleteSelectedCollection(deleteItems);
    651 				event.preventDefault();
    652 				return;
    653 			}
    654 		}
    655 		else if (from == 'zotero-items-tree') {
    656 			// Focus TinyMCE explicitly on tab key, since the normal focusing doesn't work right
    657 			if (!event.shiftKey && event.keyCode == event.DOM_VK_TAB) {
    658 				var deck = document.getElementById('zotero-item-pane-content');
    659 				if (deck.selectedPanel.id == 'zotero-view-note') {
    660 					document.getElementById('zotero-note-editor').focus();
    661 					event.preventDefault();
    662 					return;
    663 				}
    664 			}
    665 			else if ((event.keyCode == event.DOM_VK_BACK_SPACE && Zotero.isMac) ||
    666 					event.keyCode == event.DOM_VK_DELETE) {
    667 				// If Cmd/Shift delete, use forced mode, which does different
    668 				// things depending on the context
    669 				var force = event.metaKey || (!Zotero.isMac && event.shiftKey);
    670 				ZoteroPane_Local.deleteSelectedItems(force);
    671 				event.preventDefault();
    672 				return;
    673 			}
    674 			else if (event.keyCode == event.DOM_VK_RETURN) {
    675 				var items = this.itemsView.getSelectedItems();
    676 				// Don't do anything if more than 20 items selected
    677 				if (!items.length || items.length > 20) {
    678 					return;
    679 				}
    680 				ZoteroPane_Local.viewItems(items, event);
    681 				// These don't seem to do anything. Instead we override
    682 				// the tree binding's _handleEnter method in itemTreeView.js.
    683 				//event.preventDefault();
    684 				//event.stopPropagation();
    685 				return;
    686 			}
    687 			else if (command == 'toggleRead') {
    688 				// Toggle read/unread
    689 				let row = this.collectionsView.getRow(this.collectionsView.selection.currentIndex);
    690 				if (!row || !row.isFeed()) return;
    691 				this.toggleSelectedItemsRead();
    692 				if (itemReadPromise) {
    693 					itemReadPromise.cancel();
    694 					itemReadPromise = null;
    695 				}
    696 				return;
    697 			}
    698 		}
    699 		
    700 		// Ignore modifiers other than Ctrl-Shift/Cmd-Shift
    701 		if (!((Zotero.isMac ? event.metaKey : event.ctrlKey) && event.shiftKey)) {
    702 			return;
    703 		}
    704 		
    705 		if (!key) {
    706 			Zotero.debug('No key');
    707 			return;
    708 		}
    709 		
    710 		if (!command) {
    711 			return;
    712 		}
    713 		
    714 		Zotero.debug('Keyboard shortcut: ' + command);
    715 		
    716 		// Errors don't seem to make it out otherwise
    717 		try {
    718 			switch (command) {
    719 				case 'openZotero':
    720 					try {
    721 						// Ignore Cmd-Shift-Z keystroke in text areas
    722 						if (Zotero.isMac && key == 'Z' &&
    723 								(event.originalTarget.localName == 'input'
    724 									|| event.originalTarget.localName == 'textarea')) {
    725 							try {
    726 								var isSearchBar = event.originalTarget.parentNode.parentNode.id == 'zotero-tb-search';
    727 							}
    728 							catch (e) {
    729 								Zotero.debug(e, 1);
    730 								Components.utils.reportError(e);
    731 							}
    732 							if (!isSearchBar) {
    733 								Zotero.debug('Ignoring keystroke in text field');
    734 								return;
    735 							}
    736 						}
    737 					}
    738 					catch (e) {
    739 						Zotero.debug(e);
    740 					}
    741 					if (window.ZoteroOverlay) window.ZoteroOverlay.toggleDisplay()
    742 					break;
    743 				case 'library':
    744 					document.getElementById('zotero-collections-tree').focus();
    745 					break;
    746 				case 'quicksearch':
    747 					document.getElementById('zotero-tb-search').select();
    748 					break;
    749 				case 'newItem':
    750 					Zotero.Promise.coroutine(function* () {
    751 						// Default to most recent item type from here or the
    752 						// New Type menu
    753 						var mru = Zotero.Prefs.get('newItemTypeMRU');
    754 						// Or fall back to 'book'
    755 						var typeID = mru ? mru.split(',')[0] : 2;
    756 						yield ZoteroPane_Local.newItem(typeID);
    757 						let itemBox = document.getElementById('zotero-editpane-item-box');
    758 						var menu = itemBox.itemTypeMenu;
    759 						var self = this;
    760 						var handleTypeChange = function () {
    761 							self.addItemTypeToNewItemTypeMRU(this.itemTypeMenu.value);
    762 							itemBox.removeHandler('itemtypechange', handleTypeChange);
    763 						};
    764 						// Only update the MRU when the menu is opened for the
    765 						// keyboard shortcut, not on subsequent opens
    766 						var removeTypeChangeHandler = function () {
    767 							itemBox.removeHandler('itemtypechange', handleTypeChange);
    768 							itemBox.itemTypeMenu.firstChild.removeEventListener('popuphiding', removeTypeChangeHandler);
    769 							// Focus the title field after menu closes
    770 							itemBox.focusFirstField();
    771 						};
    772 						itemBox.addHandler('itemtypechange', handleTypeChange);
    773 						itemBox.itemTypeMenu.firstChild.addEventListener('popuphiding', removeTypeChangeHandler);
    774 						
    775 						menu.focus();
    776 						document.getElementById('zotero-editpane-item-box').itemTypeMenu.menupopup.openPopup(menu, "before_start", 0, 0);
    777 					})();
    778 					break;
    779 				case 'newNote':
    780 					// If a regular item is selected, use that as the parent.
    781 					// If a child item is selected, use its parent as the parent.
    782 					// Otherwise create a standalone note.
    783 					var parentKey = false;
    784 					var items = ZoteroPane_Local.getSelectedItems();
    785 					if (items.length == 1) {
    786 						if (items[0].isRegularItem()) {
    787 							parentKey = items[0].key;
    788 						}
    789 						else {
    790 							parentKey = items[0].parentItemKey;
    791 						}
    792 					}
    793 					// Use key that's not the modifier as the popup toggle
    794 					ZoteroPane_Local.newNote(event.altKey, parentKey);
    795 					break;
    796 				case 'toggleTagSelector':
    797 					ZoteroPane_Local.toggleTagSelector();
    798 					break;
    799 				case 'sync':
    800 					Zotero.Sync.Runner.sync();
    801 					break;
    802 				case 'saveToZotero':
    803 					var collectionTreeRow = this.collectionsView.selectedTreeRow;
    804 					if (collectionTreeRow.isFeed()) {
    805 						ZoteroItemPane.translateSelectedItems();
    806 					} else {
    807 						Zotero.debug(command + ' does not do anything in non-feed views')
    808 					}
    809 					break;
    810 				case 'toggleAllRead':
    811 					var collectionTreeRow = this.collectionsView.selectedTreeRow;
    812 					if (collectionTreeRow.isFeed()) {
    813 						this.markFeedRead();
    814 					}
    815 					break;
    816 				
    817 				// Handled by <key>s in standalone.js, pointing to <command>s in zoteroPane.xul,
    818 				// which are enabled or disabled by this.updateQuickCopyCommands(), called by
    819 				// this.itemSelected()
    820 				case 'copySelectedItemCitationsToClipboard':
    821 				case 'copySelectedItemsToClipboard':
    822 					return;
    823 				
    824 				default:
    825 					throw ('Command "' + command + '" not found in ZoteroPane_Local.handleKeyDown()');
    826 			}
    827 		}
    828 		catch (e) {
    829 			Zotero.debug(e, 1);
    830 			Components.utils.reportError(e);
    831 		}
    832 		
    833 		event.preventDefault();
    834 	}
    835 	
    836 	
    837 	/*
    838 	 * Create a new item
    839 	 *
    840 	 * _data_ is an optional object with field:value for itemData
    841 	 */
    842 	this.newItem = Zotero.Promise.coroutine(function* (typeID, data, row, manual)
    843 	{
    844 		if ((row === undefined || row === null) && this.collectionsView.selection) {
    845 			row = this.collectionsView.selection.currentIndex;
    846 			
    847 			// Make sure currently selected view is editable
    848 			if (!this.canEdit(row)) {
    849 				this.displayCannotEditLibraryMessage();
    850 				return;
    851 			}
    852 		}
    853 		
    854 		yield ZoteroItemPane.blurOpenField();
    855 		
    856 		if (row !== undefined && row !== null) {
    857 			var collectionTreeRow = this.collectionsView.getRow(row);
    858 			var libraryID = collectionTreeRow.ref.libraryID;
    859 		}
    860 		else {
    861 			var libraryID = Zotero.Libraries.userLibraryID;
    862 			var collectionTreeRow = null;
    863 		}
    864 		
    865 		let itemID;
    866 		yield Zotero.DB.executeTransaction(function* () {
    867 			var item = new Zotero.Item(typeID);
    868 			item.libraryID = libraryID;
    869 			for (var i in data) {
    870 				item.setField(i, data[i]);
    871 			}
    872 			itemID = yield item.save();
    873 			
    874 			if (collectionTreeRow && collectionTreeRow.isCollection()) {
    875 				yield collectionTreeRow.ref.addItem(itemID);
    876 			}
    877 		});
    878 		
    879 		//set to Info tab
    880 		document.getElementById('zotero-view-item').selectedIndex = 0;
    881 		
    882 		if (manual) {
    883 			// Update most-recently-used list for New Item menu
    884 			this.addItemTypeToNewItemTypeMRU(typeID);
    885 			
    886 			// Focus the title field
    887 			document.getElementById('zotero-editpane-item-box').focusFirstField();
    888 		}
    889 		
    890 		return Zotero.Items.getAsync(itemID);
    891 	});
    892 	
    893 	
    894 	this.addItemTypeToNewItemTypeMRU = function (itemTypeID) {
    895 		var mru = Zotero.Prefs.get('newItemTypeMRU');
    896 		if (mru) {
    897 			var mru = mru.split(',');
    898 			var pos = mru.indexOf(itemTypeID + '');
    899 			if (pos != -1) {
    900 				mru.splice(pos, 1);
    901 			}
    902 			mru.unshift(itemTypeID);
    903 		}
    904 		else {
    905 			var mru = [itemTypeID + ''];
    906 		}
    907 		Zotero.Prefs.set('newItemTypeMRU', mru.slice(0, 5).join(','));
    908 	}
    909 	
    910 	
    911 	this.newCollection = Zotero.Promise.coroutine(function* (parentKey) {
    912 		if (!this.canEditLibrary()) {
    913 			this.displayCannotEditLibraryMessage();
    914 			return;
    915 		}
    916 		
    917 		var libraryID = this.getSelectedLibraryID();
    918 		
    919 		var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    920 								.getService(Components.interfaces.nsIPromptService);
    921 		var untitled = yield Zotero.DB.getNextName(
    922 			libraryID,
    923 			'collections',
    924 			'collectionName',
    925 			Zotero.getString('pane.collections.untitled')
    926 		);
    927 		
    928 		var newName = { value: untitled };
    929 		var result = promptService.prompt(window,
    930 			Zotero.getString('pane.collections.newCollection'),
    931 			Zotero.getString('pane.collections.name'), newName, "", {});
    932 		
    933 		if (!result)
    934 		{
    935 			return;
    936 		}
    937 		
    938 		if (!newName.value)
    939 		{
    940 			newName.value = untitled;
    941 		}
    942 		
    943 		var collection = new Zotero.Collection;
    944 		collection.libraryID = libraryID;
    945 		collection.name = newName.value;
    946 		collection.parentKey = parentKey;
    947 		return collection.saveTx();
    948 	});
    949 	
    950 	this.importFeedsFromOPML = Zotero.Promise.coroutine(function* (event) {
    951 		var nsIFilePicker = Components.interfaces.nsIFilePicker;
    952 		while (true) {
    953 			var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
    954 			fp.init(window, Zotero.getString('fileInterface.importOPML'), nsIFilePicker.modeOpen);
    955 			fp.appendFilter(Zotero.getString('fileInterface.OPMLFeedFilter'), '*.opml; *.xml');
    956 			fp.appendFilters(nsIFilePicker.filterAll);
    957 			if (fp.show() == nsIFilePicker.returnOK) {
    958 				var contents = yield Zotero.File.getContentsAsync(fp.file.path);
    959 				var success = yield Zotero.Feeds.importFromOPML(contents);
    960 				if (success) {
    961 					return true;
    962 				}
    963 				// Try again
    964 				Zotero.alert(window, Zotero.getString('general.error'), Zotero.getString('fileInterface.unsupportedFormat'));
    965 			} else {
    966 				return false;
    967 			}
    968 		}
    969 	});
    970 	
    971 	this.newFeedFromPage = Zotero.Promise.coroutine(function* (event) {
    972 		let data = {unsaved: true};
    973 		if (event) {
    974 			data.url = event.target.getAttribute('feed');
    975 		} else {
    976 			data.url = gBrowser.selectedBrowser.feeds[0].href;
    977 		}
    978 		window.openDialog('chrome://zotero/content/feedSettings.xul', 
    979 			null, 'centerscreen, modal', data);
    980 		if (!data.cancelled) {
    981 			let feed = new Zotero.Feed();
    982 			feed.url = data.url;
    983 			feed.name = data.title;
    984 			feed.refreshInterval = data.ttl;
    985 			feed.cleanupReadAfter = data.cleanupReadAfter;
    986 			feed.cleanupUnreadAfter = data.cleanupUnreadAfter;
    987 			yield feed.saveTx();
    988 			yield feed.updateFeed();
    989 		}
    990 	});
    991 	
    992 	this.newFeedFromURL = Zotero.Promise.coroutine(function* () {
    993 		let data = {};
    994 		window.openDialog('chrome://zotero/content/feedSettings.xul', 
    995 			null, 'centerscreen, modal', data);
    996 		if (!data.cancelled) {
    997 			let feed = new Zotero.Feed();
    998 			feed.url = data.url;
    999 			feed.name = data.title;
   1000 			feed.refreshInterval = data.ttl;
   1001 			feed.cleanupReadAfter = data.cleanupReadAfter;
   1002 			feed.cleanupUnreadAfter = data.cleanupUnreadAfter;
   1003 			yield feed.saveTx();
   1004 			yield feed.updateFeed();
   1005 		}
   1006 	});
   1007 	
   1008 	this.newGroup = function () {
   1009 		this.loadURI(Zotero.Groups.addGroupURL);
   1010 	}
   1011 	
   1012 	
   1013 	this.newSearch = Zotero.Promise.coroutine(function* () {
   1014 		if (Zotero.DB.inTransaction()) {
   1015 			yield Zotero.DB.waitForTransaction();
   1016 		}
   1017 		
   1018 		var s = new Zotero.Search();
   1019 		s.libraryID = this.getSelectedLibraryID();
   1020 		s.addCondition('title', 'contains', '');
   1021 		
   1022 		var untitled = Zotero.getString('pane.collections.untitled');
   1023 		untitled = yield Zotero.DB.getNextName(
   1024 			s.libraryID,
   1025 			'savedSearches',
   1026 			'savedSearchName',
   1027 			Zotero.getString('pane.collections.untitled')
   1028 		);
   1029 		var io = {dataIn: {search: s, name: untitled}, dataOut: null};
   1030 		window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
   1031 		if (!io.dataOut) {
   1032 			return false;
   1033 		}
   1034 		s.fromJSON(io.dataOut.json);
   1035 		yield s.saveTx();
   1036 		return s.id;
   1037 	});
   1038 	
   1039 	
   1040 	this.setVirtual = Zotero.Promise.coroutine(function* (libraryID, type, show) {
   1041 		switch (type) {
   1042 			case 'duplicates':
   1043 				var treeViewID = 'D' + libraryID;
   1044 				break;
   1045 			
   1046 			case 'unfiled':
   1047 				var treeViewID = 'U' + libraryID;
   1048 				break;
   1049 			
   1050 			default:
   1051 				throw new Error("Invalid virtual collection type '" + type + "'");
   1052 		}
   1053 		
   1054 		Zotero.Utilities.Internal.setVirtualCollectionStateForLibrary(libraryID, type, show);
   1055 		
   1056 		var cv = this.collectionsView;
   1057 		
   1058 		var promise = cv.waitForSelect();
   1059 		var selectedRow = cv.selection.currentIndex;
   1060 		
   1061 		yield cv.refresh();
   1062 		
   1063 		// Select new row
   1064 		if (show) {
   1065 			yield this.collectionsView.selectByID(treeViewID);
   1066 		}
   1067 		// Select next appropriate row after removal
   1068 		else {
   1069 			this.collectionsView.selectAfterRowRemoval(selectedRow);
   1070 		}
   1071 		
   1072 		this.collectionsView.selection.selectEventsSuppressed = false;
   1073 		
   1074 		return promise;
   1075 	});
   1076 	
   1077 	
   1078 	this.openLookupWindow = Zotero.Promise.coroutine(function* () {
   1079 		if (Zotero.DB.inTransaction()) {
   1080 			yield Zotero.DB.waitForTransaction();
   1081 		}
   1082 		
   1083 		if (!this.canEdit()) {
   1084 			this.displayCannotEditLibraryMessage();
   1085 			return;
   1086 		}
   1087 		
   1088 		window.openDialog('chrome://zotero/content/lookup.xul', 'zotero-lookup', 'chrome,modal');
   1089 	});
   1090 	
   1091 	
   1092 	this.openAdvancedSearchWindow = function () {
   1093 		var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
   1094 					.getService(Components.interfaces.nsIWindowMediator);
   1095 		var enumerator = wm.getEnumerator('zotero:search');
   1096 		while (enumerator.hasMoreElements()) {
   1097 			var win = enumerator.getNext();
   1098 		}
   1099 		
   1100 		if (win) {
   1101 			win.focus();
   1102 			return;
   1103 		}
   1104 		
   1105 		var s = new Zotero.Search();
   1106 		s.libraryID = this.getSelectedLibraryID();
   1107 		s.addCondition('title', 'contains', '');
   1108 		
   1109 		var io = {dataIn: {search: s}, dataOut: null};
   1110 		window.openDialog('chrome://zotero/content/advancedSearch.xul', '', 'chrome,dialog=no,centerscreen', io);
   1111 	};
   1112 	
   1113 	
   1114 	this.toggleTagSelector = Zotero.Promise.coroutine(function* () {
   1115 		var tagSelector = document.getElementById('zotero-tag-selector');
   1116 		
   1117 		var showing = tagSelector.getAttribute('collapsed') == 'true';
   1118 		tagSelector.setAttribute('collapsed', !showing);
   1119 		this.updateTagSelectorSize();
   1120 		
   1121 		// If showing, set scope to items in current view
   1122 		// and focus filter textbox
   1123 		if (showing) {
   1124 			yield this.setTagScope();
   1125 			tagSelector.focusTextbox();
   1126 		}
   1127 		// If hiding, clear selection
   1128 		else {
   1129 			tagSelector.uninit();
   1130 		}
   1131 	});
   1132 	
   1133 	
   1134 	this.updateTagSelectorSize = function () {
   1135 		//Zotero.debug('Updating tag selector size');
   1136 		var zoteroPane = document.getElementById('zotero-pane-stack');
   1137 		var splitter = document.getElementById('zotero-tags-splitter');
   1138 		var tagSelector = document.getElementById('zotero-tag-selector');
   1139 		
   1140 		// Nothing should be bigger than appcontent's height
   1141 		var max = document.getElementById('appcontent').boxObject.height
   1142 					- splitter.boxObject.height;
   1143 		
   1144 		// Shrink tag selector to appcontent's height
   1145 		var maxTS = max - COLLECTIONS_HEIGHT;
   1146 		if (parseInt(tagSelector.getAttribute("height")) > maxTS) {
   1147 			//Zotero.debug("Limiting tag selector height to appcontent");
   1148 			tagSelector.setAttribute('height', maxTS);
   1149 		}
   1150 		
   1151 		var height = tagSelector.boxObject.height;
   1152 		
   1153 		
   1154 		/*Zotero.debug("tagSelector.boxObject.height: " + tagSelector.boxObject.height);
   1155 		Zotero.debug("tagSelector.getAttribute('height'): " + tagSelector.getAttribute('height'));
   1156 		Zotero.debug("zoteroPane.boxObject.height: " + zoteroPane.boxObject.height);
   1157 		Zotero.debug("zoteroPane.getAttribute('height'): " + zoteroPane.getAttribute('height'));*/
   1158 		
   1159 		
   1160 		// Don't let the Z-pane jump back down to its previous height
   1161 		// (if shrinking or hiding the tag selector let it clear the min-height)
   1162 		if (zoteroPane.getAttribute('height') < zoteroPane.boxObject.height) {
   1163 			//Zotero.debug("Setting Zotero pane height attribute to " +  zoteroPane.boxObject.height);
   1164 			zoteroPane.setAttribute('height', zoteroPane.boxObject.height);
   1165 		}
   1166 		
   1167 		if (tagSelector.getAttribute('collapsed') == 'true') {
   1168 			// 32px is the default Z pane min-height in overlay.css
   1169 			height = 32;
   1170 		}
   1171 		else {
   1172 			// tS.boxObject.height doesn't exist at startup, so get from attribute
   1173 			if (!height) {
   1174 				height = parseInt(tagSelector.getAttribute('height'));
   1175 			}
   1176 			// 121px seems to be enough room for the toolbar and collections
   1177 			// tree at minimum height
   1178 			height = height + COLLECTIONS_HEIGHT;
   1179 		}
   1180 		
   1181 		//Zotero.debug('Setting Zotero pane minheight to ' + height);
   1182 		zoteroPane.setAttribute('minheight', height);
   1183 		
   1184 		if (this.isShowing() && !this.isFullScreen()) {
   1185 			zoteroPane.setAttribute('savedHeight', zoteroPane.boxObject.height);
   1186 		}
   1187 		
   1188 		// Fix bug whereby resizing the Z pane downward after resizing
   1189 		// the tag selector up and then down sometimes caused the Z pane to
   1190 		// stay at a fixed size and get pushed below the bottom
   1191 		tagSelector.height++;
   1192 		tagSelector.height--;
   1193 	}
   1194 	
   1195 	
   1196 	function getTagSelection() {
   1197 		var tagSelector = document.getElementById('zotero-tag-selector');
   1198 		return tagSelector.selection ? tagSelector.selection : new Set();
   1199 	}
   1200 	
   1201 	
   1202 	this.clearTagSelection = function () {
   1203 		document.getElementById('zotero-tag-selector').deselectAll();
   1204 	}
   1205 	
   1206 	
   1207 	/*
   1208 	 * Sets the tag filter on the items view
   1209 	 */
   1210 	this.updateTagFilter = Zotero.Promise.coroutine(function* () {
   1211 		if (this.itemsView) {
   1212 			yield this.itemsView.setFilter('tags', getTagSelection());
   1213 		}
   1214 	});
   1215 	
   1216 	
   1217 	this.tagSelectorShown = function () {
   1218 		var collectionTreeRow = this.getCollectionTreeRow();
   1219 		if (!collectionTreeRow) return;
   1220 		var tagSelector = document.getElementById('zotero-tag-selector');
   1221 		return !tagSelector.getAttribute('collapsed')
   1222 			|| tagSelector.getAttribute('collapsed') == 'false';
   1223 	};
   1224 	
   1225 	
   1226 	/*
   1227 	 * Set the tags scope to the items in the current view
   1228 	 *
   1229 	 * Passed to the items tree to trigger on changes
   1230 	 */
   1231 	this.setTagScope = Zotero.Promise.coroutine(function* () {
   1232 		var collectionTreeRow = this.getCollectionTreeRow();
   1233 		var tagSelector = document.getElementById('zotero-tag-selector');
   1234 		if (this.tagSelectorShown()) {
   1235 			Zotero.debug('Updating tag selector with current tags');
   1236 			if (collectionTreeRow.editable) {
   1237 				tagSelector.mode = 'edit';
   1238 			}
   1239 			else {
   1240 				tagSelector.mode = 'view';
   1241 			}
   1242 			tagSelector.collectionTreeRow = collectionTreeRow;
   1243 			tagSelector.updateScope = () => this.setTagScope();
   1244 			tagSelector.libraryID = collectionTreeRow.ref.libraryID;
   1245 			tagSelector.scope = yield collectionTreeRow.getChildTags();
   1246 		}
   1247 	});
   1248 	
   1249 	
   1250 	this.onCollectionSelected = function () {
   1251 		return Zotero.spawn(function* () {
   1252 			var collectionTreeRow = this.getCollectionTreeRow();
   1253 			if (!collectionTreeRow) {
   1254 				return;
   1255 			}
   1256 			
   1257 			if (this.itemsView && this.itemsView.collectionTreeRow.id == collectionTreeRow.id) {
   1258 				Zotero.debug("Collection selection hasn't changed");
   1259 				
   1260 				// Update toolbar, in case editability has changed
   1261 				this._updateToolbarIconsForRow(collectionTreeRow);
   1262 				return;
   1263 			}
   1264 			
   1265 			if (this.itemsView) {
   1266 				// Wait for existing items view to finish loading before unloading it
   1267 				//
   1268 				// TODO: Cancel loading
   1269 				let promise = this.itemsView.waitForLoad();
   1270 				if (promise.isPending()) {
   1271 					Zotero.debug("Waiting for items view " + this.itemsView.id + " to finish loading");
   1272 					yield promise;
   1273 				}
   1274 				
   1275 				this.itemsView.unregister();
   1276 				document.getElementById('zotero-items-tree').view = this.itemsView = null;
   1277 			}
   1278 			
   1279 			if (this.collectionsView.selection.count != 1) {
   1280 				return;
   1281 			}
   1282 			
   1283 			// Clear quick search and tag selector when switching views
   1284 			document.getElementById('zotero-tb-search').value = "";
   1285 			
   1286 			// XBL functions might not yet be available
   1287 			var tagSelector = document.getElementById('zotero-tag-selector');
   1288 			if (tagSelector.deselectAll) {
   1289 				tagSelector.deselectAll();
   1290 			}
   1291 			
   1292 			// Not necessary with seltype="cell", which calls nsITreeView::isSelectable()
   1293 			/*if (collectionTreeRow.isSeparator()) {
   1294 				document.getElementById('zotero-items-tree').view = this.itemsView = null;
   1295 				return;
   1296 			}*/
   1297 			
   1298 			collectionTreeRow.setSearch('');
   1299 			collectionTreeRow.setTags(getTagSelection());
   1300 			
   1301 			this._updateToolbarIconsForRow(collectionTreeRow);
   1302 			
   1303 			this.itemsView = new Zotero.ItemTreeView(collectionTreeRow);
   1304 			if (collectionTreeRow.isPublications()) {
   1305 				this.itemsView.collapseAll = true;
   1306 			}
   1307 			this.itemsView.onError = function () {
   1308 				// Don't reload last folder, in case that's the problem
   1309 				Zotero.Prefs.clear('lastViewedFolder');
   1310 				ZoteroPane_Local.displayErrorMessage();
   1311 			};
   1312 			this.itemsView.onRefresh.addListener(() => this.setTagScope());
   1313 			if (this.tagSelectorShown()) {
   1314 				let tagSelector = document.getElementById('zotero-tag-selector')
   1315 				let handler = function () {
   1316 					tagSelector.removeEventListener('refresh', handler);
   1317 					Zotero.uiIsReady();
   1318 				};
   1319 				tagSelector.addEventListener('refresh', handler);
   1320 			}
   1321 			else {
   1322 				this.itemsView.onLoad.addListener(() => Zotero.uiIsReady());
   1323 			}
   1324 			
   1325 			// If item data not yet loaded for library, load it now.
   1326 			// Other data types are loaded at startup
   1327 			var library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID);
   1328 			if (!library.getDataLoaded('item')) {
   1329 				Zotero.debug("Waiting for items to load for library " + library.libraryID);
   1330 				ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
   1331 				yield library.waitForDataLoad('item');
   1332 			}
   1333 			
   1334 			document.getElementById('zotero-items-tree').view = this.itemsView;
   1335 			
   1336 			try {
   1337 				let tree = document.getElementById('zotero-items-tree');
   1338 				let treecols = document.getElementById('zotero-items-columns-header');
   1339 				let treecolpicker = treecols.boxObject.firstChild.nextSibling;
   1340 				let menupopup = treecolpicker.boxObject.firstChild.nextSibling;
   1341 				// Add events to treecolpicker to update menu before showing/hiding
   1342 				let attr = menupopup.getAttribute('onpopupshowing');
   1343 				if (attr.indexOf('Zotero') == -1) {
   1344 					menupopup.setAttribute('onpopupshowing', 'ZoteroPane.itemsView.onColumnPickerShowing(event); '
   1345 						// Keep whatever else is there
   1346 						+ attr);
   1347 					menupopup.setAttribute('onpopuphidden', 'ZoteroPane.itemsView.onColumnPickerHidden(event); '
   1348 						// Keep whatever else is there
   1349 						+ menupopup.getAttribute('onpopuphidden'));
   1350 				}
   1351 				
   1352 				// Items view column visibility for different groups
   1353 				let prevViewGroup = tree.getAttribute('current-view-group');
   1354 				let curViewGroup = collectionTreeRow.visibilityGroup;
   1355 				tree.setAttribute('current-view-group', curViewGroup);
   1356 				if (curViewGroup != prevViewGroup) {
   1357 					let cols = Array.from(treecols.getElementsByTagName('treecol'));
   1358 					let settings = JSON.parse(Zotero.Prefs.get('itemsView.columnVisibility') || '{}');
   1359 					if (prevViewGroup) {
   1360 						// Store previous view settings
   1361 						let setting = {};
   1362 						for (let col of cols) {
   1363 							let colType = col.id.substring('zotero-items-column-'.length);
   1364 							setting[colType] = col.getAttribute('hidden') == 'true' ? 0 : 1
   1365 						}
   1366 						settings[prevViewGroup] = setting;
   1367 						Zotero.Prefs.set('itemsView.columnVisibility', JSON.stringify(settings));
   1368 					}
   1369 					
   1370 					// Recover current view settings
   1371 					if (settings[curViewGroup]) {
   1372 						for (let col of cols) {
   1373 							let colType = col.id.substring('zotero-items-column-'.length);
   1374 							col.setAttribute('hidden', !settings[curViewGroup][colType]);
   1375 						}
   1376 					} else {
   1377 						cols.forEach((col) => {
   1378 							col.setAttribute('hidden', !(col.hasAttribute('default-in') &&
   1379 									col.getAttribute('default-in').split(' ').indexOf(curViewGroup) != -1)
   1380 							)
   1381 						})
   1382 					}
   1383 				}
   1384 			}
   1385 			catch (e) {
   1386 				Zotero.debug(e);
   1387 			}
   1388 			
   1389 			Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id);
   1390 		}, this)
   1391 		.finally(function () {
   1392 			return this.collectionsView.runListeners('select');
   1393 		}.bind(this));
   1394 	};
   1395 	
   1396 	
   1397 	/**
   1398 	 * Enable or disable toolbar icons and menu options as necessary
   1399 	 */
   1400 	this._updateToolbarIconsForRow = function (collectionTreeRow) {
   1401 		const disableIfNoEdit = [
   1402 			"cmd_zotero_newCollection",
   1403 			"cmd_zotero_newSavedSearch",
   1404 			"zotero-tb-add",
   1405 			"cmd_zotero_newItemFromCurrentPage",
   1406 			"zotero-tb-lookup",
   1407 			"cmd_zotero_newStandaloneNote",
   1408 			"zotero-tb-note-add",
   1409 			"zotero-tb-attachment-add"
   1410 		];
   1411 		for (let i = 0; i < disableIfNoEdit.length; i++) {
   1412 			let command = disableIfNoEdit[i];
   1413 			let el = document.getElementById(command);
   1414 			
   1415 			// If a trash is selected, new collection depends on the
   1416 			// editability of the library
   1417 			if (collectionTreeRow.isTrash() && command == 'cmd_zotero_newCollection') {
   1418 				var overrideEditable = Zotero.Libraries.get(collectionTreeRow.ref.libraryID).editable;
   1419 			}
   1420 			else {
   1421 				var overrideEditable = false;
   1422 			}
   1423 			
   1424 			// Don't allow normal buttons in My Publications, because things need to
   1425 			// be dragged and go through the wizard
   1426 			let forceDisable = collectionTreeRow.isPublications() && command != 'zotero-tb-note-add';
   1427 			
   1428 			if ((collectionTreeRow.editable || overrideEditable) && !forceDisable) {
   1429 				if(el.hasAttribute("disabled")) el.removeAttribute("disabled");
   1430 			} else {
   1431 				el.setAttribute("disabled", "true");
   1432 			}
   1433 		}
   1434 	};
   1435 	
   1436 	
   1437 	this.getCollectionTreeRow = function () {
   1438 		if (!this.collectionsView || !this.collectionsView.selection.count) {
   1439 			return false;
   1440 		}
   1441 		return this.collectionsView.getRow(this.collectionsView.selection.currentIndex);
   1442 	}
   1443 	
   1444 	
   1445 	/**
   1446 	 * @return {Promise<Boolean>} - Promise that resolves to true if an item was selected,
   1447 	 *                              or false if not (used for tests, though there could possibly
   1448 	 *                              be a better test for whether the item pane changed)
   1449 	 */
   1450 	this.itemSelected = function (event) {
   1451 		return Zotero.Promise.coroutine(function* () {
   1452 			// Don't select item until items list has loaded
   1453 			//
   1454 			// This avoids an error if New Item is used while the pane is first loading.
   1455 			var promise = this.itemsView.waitForLoad();
   1456 			if (promise.isPending()) {
   1457 				yield promise;
   1458 			}
   1459 			
   1460 			if (!this.itemsView || !this.itemsView.selection) {
   1461 				Zotero.debug("Items view not available in itemSelected", 2);
   1462 				return false;
   1463 			}
   1464 			
   1465 			var selectedItems = this.itemsView.getSelectedItems();
   1466 			
   1467 			// Display buttons at top of item pane depending on context. This needs to run even if the
   1468 			// selection hasn't changed, because the selected items might have been modified.
   1469 			this.updateItemPaneButtons(selectedItems);
   1470 			
   1471 			this.updateQuickCopyCommands(selectedItems);
   1472 			
   1473 			// Check if selection has actually changed. The onselect event that calls this
   1474 			// can be called in various situations where the selection didn't actually change,
   1475 			// such as whenever selectEventsSuppressed is set to false.
   1476 			var ids = selectedItems.map(item => item.id);
   1477 			ids.sort();
   1478 			if (ids.length && Zotero.Utilities.arrayEquals(_lastSelectedItems, ids)) {
   1479 				return false;
   1480 			}
   1481 			_lastSelectedItems = ids;
   1482 			
   1483 			var tabs = document.getElementById('zotero-view-tabbox');
   1484 			
   1485 			// save note when switching from a note
   1486 			if(document.getElementById('zotero-item-pane-content').selectedIndex == 2) {
   1487 				// TODO: only try to save when selected item is different
   1488 				yield document.getElementById('zotero-note-editor').save();
   1489 			}
   1490 			
   1491 			var collectionTreeRow = this.getCollectionTreeRow();
   1492 			// I don't think this happens in normal usage, but it can happen during tests
   1493 			if (!collectionTreeRow) {
   1494 				return false;
   1495 			}
   1496 			
   1497 			// Single item selected
   1498 			if (selectedItems.length == 1) {
   1499 				var item = selectedItems[0];
   1500 				
   1501 				if (item.isNote()) {
   1502 					ZoteroItemPane.onNoteSelected(item, this.collectionsView.editable);
   1503 				}
   1504 				
   1505 				else if (item.isAttachment()) {
   1506 					var attachmentBox = document.getElementById('zotero-attachment-box');
   1507 					attachmentBox.mode = this.collectionsView.editable ? 'edit' : 'view';
   1508 					attachmentBox.item = item;
   1509 					
   1510 					document.getElementById('zotero-item-pane-content').selectedIndex = 3;
   1511 				}
   1512 				
   1513 				// Regular item
   1514 				else {
   1515 					var isCommons = collectionTreeRow.isBucket();
   1516 					
   1517 					document.getElementById('zotero-item-pane-content').selectedIndex = 1;
   1518 					var tabBox = document.getElementById('zotero-view-tabbox');
   1519 					
   1520 					// Reset tab when viewing a feed item, which only has the info tab
   1521 					if (item.isFeedItem) {
   1522 						tabBox.selectedIndex = 0;
   1523 					}
   1524 					
   1525 					var pane = tabBox.selectedIndex;
   1526 					tabBox.firstChild.hidden = isCommons;
   1527 					
   1528 					var button = document.getElementById('zotero-item-show-original');
   1529 					if (isCommons) {
   1530 						button.hidden = false;
   1531 						button.disabled = !this.getOriginalItem();
   1532 					}
   1533 					else {
   1534 						button.hidden = true;
   1535 					}
   1536 					
   1537 					if (this.collectionsView.editable) {
   1538 						yield ZoteroItemPane.viewItem(item, null, pane);
   1539 						tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex;
   1540 					}
   1541 					else {
   1542 						yield ZoteroItemPane.viewItem(item, 'view', pane);
   1543 						tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex;
   1544 					}
   1545 					
   1546 					if (item.isFeedItem) {
   1547 						// Too slow for now
   1548 						// if (!item.isTranslated) {
   1549 						// 	item.translate();
   1550 						// }
   1551 						this.updateReadLabel();
   1552 						this.startItemReadTimeout(item.id);
   1553 					}
   1554 				}
   1555 			}
   1556 			// Zero or multiple items selected
   1557 			else {
   1558 				if (collectionTreeRow.isFeed()) {
   1559 					this.updateReadLabel();
   1560 				}
   1561 				
   1562 				let count = selectedItems.length;
   1563 				
   1564 				// Display duplicates merge interface in item pane
   1565 				if (collectionTreeRow.isDuplicates()) {
   1566 					if (!collectionTreeRow.editable) {
   1567 						if (count) {
   1568 							var msg = Zotero.getString('pane.item.duplicates.writeAccessRequired');
   1569 						}
   1570 						else {
   1571 							var msg = Zotero.getString('pane.item.selected.zero');
   1572 						}
   1573 						this.setItemPaneMessage(msg);
   1574 					}
   1575 					else if (count) {
   1576 						document.getElementById('zotero-item-pane-content').selectedIndex = 4;
   1577 						
   1578 						// Load duplicates UI code
   1579 						if (typeof Zotero_Duplicates_Pane == 'undefined') {
   1580 							Zotero.debug("Loading duplicatesMerge.js");
   1581 							Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
   1582 								.getService(Components.interfaces.mozIJSSubScriptLoader)
   1583 								.loadSubScript("chrome://zotero/content/duplicatesMerge.js");
   1584 						}
   1585 						
   1586 						// On a Select All of more than a few items, display a row
   1587 						// count instead of the usual item type mismatch error
   1588 						var displayNumItemsOnTypeError = count > 5 && count == this.itemsView.rowCount;
   1589 						
   1590 						// Initialize the merge pane with the selected items
   1591 						Zotero_Duplicates_Pane.setItems(selectedItems, displayNumItemsOnTypeError);
   1592 					}
   1593 					else {
   1594 						var msg = Zotero.getString('pane.item.duplicates.selectToMerge');
   1595 						this.setItemPaneMessage(msg);
   1596 					}
   1597 				}
   1598 				// Display label in the middle of the item pane
   1599 				else {
   1600 					if (count) {
   1601 						var msg = Zotero.getString('pane.item.selected.multiple', count);
   1602 					}
   1603 					else {
   1604 						var rowCount = this.itemsView.rowCount;
   1605 						var str = 'pane.item.unselected.';
   1606 						switch (rowCount){
   1607 							case 0:
   1608 								str += 'zero';
   1609 								break;
   1610 							case 1:
   1611 								str += 'singular';
   1612 								break;
   1613 							default:
   1614 								str += 'plural';
   1615 								break;
   1616 						}
   1617 						var msg = Zotero.getString(str, [rowCount]);
   1618 					}
   1619 					
   1620 					this.setItemPaneMessage(msg);
   1621 					
   1622 					return false;
   1623 				}
   1624 			}
   1625 			
   1626 			return true;
   1627 		}.bind(this))()
   1628 		.catch(function (e) {
   1629 			this.displayErrorMessage();
   1630 			throw e;
   1631 		}.bind(this))
   1632 		.finally(function () {
   1633 			return this.itemsView.runListeners('select');
   1634 		}.bind(this));
   1635 	}
   1636 	
   1637 	
   1638 	/**
   1639 	 * Display buttons at top of item pane depending on context
   1640 	 *
   1641 	 * @param {Zotero.Item[]}
   1642 	 */
   1643 	this.updateItemPaneButtons = function (selectedItems) {
   1644 		if (!selectedItems.length) {
   1645 			document.querySelectorAll('.zotero-item-pane-top-buttons').forEach(x => x.hidden = true);
   1646 			return;
   1647 		}
   1648 		
   1649 		// My Publications buttons
   1650 		var isPublications = this.getCollectionTreeRow().isPublications();
   1651 		// Show in My Publications view if selected items are all notes or non-linked-file attachments
   1652 		var showMyPublicationsButtons = isPublications
   1653 			&& selectedItems.every((item) => {
   1654 				return item.isNote()
   1655 					|| (item.isAttachment()
   1656 						&& item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE);
   1657 			});
   1658 		var myPublicationsButtons = document.getElementById('zotero-item-pane-top-buttons-my-publications');
   1659 		myPublicationsButtons.hidden = !showMyPublicationsButtons;
   1660 		if (showMyPublicationsButtons) {
   1661 			let button = myPublicationsButtons.firstChild;
   1662 			let hiddenItemsSelected = selectedItems.some(item => !item.inPublications);
   1663 			let str, onclick;
   1664 			if (hiddenItemsSelected) {
   1665 				str = 'showInMyPublications';
   1666 				onclick = () => Zotero.Items.addToPublications(selectedItems);
   1667 			}
   1668 			else {
   1669 				str = 'hideFromMyPublications';
   1670 				onclick = () => Zotero.Items.removeFromPublications(selectedItems);
   1671 			}
   1672 			button.label = Zotero.getString('pane.item.' + str);
   1673 			button.onclick = onclick;
   1674 		}
   1675 		
   1676 		// Trash button
   1677 		let nonDeletedItemsSelected = selectedItems.some(item => !item.deleted);
   1678 		document.getElementById('zotero-item-pane-top-buttons-trash').hidden
   1679 			= !this.getCollectionTreeRow().isTrash() || nonDeletedItemsSelected;
   1680 		
   1681 		// Feed buttons
   1682 		document.getElementById('zotero-item-pane-top-buttons-feed').hidden
   1683 			= !this.getCollectionTreeRow().isFeed()
   1684 	};
   1685 	
   1686 	
   1687 	/**
   1688 	 * @return {Promise}
   1689 	 */
   1690 	this.updateNoteButtonMenu = function () {
   1691 		var items = ZoteroPane_Local.getSelectedItems();
   1692 		var cmd = document.getElementById('cmd_zotero_newChildNote');
   1693 		cmd.setAttribute("disabled", !this.canEdit() ||
   1694 			!(items.length == 1 && (items[0].isRegularItem() || !items[0].isTopLevelItem())));
   1695 	}
   1696 	
   1697 	
   1698 	this.updateAttachmentButtonMenu = function (popup) {
   1699 		var items = ZoteroPane_Local.getSelectedItems();
   1700 		
   1701 		var disabled = !this.canEdit() || !(items.length == 1 && items[0].isRegularItem());
   1702 		
   1703 		if (disabled) {
   1704 			for (let node of popup.childNodes) {
   1705 				node.disabled = true;
   1706 			}
   1707 			return;
   1708 		}
   1709 		
   1710 		var collectionTreeRow = this.collectionsView.selectedTreeRow;
   1711 		var canEditFiles = this.canEditFiles();
   1712 		
   1713 		var prefix = "menuitem-iconic zotero-menuitem-attachments-";
   1714 		
   1715 		for (var i=0; i<popup.childNodes.length; i++) {
   1716 			var node = popup.childNodes[i];
   1717 			var className = node.className.replace('standalone-no-display', '').trim();
   1718 			
   1719 			switch (className) {
   1720 				case prefix + 'link':
   1721 					node.disabled = collectionTreeRow.isWithinGroup();
   1722 					break;
   1723 				
   1724 				case prefix + 'snapshot':
   1725 				case prefix + 'file':
   1726 					node.disabled = !canEditFiles;
   1727 					break;
   1728 				
   1729 				case prefix + 'web-link':
   1730 					node.disabled = false;
   1731 					break;
   1732 				
   1733 				default:
   1734 					throw ("Invalid class name '" + className + "' in ZoteroPane_Local.updateAttachmentButtonMenu()");
   1735 			}
   1736 		}
   1737 	}
   1738 	
   1739 	
   1740 	/**
   1741 	 * Update the <command> elements that control the shortcut keys and the enabled state of the
   1742 	 * "Copy Citation"/"Copy Bibliography"/"Copy as" menu options. When disabled, the shortcuts are
   1743 	 * still caught in handleKeyPress so that we can show an alert about not having references selected.
   1744 	 */
   1745 	this.updateQuickCopyCommands = function (selectedItems) {
   1746 		var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
   1747 		format = Zotero.QuickCopy.unserializeSetting(format);
   1748 		if (format.mode == 'bibliography') {
   1749 			var canCopy = selectedItems.some(item => item.isRegularItem());
   1750 		}
   1751 		else {
   1752 			var canCopy = true;
   1753 		}
   1754 		document.getElementById('cmd_zotero_copyCitation').setAttribute('disabled', !canCopy);
   1755 		document.getElementById('cmd_zotero_copyBibliography').setAttribute('disabled', !canCopy);
   1756 	};
   1757 	
   1758 	
   1759 	/**
   1760 	 * @return {Promise}
   1761 	 */
   1762 	this.reindexItem = Zotero.Promise.coroutine(function* () {
   1763 		var items = this.getSelectedItems();
   1764 		if (!items) {
   1765 			return;
   1766 		}
   1767 		
   1768 		var itemIDs = [];
   1769 
   1770 		for (var i=0; i<items.length; i++) {
   1771 			itemIDs.push(items[i].id);
   1772 		}
   1773 		
   1774 		yield Zotero.Fulltext.indexItems(itemIDs, true);
   1775 		yield document.getElementById('zotero-attachment-box').updateItemIndexedState();
   1776 	});
   1777 	
   1778 	
   1779 	/**
   1780 	 * @return {Promise<Zotero.Item>} - The new Zotero.Item
   1781 	 */
   1782 	this.duplicateSelectedItem = Zotero.Promise.coroutine(function* () {
   1783 		var self = this;
   1784 		if (!self.canEdit()) {
   1785 			self.displayCannotEditLibraryMessage();
   1786 			return;
   1787 		}
   1788 		
   1789 		var item = self.getSelectedItems()[0];
   1790 		var newItem;
   1791 		
   1792 		yield Zotero.DB.executeTransaction(function* () {
   1793 			newItem = item.clone();
   1794 			// If in a collection, add new item to it
   1795 			if (self.collectionsView.selectedTreeRow.isCollection() && newItem.isTopLevelItem()) {
   1796 				newItem.setCollections([self.collectionsView.selectedTreeRow.ref.id]);
   1797 			}
   1798 			yield newItem.save();
   1799 			for (let relItemKey of item.relatedItems) {
   1800 				try {
   1801 					let relItem = yield Zotero.Items.getByLibraryAndKeyAsync(item.libraryID, relItemKey);
   1802 					if (relItem.addRelatedItem(newItem)) {
   1803 						yield relItem.save({
   1804 							skipDateModifiedUpdate: true
   1805 						});
   1806 					}
   1807 				}
   1808 				catch (e) {
   1809 					Zotero.logError(e);
   1810 				}
   1811 			}
   1812 		});
   1813 		
   1814 		yield self.selectItem(newItem.id);
   1815 		
   1816 		return newItem;
   1817 	});
   1818 	
   1819 	
   1820 	this.deleteSelectedItem = function () {
   1821 		Zotero.debug("ZoteroPane_Local.deleteSelectedItem() is deprecated -- use ZoteroPane_Local.deleteSelectedItems()");
   1822 		this.deleteSelectedItems();
   1823 	}
   1824 	
   1825 	/*
   1826 	 * Remove, trash, or delete item(s), depending on context
   1827 	 *
   1828 	 * @param  {Boolean}  [force=false]     Trash or delete even if in a collection or search,
   1829 	 *                                      or trash without prompt in library
   1830 	 * @param  {Boolean}  [fromMenu=false]  If triggered from context menu, which always prompts for deletes
   1831 	 */
   1832 	this.deleteSelectedItems = function (force, fromMenu) {
   1833 		if (!this.itemsView || !this.itemsView.selection.count) {
   1834 			return;
   1835 		}
   1836 		var collectionTreeRow = this.collectionsView.selectedTreeRow;
   1837 		
   1838 		if (!collectionTreeRow.isTrash() && !collectionTreeRow.isBucket() && !this.canEdit()) {
   1839 			this.displayCannotEditLibraryMessage();
   1840 			return;
   1841 		}
   1842 		
   1843 		var toTrash = {
   1844 			title: Zotero.getString('pane.items.trash.title'),
   1845 			text: Zotero.getString(
   1846 				'pane.items.trash' + (this.itemsView.selection.count > 1 ? '.multiple' : '')
   1847 			)
   1848 		};
   1849 		var toDelete = {
   1850 			title: Zotero.getString('pane.items.delete.title'),
   1851 			text: Zotero.getString(
   1852 				'pane.items.delete' + (this.itemsView.selection.count > 1 ? '.multiple' : '')
   1853 			)
   1854 		};
   1855 		var toRemove = {
   1856 			title: Zotero.getString('pane.items.remove.title'),
   1857 			text: Zotero.getString(
   1858 				'pane.items.remove' + (this.itemsView.selection.count > 1 ? '.multiple' : '')
   1859 			)
   1860 		};
   1861 		
   1862 		if (collectionTreeRow.isPublications()) {
   1863 			let toRemoveFromPublications = {
   1864 				title: Zotero.getString('pane.items.removeFromPublications.title'),
   1865 				text: Zotero.getString(
   1866 					'pane.items.removeFromPublications' + (this.itemsView.selection.count > 1 ? '.multiple' : '')
   1867 				)
   1868 			};
   1869 			var prompt = force ? toTrash : toRemoveFromPublications;
   1870 		}
   1871 		else if (collectionTreeRow.isLibrary(true)) {
   1872 			// In library, don't prompt if meta key was pressed
   1873 			var prompt = (force && !fromMenu) ? false : toTrash;
   1874 		}
   1875 		else if (collectionTreeRow.isCollection()) {
   1876 			
   1877 			// Ignore unmodified action if only child items are selected
   1878 			if (!force && this.itemsView.getSelectedItems().every(item => !item.isTopLevelItem())) {
   1879 				return;
   1880 			}
   1881 			
   1882 			var prompt = force ? toTrash : toRemove;
   1883 		}
   1884 		else if (collectionTreeRow.isSearch() || collectionTreeRow.isUnfiled() || collectionTreeRow.isDuplicates()) {
   1885 			if (!force) {
   1886 				return;
   1887 			}
   1888 			var prompt = toTrash;
   1889 		}
   1890 		// Do nothing in trash view if any non-deleted items are selected
   1891 		else if (collectionTreeRow.isTrash()) {
   1892 			var start = {};
   1893 			var end = {};
   1894 			for (var i=0, len=this.itemsView.selection.getRangeCount(); i<len; i++) {
   1895 				this.itemsView.selection.getRangeAt(i, start, end);
   1896 				for (var j=start.value; j<=end.value; j++) {
   1897 					if (!this.itemsView.getRow(j).ref.deleted) {
   1898 						return;
   1899 					}
   1900 				}
   1901 			}
   1902 			var prompt = toDelete;
   1903 		}
   1904 		else if (collectionTreeRow.isBucket()) {
   1905 			var prompt = toDelete;
   1906 		}
   1907 		// Do nothing in share views
   1908 		else if (collectionTreeRow.isShare()) {
   1909 			return;
   1910 		}
   1911 		
   1912 		var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   1913 										.getService(Components.interfaces.nsIPromptService);
   1914 		if (!prompt || promptService.confirm(window, prompt.title, prompt.text)) {
   1915 			this.itemsView.deleteSelection(force);
   1916 		}
   1917 	}
   1918 	
   1919 	
   1920 	this.mergeSelectedItems = function () {
   1921 		if (!this.canEdit()) {
   1922 			this.displayCannotEditLibraryMessage();
   1923 			return;
   1924 		}
   1925 		
   1926 		document.getElementById('zotero-item-pane-content').selectedIndex = 4;
   1927 		
   1928 		if (typeof Zotero_Duplicates_Pane == 'undefined') {
   1929 			Zotero.debug("Loading duplicatesMerge.js");
   1930 			Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
   1931 				.getService(Components.interfaces.mozIJSSubScriptLoader)
   1932 				.loadSubScript("chrome://zotero/content/duplicatesMerge.js");
   1933 		}
   1934 		
   1935 		// Initialize the merge pane with the selected items
   1936 		Zotero_Duplicates_Pane.setItems(this.getSelectedItems());
   1937 	}
   1938 	
   1939 	
   1940 	this.deleteSelectedCollection = function (deleteItems) {
   1941 		var collectionTreeRow = this.getCollectionTreeRow();
   1942 		
   1943 		// Don't allow deleting libraries
   1944 		if (collectionTreeRow.isLibrary(true) && !collectionTreeRow.isFeed()) {
   1945 			return;
   1946 		}
   1947 		
   1948 		// Remove virtual duplicates collection
   1949 		if (collectionTreeRow.isDuplicates()) {
   1950 			this.setVirtual(collectionTreeRow.ref.libraryID, 'duplicates', false);
   1951 			return;
   1952 		}
   1953 		// Remove virtual unfiled collection
   1954 		else if (collectionTreeRow.isUnfiled()) {
   1955 			this.setVirtual(collectionTreeRow.ref.libraryID, 'unfiled', false);
   1956 			return;
   1957 		}
   1958 		
   1959 		if (!this.canEdit() && !collectionTreeRow.isFeed()) {
   1960 			this.displayCannotEditLibraryMessage();
   1961 			return;
   1962 		}
   1963 		
   1964 		
   1965 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   1966 			.getService(Components.interfaces.nsIPromptService);
   1967 		buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
   1968 			+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
   1969 		if (this.collectionsView.selection.count == 1) {
   1970 			var title, message;
   1971 			// Work out the required title and message
   1972 			if (collectionTreeRow.isCollection()) {
   1973 				if (deleteItems) {
   1974 					title = Zotero.getString('pane.collections.deleteWithItems.title');
   1975 					message = Zotero.getString('pane.collections.deleteWithItems');
   1976 				}
   1977 				else {
   1978 					title = Zotero.getString('pane.collections.delete.title');
   1979 					message = Zotero.getString('pane.collections.delete')
   1980 							+ "\n\n"
   1981 							+ Zotero.getString('pane.collections.delete.keepItems');
   1982 				}
   1983 			}
   1984 			else if (collectionTreeRow.isFeed()) {
   1985 				title = Zotero.getString('pane.feed.deleteWithItems.title');
   1986 				message = Zotero.getString('pane.feed.deleteWithItems');
   1987 			}
   1988 			else if (collectionTreeRow.isSearch()) {
   1989 				title = Zotero.getString('pane.collections.deleteSearch.title');
   1990 				message = Zotero.getString('pane.collections.deleteSearch');
   1991 			}
   1992 			
   1993 			// Display prompt
   1994 			var index = ps.confirmEx(
   1995 				null,
   1996 				title,
   1997 				message,
   1998 				buttonFlags,
   1999 				title,
   2000 				"", "", "", {}
   2001 			);
   2002 			if (index == 0) {
   2003 				return this.collectionsView.deleteSelection(deleteItems);
   2004 			}
   2005 		}
   2006 	}
   2007 	
   2008 	
   2009 	// Currently used only for Commons to find original linked item
   2010 	this.getOriginalItem = function () {
   2011 		var item = this.getSelectedItems()[0];
   2012 		var collectionTreeRow = this.getCollectionTreeRow();
   2013 		// TEMP: Commons buckets only
   2014 		return collectionTreeRow.ref.getLocalItem(item);
   2015 	}
   2016 	
   2017 	
   2018 	this.showOriginalItem = function () {
   2019 		var item = this.getOriginalItem();
   2020 		if (!item) {
   2021 			Zotero.debug("Original item not found");
   2022 			return;
   2023 		}
   2024 		this.selectItem(item.id).done();
   2025 	}
   2026 	
   2027 	
   2028 	/**
   2029 	 * @return {Promise}
   2030 	 */
   2031 	this.restoreSelectedItems = Zotero.Promise.coroutine(function* () {
   2032 		var items = this.getSelectedItems();
   2033 		if (!items) {
   2034 			return;
   2035 		}
   2036 		
   2037 		yield Zotero.DB.executeTransaction(function* () {
   2038 			for (let i=0; i<items.length; i++) {
   2039 				items[i].deleted = false;
   2040 				yield items[i].save({
   2041 					skipDateModifiedUpdate: true
   2042 				});
   2043 			}
   2044 		}.bind(this));
   2045 	});
   2046 	
   2047 	
   2048 	/**
   2049 	 * @return {Promise}
   2050 	 */
   2051 	this.emptyTrash = Zotero.Promise.coroutine(function* () {
   2052 		var libraryID = this.getSelectedLibraryID();
   2053 		
   2054 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   2055 								.getService(Components.interfaces.nsIPromptService);
   2056 		
   2057 		var result = ps.confirm(
   2058 			null,
   2059 			"",
   2060 			Zotero.getString('pane.collections.emptyTrash') + "\n\n"
   2061 				+ Zotero.getString('general.actionCannotBeUndone')
   2062 		);
   2063 		if (result) {
   2064 			Zotero.showZoteroPaneProgressMeter(null, true);
   2065 			try {
   2066 				let deleted = yield Zotero.Items.emptyTrash(
   2067 					libraryID,
   2068 					{
   2069 						onProgress: (progress, progressMax) => {
   2070 							var percentage = Math.round((progress / progressMax) * 100);
   2071 							Zotero.updateZoteroPaneProgressMeter(percentage);
   2072 						}
   2073 					}
   2074 				);
   2075 			}
   2076 			finally {
   2077 				Zotero.hideZoteroPaneOverlays();
   2078 			}
   2079 			yield Zotero.purgeDataObjects();
   2080 		}
   2081 	});
   2082 	
   2083 	
   2084 	this.editSelectedCollection = Zotero.Promise.coroutine(function* () {
   2085 		if (!this.canEdit()) {
   2086 			this.displayCannotEditLibraryMessage();
   2087 			return;
   2088 		}
   2089 		
   2090 		if (this.collectionsView.selection.count > 0) {
   2091 			var row = this.collectionsView.selectedTreeRow;
   2092 			
   2093 			if (row.isCollection()) {
   2094 				var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   2095 										.getService(Components.interfaces.nsIPromptService);
   2096 				
   2097 				var newName = { value: row.getName() };
   2098 				var result = promptService.prompt(window, "",
   2099 					Zotero.getString('pane.collections.rename'), newName, "", {});
   2100 				
   2101 				if (result && newName.value) {
   2102 					row.ref.name = newName.value;
   2103 					row.ref.saveTx();
   2104 				}
   2105 			}
   2106 			else {
   2107 				let s = row.ref.clone();
   2108 				let groups = [];
   2109 				// Promises don't work in the modal dialog, so get the group name here, if
   2110 				// applicable, and pass it in. We only need the group that this search belongs
   2111 				// to, if any, since the library drop-down is disabled for saved searches.
   2112 				if (Zotero.Libraries.get(s.libraryID).libraryType == 'group') {
   2113 					groups.push(Zotero.Groups.getByLibraryID(s.libraryID));
   2114 				}
   2115 				var io = {
   2116 					dataIn: {
   2117 						search: s,
   2118 						name: row.getName(),
   2119 						groups: groups
   2120 					},
   2121 					dataOut: null
   2122 				};
   2123 				window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
   2124 				if (io.dataOut) {
   2125 					row.ref.fromJSON(io.dataOut.json);
   2126 					yield row.ref.saveTx();
   2127 				}
   2128 			}
   2129 		}
   2130 	});
   2131 
   2132 	this.toggleSelectedItemsRead = Zotero.Promise.coroutine(function* () {
   2133 		yield Zotero.FeedItems.toggleReadByID(this.getSelectedItems(true));
   2134 	});
   2135 
   2136 	this.markFeedRead = Zotero.Promise.coroutine(function* () {
   2137 		if (!this.collectionsView.selection.count) return;
   2138 
   2139 		let feed = this.collectionsView.selectedTreeRow.ref;
   2140 		let feedItemIDs = yield Zotero.FeedItems.getAll(feed.libraryID, true, false, true);
   2141 		yield Zotero.FeedItems.toggleReadByID(feedItemIDs, true);
   2142 	});
   2143 
   2144 	
   2145 	this.editSelectedFeed = Zotero.Promise.coroutine(function* () {
   2146 		if (!this.collectionsView.selection.count) return;
   2147 		
   2148 		let feed = this.collectionsView.selectedTreeRow.ref;
   2149 		let data = {
   2150 			url: feed.url,
   2151 			title: feed.name,
   2152 			ttl: feed.refreshInterval,
   2153 			cleanupReadAfter: feed.cleanupReadAfter,
   2154 			cleanupUnreadAfter: feed.cleanupUnreadAfter
   2155 		};
   2156 		
   2157 		window.openDialog('chrome://zotero/content/feedSettings.xul', 
   2158 			null, 'centerscreen, modal', data);
   2159 		if (data.cancelled) return;
   2160 		
   2161 		feed.name = data.title;
   2162 		feed.refreshInterval = data.ttl;
   2163 		feed.cleanupReadAfter = data.cleanupReadAfter;
   2164 		feed.cleanupUnreadAfter = data.cleanupUnreadAfter;
   2165 		yield feed.saveTx();
   2166 	});
   2167 	
   2168 	this.refreshFeed = function() {
   2169 		if (!this.collectionsView.selection.count) return;
   2170 		
   2171 		let feed = this.collectionsView.selectedTreeRow.ref;
   2172 		
   2173 		return feed.updateFeed();
   2174 	}
   2175 	
   2176 	
   2177 	this.copySelectedItemsToClipboard = function (asCitations) {
   2178 		var items = this.getSelectedItems();
   2179 		if (!items.length) {
   2180 			return;
   2181 		}
   2182 		
   2183 		var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
   2184 		format = Zotero.QuickCopy.unserializeSetting(format);
   2185 		
   2186 		// In bibliography mode, remove notes and attachments
   2187 		if (format.mode == 'bibliography') {
   2188 			items = items.filter(item => item.isRegularItem());
   2189 		}
   2190 		
   2191 		// DEBUG: We could copy notes via keyboard shortcut if we altered
   2192 		// Z_F_I.copyItemsToClipboard() to use Z.QuickCopy.getContentFromItems(),
   2193 		// but 1) we'd need to override that function's drag limit and 2) when I
   2194 		// tried it the OS X clipboard seemed to be getting text vs. HTML wrong,
   2195 		// automatically converting text/html to plaintext rather than using
   2196 		// text/unicode. (That may be fixable, however.)
   2197 		//
   2198 		// This isn't currently shown, because the commands are disabled when not relevant, so this
   2199 		// function isn't called
   2200 		if (!items.length) {
   2201 			let ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   2202 									.getService(Components.interfaces.nsIPromptService);
   2203 			ps.alert(null, "", Zotero.getString("fileInterface.noReferencesError"));
   2204 			return;
   2205 		}
   2206 		
   2207 		// determine locale preference
   2208 		var locale = format.locale ? format.locale : Zotero.Prefs.get('export.quickCopy.locale');
   2209 		
   2210 		if (format.mode == 'bibliography') {
   2211 			Zotero_File_Interface.copyItemsToClipboard(
   2212 				items, format.id, locale, format.contentType == 'html', asCitations
   2213 			);
   2214 		}
   2215 		else if (format.mode == 'export') {
   2216 			// Copy citations doesn't work in export mode
   2217 			if (asCitations) {
   2218 				return;
   2219 			}
   2220 			else {
   2221 				Zotero_File_Interface.exportItemsToClipboard(items, format.id);
   2222 			}
   2223 		}
   2224 	}
   2225 	
   2226 	
   2227 	this.clearQuicksearch = Zotero.Promise.coroutine(function* () {
   2228 		var search = document.getElementById('zotero-tb-search');
   2229 		if (search.value !== '') {
   2230 			search.value = '';
   2231 			yield this.search();
   2232 			return true;
   2233 		}
   2234 		return false;
   2235 	});
   2236 	
   2237 	
   2238 	/**
   2239 	 * Some keys trigger an immediate search
   2240 	 */
   2241 	this.handleSearchKeypress = function (textbox, event) {
   2242 		if (event.keyCode == event.DOM_VK_ESCAPE) {
   2243 			textbox.value = '';
   2244 			this.search();
   2245 		}
   2246 		else if (event.keyCode == event.DOM_VK_RETURN) {
   2247 			this.search(true);
   2248 		}
   2249 	}
   2250 	
   2251 	
   2252 	this.handleSearchInput = function (textbox, event) {
   2253 		if (textbox.value.indexOf('"') != -1) {
   2254 			this.setItemsPaneMessage(Zotero.getString('advancedSearchMode'));
   2255 		}
   2256 	}
   2257 	
   2258 	
   2259 	/**
   2260 	 * @return {Promise}
   2261 	 */
   2262 	this.search = Zotero.Promise.coroutine(function* (runAdvanced) {
   2263 		if (!this.itemsView) {
   2264 			return;
   2265 		}
   2266 		var search = document.getElementById('zotero-tb-search');
   2267 		if (!runAdvanced && search.value.indexOf('"') != -1) {
   2268 			return;
   2269 		}
   2270 		var spinner = document.getElementById('zotero-tb-search-spinner');
   2271 		spinner.style.display = 'inline';
   2272 		var searchVal = search.value;
   2273 		yield this.itemsView.setFilter('search', searchVal);
   2274 		spinner.style.display = 'none';
   2275 		if (runAdvanced) {
   2276 			this.clearItemsPaneMessage();
   2277 		}
   2278 	});
   2279 	
   2280 	
   2281 	this.selectItem = Zotero.Promise.coroutine(function* (itemID, inLibraryRoot, expand) {
   2282 		if (!itemID) {
   2283 			return false;
   2284 		}
   2285 		
   2286 		var item = yield Zotero.Items.getAsync(itemID);
   2287 		if (!item) {
   2288 			return false;
   2289 		}
   2290 		
   2291 		// Restore window if it's in the dock
   2292 		if (window.windowState == Components.interfaces.nsIDOMChromeWindow.STATE_MINIMIZED) {
   2293 			window.restore();
   2294 		}
   2295 		
   2296 		if (!this.collectionsView) {
   2297 			throw new Error("Collections view not loaded");
   2298 		}
   2299 		
   2300 		var found = yield this.collectionsView.selectItem(itemID, inLibraryRoot, expand);
   2301 		
   2302 		// Focus the items pane
   2303 		if (found) {
   2304 			document.getElementById('zotero-items-tree').focus();
   2305 		}
   2306 		
   2307 		// open Zotero pane
   2308 		this.show();
   2309 	});
   2310 	
   2311 	
   2312 	this.getSelectedLibraryID = function () {
   2313 		return this.collectionsView.getSelectedLibraryID();
   2314 	}
   2315 	
   2316 	
   2317 	function getSelectedCollection(asID) {
   2318 		return this.collectionsView ? this.collectionsView.getSelectedCollection(asID) : false;
   2319 	}
   2320 	
   2321 	
   2322 	function getSelectedSavedSearch(asID)
   2323 	{
   2324 		if (this.collectionsView.selection.count > 0 && this.collectionsView.selection.currentIndex != -1) {
   2325 			var collection = this.collectionsView.getRow(this.collectionsView.selection.currentIndex);
   2326 			if (collection && collection.isSearch()) {
   2327 				return asID ? collection.ref.id : collection.ref;
   2328 			}
   2329 		}
   2330 		return false;
   2331 	}
   2332 	
   2333 	
   2334 	/*
   2335 	 * Return an array of Item objects for selected items
   2336 	 *
   2337 	 * If asIDs is true, return an array of itemIDs instead
   2338 	 */
   2339 	function getSelectedItems(asIDs)
   2340 	{
   2341 		if (!this.itemsView) {
   2342 			return [];
   2343 		}
   2344 		
   2345 		return this.itemsView.getSelectedItems(asIDs);
   2346 	}
   2347 	
   2348 	
   2349 	this.getSelectedGroup = function (asID) {
   2350 		if (this.collectionsView.selection
   2351 				&& this.collectionsView.selection.count > 0
   2352 				&& this.collectionsView.selection.currentIndex != -1) {
   2353 		
   2354 			var collectionTreeRow = this.getCollectionTreeRow();
   2355 			if (collectionTreeRow && collectionTreeRow.isGroup()) {
   2356 				return asID ? collectionTreeRow.ref.id : collectionTreeRow.ref;
   2357 			}
   2358 		}
   2359 		return false;
   2360 	}
   2361 	
   2362 	
   2363 	/*
   2364 	 * Returns an array of Zotero.Item objects of visible items in current sort order
   2365 	 *
   2366 	 * If asIDs is true, return an array of itemIDs instead
   2367 	 */
   2368 	function getSortedItems(asIDs) {
   2369 		if (!this.itemsView) {
   2370 			return [];
   2371 		}
   2372 		
   2373 		return this.itemsView.getSortedItems(asIDs);
   2374 	}
   2375 	
   2376 	
   2377 	function getSortField() {
   2378 		if (!this.itemsView) {
   2379 			return false;
   2380 		}
   2381 		
   2382 		return this.itemsView.getSortField();
   2383 	}
   2384 	
   2385 	
   2386 	function getSortDirection() {
   2387 		if (!this.itemsView) {
   2388 			return false;
   2389 		}
   2390 		
   2391 		return this.itemsView.getSortDirection();
   2392 	}
   2393 	
   2394 	
   2395 	/**
   2396 	 * Show context menu once it's ready
   2397 	 */
   2398 	this.onCollectionsContextMenuOpen = async function (event) {
   2399 		await ZoteroPane.buildCollectionContextMenu();
   2400 		document.getElementById('zotero-collectionmenu').openPopup(
   2401 			null, null, event.clientX + 1, event.clientY + 1, true, false, event
   2402 		);
   2403 	};
   2404 	
   2405 	
   2406 	/**
   2407 	 * Show context menu once it's ready
   2408 	 */
   2409 	this.onItemsContextMenuOpen = async function (event) {
   2410 		await ZoteroPane.buildItemContextMenu()
   2411 		document.getElementById('zotero-itemmenu').openPopup(
   2412 			null, null, event.clientX + 1, event.clientY + 1, true, false, event
   2413 		);
   2414 	};
   2415 	
   2416 	
   2417 	this.onCollectionContextMenuSelect = function (event) {
   2418 		event.stopPropagation();
   2419 		var o = _collectionContextMenuOptions.find(o => o.id == event.target.id)
   2420 		if (o.oncommand) {
   2421 			o.oncommand();
   2422 		}
   2423 	};
   2424 	
   2425 	
   2426 	// menuitem configuration
   2427 	//
   2428 	// This has to be kept in sync with zotero-collectionmenu in zoteroPane.xul. We could do this
   2429 	// entirely in JS, but various localized strings are only in zotero.dtd, and they're used in
   2430 	// standalone.xul as well, so for now they have to remain as XML entities.
   2431 	var _collectionContextMenuOptions = [
   2432 		{
   2433 			id: "sync",
   2434 			label: Zotero.getString('sync.sync'),
   2435 			oncommand: () => {
   2436 				Zotero.Sync.Runner.sync({
   2437 					libraries: [this.getSelectedLibraryID()],
   2438 				});
   2439 			}
   2440 		},
   2441 		{
   2442 			id: "sep1",
   2443 		},
   2444 		{
   2445 			id: "newCollection",
   2446 			command: "cmd_zotero_newCollection"
   2447 		},
   2448 		{
   2449 			id: "newSavedSearch",
   2450 			command: "cmd_zotero_newSavedSearch"
   2451 		},
   2452 		{
   2453 			id: "newSubcollection",
   2454 			oncommand: () => {
   2455 				this.newCollection(this.getSelectedCollection().key);
   2456 			}
   2457 		},
   2458 		{
   2459 			id: "refreshFeed",
   2460 			oncommand: () => this.refreshFeed()
   2461 		},
   2462 		{
   2463 			id: "sep2",
   2464 		},
   2465 		{
   2466 			id: "showDuplicates",
   2467 			oncommand: () => {
   2468 				this.setVirtual(this.getSelectedLibraryID(), 'duplicates', true);
   2469 			}
   2470 		},
   2471 		{
   2472 			id: "showUnfiled",
   2473 			oncommand: () => {
   2474 				this.setVirtual(this.getSelectedLibraryID(), 'unfiled', true);
   2475 			}
   2476 		},
   2477 		{
   2478 			id: "editSelectedCollection",
   2479 			oncommand: () => this.editSelectedCollection()
   2480 		},
   2481 		{
   2482 			id: "markReadFeed",
   2483 			oncommand: () => this.markFeedRead()
   2484 		},
   2485 		{
   2486 			id: "editSelectedFeed",
   2487 			oncommand: () => this.editSelectedFeed()
   2488 		},
   2489 		{
   2490 			id: "deleteCollection",
   2491 			oncommand: () => this.deleteSelectedCollection()
   2492 		},
   2493 		{
   2494 			id: "deleteCollectionAndItems",
   2495 			oncommand: () => this.deleteSelectedCollection(true)
   2496 		},
   2497 		{
   2498 			id: "sep3",
   2499 		},
   2500 		{
   2501 			id: "exportCollection",
   2502 			oncommand: () => Zotero_File_Interface.exportCollection()
   2503 		},
   2504 		{
   2505 			id: "createBibCollection",
   2506 			oncommand: () => Zotero_File_Interface.bibliographyFromCollection()
   2507 		},
   2508 		{
   2509 			id: "exportFile",
   2510 			oncommand: () => Zotero_File_Interface.exportFile()
   2511 		},
   2512 		{
   2513 			id: "loadReport",
   2514 			oncommand: event => Zotero_Report_Interface.loadCollectionReport(event)
   2515 		},
   2516 		{
   2517 			id: "emptyTrash",
   2518 			oncommand: () => this.emptyTrash()
   2519 		},
   2520 		{
   2521 			id: "removeLibrary",
   2522 			label: Zotero.getString('pane.collections.menu.remove.library'),
   2523 			oncommand: () => {
   2524 				let library = Zotero.Libraries.get(this.getSelectedLibraryID());
   2525 				let ps = Services.prompt;
   2526 				let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
   2527 					+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
   2528 				let index = ps.confirmEx(
   2529 					null,
   2530 					Zotero.getString('pane.collections.removeLibrary'),
   2531 					Zotero.getString('pane.collections.removeLibrary.text', library.name),
   2532 					buttonFlags,
   2533 					Zotero.getString('general.remove'),
   2534 					null,
   2535 					null, null, {}
   2536 				);
   2537 				if (index == 0) {
   2538 					library.eraseTx();
   2539 				}
   2540 			}
   2541 		},
   2542 	];
   2543 	
   2544 	this.buildCollectionContextMenu = async function () {
   2545 		var libraryID = this.getSelectedLibraryID();
   2546 		var options = _collectionContextMenuOptions;
   2547 		
   2548 		var collectionTreeRow = this.collectionsView.selectedTreeRow;
   2549 		// This can happen if selection is changing during delayed second call below
   2550 		if (!collectionTreeRow) {
   2551 			return;
   2552 		}
   2553 		
   2554 		// If the items view isn't initialized, this was a right-click on a different collection
   2555 		// and the new collection's items are still loading, so continue menu after loading is
   2556 		// done. This causes some menu items (e.g., export/createBib/loadReport) to appear gray
   2557 		// in the menu at first and then turn black once there are items
   2558 		if (!collectionTreeRow.isHeader() && !this.itemsView.initialized) {
   2559 			await new Promise((resolve) => {
   2560 				this.itemsView.onLoad.addListener(() => {
   2561 					resolve();
   2562 				});
   2563 			});
   2564 		}
   2565 		
   2566 		// Set attributes on the menu from the configuration object
   2567 		var menu = document.getElementById('zotero-collectionmenu');
   2568 		var m = {};
   2569 		for (let i = 0; i < options.length; i++) {
   2570 			let option = options[i];
   2571 			let menuitem = menu.childNodes[i];
   2572 			m[option.id] = menuitem;
   2573 			
   2574 			menuitem.id = option.id;
   2575 			if (!menuitem.classList.contains('menuitem-iconic')) {
   2576 				menuitem.classList.add('menuitem-iconic');
   2577 			}
   2578 			if (option.label) {
   2579 				menuitem.setAttribute('label', option.label);
   2580 			}
   2581 			if (option.command) {
   2582 				menuitem.setAttribute('command', option.command);
   2583 			}
   2584 		}
   2585 		
   2586 		// By default things are hidden and visible, so we only need to record
   2587 		// when things are visible and when they're visible but disabled
   2588 		var show = [], disable = [];
   2589 		
   2590 		if (collectionTreeRow.isCollection()) {
   2591 			show = [
   2592 				'newSubcollection',
   2593 				'sep2',
   2594 				'editSelectedCollection',
   2595 				'deleteCollection',
   2596 				'deleteCollectionAndItems',
   2597 				'sep3',
   2598 				'exportCollection',
   2599 				'createBibCollection',
   2600 				'loadReport'
   2601 			];
   2602 			
   2603 			if (!this.itemsView.rowCount) {
   2604 				disable = ['createBibCollection', 'loadReport'];
   2605 				
   2606 				// If no items in subcollections either, disable export
   2607 				if (!(await collectionTreeRow.ref.getDescendents(false, 'item', false).length)) {
   2608 					disable.push('exportCollection');
   2609 				}
   2610 			}
   2611 			
   2612 			// Adjust labels
   2613 			m.editSelectedCollection.setAttribute('label', Zotero.getString('pane.collections.menu.rename.collection'));
   2614 			m.deleteCollection.setAttribute('label', Zotero.getString('pane.collections.menu.delete.collection'));
   2615 			m.deleteCollectionAndItems.setAttribute('label', Zotero.getString('pane.collections.menu.delete.collectionAndItems'));
   2616 			m.exportCollection.setAttribute('label', Zotero.getString('pane.collections.menu.export.collection'));
   2617 			m.createBibCollection.setAttribute('label', Zotero.getString('pane.collections.menu.createBib.collection'));
   2618 			m.loadReport.setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.collection'));
   2619 		}
   2620 		else if (collectionTreeRow.isFeed()) {
   2621 			show = [
   2622 				'refreshFeed',
   2623 				'sep2',
   2624 				'markReadFeed',
   2625 				'editSelectedFeed',
   2626 				'deleteCollectionAndItems'
   2627 			];
   2628 			
   2629 			if (collectionTreeRow.ref.unreadCount == 0) {
   2630 				disable = ['markReadFeed'];
   2631 			}
   2632 			
   2633 			// Adjust labels
   2634 			m.deleteCollectionAndItems.setAttribute('label', Zotero.getString('pane.collections.menu.delete.feedAndItems'));
   2635 		}
   2636 		else if (collectionTreeRow.isSearch()) {
   2637 			show = [
   2638 				'editSelectedCollection',
   2639 				'deleteCollection',
   2640 				'sep3',
   2641 				'exportCollection',
   2642 				'createBibCollection',
   2643 				'loadReport'
   2644 			];
   2645 			
   2646 			m.deleteCollection.setAttribute('label', Zotero.getString('pane.collections.menu.delete.savedSearch'));
   2647 			
   2648 			if (!this.itemsView.rowCount) {
   2649 				disable.push('exportCollection', 'createBibCollection', 'loadReport');
   2650 			}
   2651 			
   2652 			// Adjust labels
   2653 			m.editSelectedCollection.setAttribute('label', Zotero.getString('pane.collections.menu.edit.savedSearch'));
   2654 			m.exportCollection.setAttribute('label', Zotero.getString('pane.collections.menu.export.savedSearch'));
   2655 			m.createBibCollection.setAttribute('label', Zotero.getString('pane.collections.menu.createBib.savedSearch'));
   2656 			m.loadReport.setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.savedSearch'));
   2657 		}
   2658 		else if (collectionTreeRow.isTrash()) {
   2659 			show = ['emptyTrash'];
   2660 		}
   2661 		else if (collectionTreeRow.isDuplicates() || collectionTreeRow.isUnfiled()) {
   2662 			show = ['deleteCollection'];
   2663 			
   2664 			m.deleteCollection.setAttribute('label', Zotero.getString('general.hide'));
   2665 		}
   2666 		else if (collectionTreeRow.isHeader()) {
   2667 		}
   2668 		else if (collectionTreeRow.isPublications()) {
   2669 			show = [
   2670 				'exportFile'
   2671 			];
   2672 		}
   2673 		// Library
   2674 		else {
   2675 			let library = Zotero.Libraries.get(libraryID);
   2676 			show = [];
   2677 			if (!library.archived) {
   2678 				show.push(
   2679 					'sync',
   2680 					'sep1',
   2681 					'newCollection',
   2682 					'newSavedSearch'
   2683 				);
   2684 			}
   2685 			// Only show "Show Duplicates" and "Show Unfiled Items" if rows are hidden
   2686 			let duplicates = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
   2687 				libraryID, 'duplicates'
   2688 			);
   2689 			let unfiled = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
   2690 				libraryID, 'unfiled'
   2691 			);
   2692 			if (!duplicates || !unfiled) {
   2693 				if (!library.archived) {
   2694 					show.push('sep2');
   2695 				}
   2696 				if (!duplicates) {
   2697 					show.push('showDuplicates');
   2698 				}
   2699 				if (!unfiled) {
   2700 					show.push('showUnfiled');
   2701 				}
   2702 			}
   2703 			if (!library.archived) {
   2704 				show.push('sep3');
   2705 			}
   2706 			show.push(
   2707 				'exportFile'
   2708 			);
   2709 			if (library.archived) {
   2710 				show.push('removeLibrary');
   2711 			}
   2712 		}
   2713 		
   2714 		// Disable some actions if user doesn't have write access
   2715 		//
   2716 		// Some actions are disabled via their commands in onCollectionSelected()
   2717 		if (collectionTreeRow.isWithinGroup() && !collectionTreeRow.editable && !collectionTreeRow.isDuplicates() && !collectionTreeRow.isUnfiled()) {
   2718 			disable.push(
   2719 				'newSubcollection',
   2720 				'editSelectedCollection',
   2721 				'deleteCollection',
   2722 				'deleteCollectionAndItems'
   2723 			);
   2724 		}
   2725 		
   2726 		// If within non-editable group or trash it empty, disable Empty Trash
   2727 		if (collectionTreeRow.isTrash()) {
   2728 			if ((collectionTreeRow.isWithinGroup() && !collectionTreeRow.isWithinEditableGroup()) || !this.itemsView.rowCount) {
   2729 				disable.push('emptyTrash');
   2730 			}
   2731 		}
   2732 		
   2733 		// Hide and enable all actions by default (so if they're shown they're enabled)
   2734 		for (let i in m) {
   2735 			m[i].setAttribute('hidden', true);
   2736 			m[i].setAttribute('disabled', false);
   2737 		}
   2738 		
   2739 		for (let id of show) {
   2740 			m[id].setAttribute('hidden', false);
   2741 		}
   2742 		
   2743 		for (let id of disable) {
   2744 			m[id].setAttribute('disabled', true);
   2745 		}
   2746 	};
   2747 	
   2748 	
   2749 	this.buildItemContextMenu = Zotero.Promise.coroutine(function* () {
   2750 		var options = [
   2751 			'showInLibrary',
   2752 			'sep1',
   2753 			'addNote',
   2754 			'addAttachments',
   2755 			'sep2',
   2756 			'toggleRead',
   2757 			'duplicateItem',
   2758 			'removeItems',
   2759 			'restoreToLibrary',
   2760 			'moveToTrash',
   2761 			'deleteFromLibrary',
   2762 			'mergeItems',
   2763 			'sep3',
   2764 			'exportItems',
   2765 			'createBib',
   2766 			'loadReport',
   2767 			'sep4',
   2768 			'recognizePDF',
   2769 			'unrecognize',
   2770 			'reportMetadata',
   2771 			'createParent',
   2772 			'renameAttachments',
   2773 			'reindexItem'
   2774 		];
   2775 		
   2776 		var m = {};
   2777 		for (let i = 0; i < options.length; i++) {
   2778 			m[options[i]] = i;
   2779 		}
   2780 		
   2781 		var menu = document.getElementById('zotero-itemmenu');
   2782 		
   2783 		// remove old locate menu items
   2784 		while(menu.firstChild && menu.firstChild.getAttribute("zotero-locate")) {
   2785 			menu.removeChild(menu.firstChild);
   2786 		}
   2787 		
   2788 		var disable = [], show = [], multiple = '';
   2789 		
   2790 		if (!this.itemsView) {
   2791 			return;
   2792 		}
   2793 		
   2794 		var collectionTreeRow = this.getCollectionTreeRow();
   2795 		var isTrash = collectionTreeRow.isTrash();
   2796 		
   2797 		if (isTrash) {
   2798 			show.push(m.deleteFromLibrary);
   2799 			show.push(m.restoreToLibrary);
   2800 		}
   2801 		else if (!collectionTreeRow.isFeed()) {
   2802 			show.push(m.moveToTrash);
   2803 		}
   2804 
   2805 		if(!collectionTreeRow.isFeed()) {
   2806 			show.push(m.sep3, m.exportItems, m.createBib, m.loadReport);
   2807 		}
   2808 		
   2809 		var items = this.getSelectedItems();
   2810 		
   2811 		if (items.length > 0) {
   2812 			// Multiple items selected
   2813 			if (items.length > 1) {
   2814 				var multiple =  '.multiple';
   2815 				
   2816 				var canMerge = true, canIndex = true, canRecognize = true, canUnrecognize = true, canRename = true;
   2817 				var canMarkRead = collectionTreeRow.isFeed();
   2818 				var markUnread = true;
   2819 				
   2820 				for (let i = 0; i < items.length; i++) {
   2821 					let item = items[i];
   2822 					if (canMerge && !item.isRegularItem() || item.isFeedItem || collectionTreeRow.isDuplicates()) {
   2823 						canMerge = false;
   2824 					}
   2825 					
   2826 					if (canIndex && !(yield Zotero.Fulltext.canReindex(item))) {
   2827 						canIndex = false;
   2828 					}
   2829 					
   2830 					if (canRecognize && !Zotero.RecognizePDF.canRecognize(item)) {
   2831 						canRecognize = false;
   2832 					}
   2833 					
   2834 					if (canUnrecognize && !Zotero.RecognizePDF.canUnrecognize(item)) {
   2835 						canUnrecognize = false;
   2836 					}
   2837 					
   2838 					// Show rename option only if all items are child attachments
   2839 					if (canRename && (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL)) {
   2840 						canRename = false;
   2841 					}
   2842 					
   2843 					if(canMarkRead && markUnread && !item.isRead) {
   2844 						markUnread = false;
   2845 					}
   2846 				}
   2847 				
   2848 				if (canMerge) {
   2849 					show.push(m.mergeItems);
   2850 				}
   2851 				
   2852 				if (canIndex) {
   2853 					show.push(m.reindexItem);
   2854 				}
   2855 				
   2856 				if (canRecognize) {
   2857 					show.push(m.recognizePDF);
   2858 				}
   2859 				
   2860 				if (canUnrecognize) {
   2861 					show.push(m.unrecognize);
   2862 				}
   2863 				
   2864 				if (canMarkRead) {
   2865 					show.push(m.toggleRead);
   2866 					if (markUnread) {
   2867 						menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsUnread'));
   2868 					} else {
   2869 						menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsRead'));
   2870 					}
   2871 				}
   2872 				
   2873 				var canCreateParent = true;
   2874 				for (let i = 0; i < items.length; i++) {
   2875 					let item = items[i];
   2876 					if (!item.isTopLevelItem() || !item.isAttachment() || item.isFeedItem) {
   2877 						canCreateParent = false;
   2878 						break;
   2879 					}
   2880 				}
   2881 				if (canCreateParent) {
   2882 					show.push(m.createParent);
   2883 				}
   2884 				
   2885 				if (canRename) {
   2886 					show.push(m.renameAttachments);
   2887 				}
   2888 				
   2889 				// Add in attachment separator
   2890 				if (canCreateParent || canRecognize || canUnrecognize || canRename || canIndex) {
   2891 					show.push(m.sep4);
   2892 				}
   2893 				
   2894 				// Block certain actions on files if no access and at least one item
   2895 				// is an imported attachment
   2896 				if (!collectionTreeRow.filesEditable) {
   2897 					var hasImportedAttachment = false;
   2898 					for (var i=0; i<items.length; i++) {
   2899 						var item = items[i];
   2900 						if (item.isImportedAttachment()) {
   2901 							hasImportedAttachment = true;
   2902 							break;
   2903 						}
   2904 					}
   2905 					if (hasImportedAttachment) {
   2906 						disable.push(m.moveToTrash, m.createParent, m.renameAttachments);
   2907 					}
   2908 				}
   2909 			}
   2910 			
   2911 			// Single item selected
   2912 			else
   2913 			{
   2914 				let item = items[0];
   2915 				menu.setAttribute('itemID', item.id);
   2916 				menu.setAttribute('itemKey', item.key);
   2917 				
   2918 				if (!isTrash) {
   2919 					// Show in Library
   2920 					if (!collectionTreeRow.isLibrary(true)) {
   2921 						show.push(m.showInLibrary, m.sep1);
   2922 					}
   2923 					
   2924 					if (item.isRegularItem() && !item.isFeedItem) {
   2925 						show.push(m.addNote, m.addAttachments, m.sep2);
   2926 					}
   2927 					
   2928 					if (Zotero.RecognizePDF.canUnrecognize(item)) {
   2929 						show.push(m.sep4, m.unrecognize, m.reportMetadata);
   2930 					}
   2931 					
   2932 					if (item.isAttachment()) {
   2933 						var showSep4 = false;
   2934 						
   2935 						if (Zotero.RecognizePDF.canRecognize(item)) {
   2936 							show.push(m.recognizePDF);
   2937 							showSep4 = true;
   2938 						}
   2939 						
   2940 						// Allow parent item creation for standalone attachments
   2941 						if (item.isTopLevelItem()) {
   2942 							show.push(m.createParent);
   2943 							showSep4 = true;
   2944 						}
   2945 						
   2946 						// Attachment rename option
   2947 						if (!item.isTopLevelItem() && item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
   2948 							show.push(m.renameAttachments);
   2949 							showSep4 = true;
   2950 						}
   2951 						
   2952 						// If not linked URL, show reindex line
   2953 						if (yield Zotero.Fulltext.canReindex(item)) {
   2954 							show.push(m.reindexItem);
   2955 							showSep4 = true;
   2956 						}
   2957 						
   2958 						if (showSep4) {
   2959 							show.push(m.sep4);
   2960 						}
   2961 					}
   2962 					else if (item.isFeedItem) {
   2963 						show.push(m.toggleRead);
   2964 						if (item.isRead) {
   2965 							menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsUnread'));
   2966 						} else {
   2967 							menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsRead'));
   2968 						}
   2969 					}
   2970 					else if (!collectionTreeRow.isPublications()) {
   2971 						show.push(m.duplicateItem);
   2972 					}
   2973 				}
   2974 				
   2975 				// Update attachment submenu
   2976 				var popup = document.getElementById('zotero-add-attachment-popup')
   2977 				this.updateAttachmentButtonMenu(popup);
   2978 				
   2979 				// Block certain actions on files if no access
   2980 				if (item.isImportedAttachment() && !collectionTreeRow.filesEditable) {
   2981 					[m.moveToTrash, m.createParent, m.renameAttachments].forEach(function (x) {
   2982 						disable.push(x);
   2983 					});
   2984 				}
   2985 			}
   2986 		}
   2987 		// No items selected
   2988 		else
   2989 		{
   2990 			// Show in Library
   2991 			if (!collectionTreeRow.isLibrary()) {
   2992 				show.push(m.showInLibrary, m.sep1);
   2993 			}
   2994 			
   2995 			disable.push(m.showInLibrary, m.duplicateItem, m.removeItems,
   2996 				m.moveToTrash, m.deleteFromLibrary, m.exportItems, m.createBib, m.loadReport);
   2997 		}
   2998 		
   2999 		if ((!collectionTreeRow.editable || collectionTreeRow.isPublications()) && !collectionTreeRow.isFeed()) {
   3000 			for (let i in m) {
   3001 				// Still allow some options for non-editable views
   3002 				switch (i) {
   3003 					case 'showInLibrary':
   3004 					case 'exportItems':
   3005 					case 'createBib':
   3006 					case 'loadReport':
   3007 					case 'toggleRead':
   3008 						continue;
   3009 				}
   3010 				if (isTrash) {
   3011 					switch (i) {
   3012 					case 'restoreToLibrary':
   3013 					case 'deleteFromLibrary':
   3014 						continue;
   3015 					}
   3016 				}
   3017 				else if (collectionTreeRow.isPublications()) {
   3018 					switch (i) {
   3019 					case 'addNote':
   3020 					case 'removeItems':
   3021 					case 'moveToTrash':
   3022 						continue;
   3023 					}
   3024 				}
   3025 				disable.push(m[i]);
   3026 			}
   3027 		}
   3028 		
   3029 		// Remove from collection
   3030 		if (collectionTreeRow.isCollection() && items.every(item => item.isTopLevelItem())) {
   3031 			menu.childNodes[m.removeItems].setAttribute('label', Zotero.getString('pane.items.menu.remove' + multiple));
   3032 			show.push(m.removeItems);
   3033 		}
   3034 		else if (collectionTreeRow.isPublications()) {
   3035 			menu.childNodes[m.removeItems].setAttribute('label', Zotero.getString('pane.items.menu.removeFromPublications' + multiple));
   3036 			show.push(m.removeItems);
   3037 		}
   3038 		
   3039 		// Set labels, plural if necessary
   3040 		menu.childNodes[m.moveToTrash].setAttribute('label', Zotero.getString('pane.items.menu.moveToTrash' + multiple));
   3041 		menu.childNodes[m.deleteFromLibrary].setAttribute('label', Zotero.getString('pane.items.menu.delete' + multiple));
   3042 		menu.childNodes[m.exportItems].setAttribute('label', Zotero.getString('pane.items.menu.export' + multiple));
   3043 		menu.childNodes[m.createBib].setAttribute('label', Zotero.getString('pane.items.menu.createBib' + multiple));
   3044 		menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.items.menu.generateReport' + multiple));
   3045 		menu.childNodes[m.createParent].setAttribute('label', Zotero.getString('pane.items.menu.createParent' + multiple));
   3046 		menu.childNodes[m.recognizePDF].setAttribute('label', Zotero.getString('pane.items.menu.recognizePDF' + multiple));
   3047 		menu.childNodes[m.renameAttachments].setAttribute('label', Zotero.getString('pane.items.menu.renameAttachments' + multiple));
   3048 		menu.childNodes[m.reindexItem].setAttribute('label', Zotero.getString('pane.items.menu.reindexItem' + multiple));
   3049 		
   3050 		// Hide and enable all actions by default (so if they're shown they're enabled)
   3051 		for (let i in m) {
   3052 			let pos = m[i];
   3053 			menu.childNodes[pos].setAttribute('hidden', true);
   3054 			menu.childNodes[pos].setAttribute('disabled', false);
   3055 		}
   3056 		
   3057 		for (var i in disable)
   3058 		{
   3059 			menu.childNodes[disable[i]].setAttribute('disabled', true);
   3060 		}
   3061 		
   3062 		for (var i in show)
   3063 		{
   3064 			menu.childNodes[show[i]].setAttribute('hidden', false);
   3065 		}
   3066 		
   3067 		// add locate menu options
   3068 		yield Zotero_LocateMenu.buildContextMenu(menu, true);
   3069 	});
   3070 	
   3071 	
   3072 	this.onTreeMouseDown = function (event) {
   3073 		var t = event.originalTarget;
   3074 		var tree = t.parentNode;
   3075 		
   3076 		// Ignore click on column headers
   3077 		if (!tree.treeBoxObject) {
   3078 			return;
   3079 		}
   3080 		
   3081 		var row = {}, col = {}, obj = {};
   3082 		tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj);
   3083 		if (row.value == -1) {
   3084 			return;
   3085 		}
   3086 		
   3087 		if (tree.id == 'zotero-collections-tree') {
   3088 			let collectionTreeRow = ZoteroPane_Local.collectionsView.getRow(row.value);
   3089 			
   3090 			// Prevent the tree's select event from being called for a click
   3091 			// on a library sync error icon
   3092 			if (collectionTreeRow.isLibrary(true)) {
   3093 				if (col.value.id == 'zotero-collections-sync-status-column') {
   3094 					var errors = Zotero.Sync.Runner.getErrors(collectionTreeRow.ref.libraryID);
   3095 					if (errors) {
   3096 						event.stopPropagation();
   3097 						return;
   3098 					}
   3099 				}
   3100 			}
   3101 		}
   3102 		else if (tree.id == 'zotero-items-tree') {
   3103 			let collectionTreeRow = ZoteroPane_Local.getCollectionTreeRow();
   3104 			
   3105 			// Automatically select all equivalent items when clicking on an item
   3106 			// in duplicates view
   3107 			if (collectionTreeRow.isDuplicates()) {
   3108 				// Trigger only on primary-button single clicks without modifiers
   3109 				// (so that items can still be selected and deselected manually)
   3110 				if (!event || event.detail != 1 || event.button != 0 || event.metaKey
   3111 					|| event.shiftKey || event.altKey || event.ctrlKey) {
   3112 					return;
   3113 				}
   3114 				
   3115 				var t = event.originalTarget;
   3116 				
   3117 				if (t.localName != 'treechildren') {
   3118 					return;
   3119 				}
   3120 				
   3121 				var tree = t.parentNode;
   3122 				
   3123 				var row = {}, col = {}, obj = {};
   3124 				tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj);
   3125 				
   3126 				// obj.value == 'cell'/'text'/'image'/'twisty'
   3127 				if (!obj.value) {
   3128 					return;
   3129 				}
   3130 				
   3131 				// Duplicated in itemTreeView.js::notify()
   3132 				var itemID = ZoteroPane_Local.itemsView.getRow(row.value).ref.id;
   3133 				var setItemIDs = collectionTreeRow.ref.getSetItemsByItemID(itemID);
   3134 				ZoteroPane_Local.itemsView.selectItems(setItemIDs);
   3135 				
   3136 				// Prevent the tree's select event from being called here,
   3137 				// since it's triggered by the multi-select
   3138 				event.stopPropagation();
   3139 			}
   3140 		}
   3141 	}
   3142 	
   3143 	
   3144 	// Adapted from: http://www.xulplanet.com/references/elemref/ref_tree.html#cmnote-9
   3145 	this.onTreeClick = function (event) {
   3146 		var t = event.originalTarget;
   3147 		
   3148 		if (t.localName != 'treechildren') {
   3149 			return;
   3150 		}
   3151 		
   3152 		var tree = t.parentNode;
   3153 		
   3154 		var row = {}, col = {}, obj = {};
   3155 		tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj);
   3156 		
   3157 		// We care only about primary-button double and triple clicks
   3158 		if (!event || (event.detail != 2 && event.detail != 3) || event.button != 0) {
   3159 			if (row.value == -1) {
   3160 				return;
   3161 			}
   3162 			
   3163 			if (tree.id == 'zotero-collections-tree') {
   3164 				let collectionTreeRow = ZoteroPane_Local.collectionsView.getRow(row.value);
   3165 				
   3166 				// Show the error panel when clicking a library-specific
   3167 				// sync error icon
   3168 				if (collectionTreeRow.isLibrary(true)) {
   3169 					if (col.value.id == 'zotero-collections-sync-status-column') {
   3170 						var errors = Zotero.Sync.Runner.getErrors(collectionTreeRow.ref.libraryID);
   3171 						if (!errors) {
   3172 							return;
   3173 						}
   3174 						
   3175 						var panel = Zotero.Sync.Runner.updateErrorPanel(window.document, errors);
   3176 						
   3177 						var anchor = document.getElementById('zotero-collections-tree-shim');
   3178 						
   3179 						var x = {}, y = {}, width = {}, height = {};
   3180 						tree.treeBoxObject.getCoordsForCellItem(row.value, col.value, 'image', x, y, width, height);
   3181 						
   3182 						x = x.value + Math.round(width.value / 2);
   3183 						y = y.value + height.value + 3;
   3184 						
   3185 						panel.openPopup(anchor, "after_start", x, y, false, false);
   3186 					}
   3187 					return;
   3188 				}
   3189 			}
   3190 			
   3191 			// The Mozilla tree binding fires select() in mousedown(),
   3192 			// but if when it gets to click() the selection differs from
   3193 			// what it expects (say, because multiple items had been
   3194 			// selected during mousedown(), as is the case in duplicates mode),
   3195 			// it fires select() again. We prevent that here.
   3196 			else if (tree.id == 'zotero-items-tree') {
   3197 				let collectionTreeRow = ZoteroPane_Local.getCollectionTreeRow();
   3198 				if (collectionTreeRow.isDuplicates()) {
   3199 					if (event.button != 0 || event.metaKey || event.shiftKey
   3200 						|| event.altKey || event.ctrlKey) {
   3201 						return;
   3202 					}
   3203 					
   3204 					if (obj.value == 'twisty') {
   3205 						return;
   3206 					}
   3207 					
   3208 					event.stopPropagation();
   3209 					event.preventDefault();
   3210 				}
   3211 			}
   3212 			
   3213 			return;
   3214 		}
   3215 		
   3216 		var collectionTreeRow = ZoteroPane_Local.getCollectionTreeRow();
   3217 		
   3218 		// Ignore double-clicks in duplicates view on everything except attachments
   3219 		if (collectionTreeRow.isDuplicates()) {
   3220 			var items = ZoteroPane_Local.getSelectedItems();
   3221 			if (items.length != 1 || !items[0].isAttachment()) {
   3222 				event.stopPropagation();
   3223 				event.preventDefault();
   3224 				return;
   3225 			}
   3226 		}
   3227 		
   3228 		// obj.value == 'cell'/'text'/'image'
   3229 		if (!obj.value) {
   3230 			return;
   3231 		}
   3232 		
   3233 		if (tree.id == 'zotero-collections-tree') {                                                    
   3234 			// Ignore triple clicks for collections
   3235 			if (event.detail != 2) {
   3236 				return;
   3237 			}
   3238 			
   3239 			if (collectionTreeRow.isLibrary()) {
   3240 				var uri = Zotero.URI.getCurrentUserLibraryURI();
   3241 				if (uri) {
   3242 					ZoteroPane_Local.loadURI(uri);
   3243 					event.stopPropagation();
   3244 				}
   3245 				return;
   3246 			}
   3247 			
   3248 			if (collectionTreeRow.isSearch()) {
   3249 				ZoteroPane_Local.editSelectedCollection();
   3250 				return;
   3251 			}
   3252 			
   3253 			if (collectionTreeRow.isGroup()) {
   3254 				var uri = Zotero.URI.getGroupURI(collectionTreeRow.ref, true);
   3255 				ZoteroPane_Local.loadURI(uri);
   3256 				event.stopPropagation();
   3257 				return;
   3258 			}
   3259 			
   3260 			// Ignore double-clicks on Unfiled Items source row
   3261 			if (collectionTreeRow.isUnfiled()) {
   3262 				return;
   3263 			}
   3264 			
   3265 			if (collectionTreeRow.isHeader()) {
   3266 				if (collectionTreeRow.ref.id == 'group-libraries-header') {
   3267 					var uri = Zotero.URI.getGroupsURL();
   3268 					ZoteroPane_Local.loadURI(uri);
   3269 					event.stopPropagation();
   3270 				}
   3271 				return;
   3272 			}
   3273 
   3274 			if (collectionTreeRow.isBucket()) {
   3275 				ZoteroPane_Local.loadURI(collectionTreeRow.ref.uri);
   3276 				event.stopPropagation();
   3277 			}
   3278 		}
   3279 		else if (tree.id == 'zotero-items-tree') {
   3280 			var viewOnDoubleClick = Zotero.Prefs.get('viewOnDoubleClick');
   3281 			if (viewOnDoubleClick) {
   3282 				// Expand/collapse on triple-click, though the double-click
   3283 				// will still trigger
   3284 				if (event.detail == 3) {
   3285 					tree.view.toggleOpenState(tree.view.selection.currentIndex);
   3286 					return;
   3287 				}
   3288 				
   3289 				// Don't expand/collapse on double-click
   3290 				event.stopPropagation();
   3291 			}
   3292 			
   3293 			if (tree.view && tree.view.selection.currentIndex > -1) {
   3294 				var item = ZoteroPane_Local.getSelectedItems()[0];
   3295 				if (item) {
   3296 					if (!viewOnDoubleClick && item.isRegularItem()) {
   3297 						return;
   3298 					}
   3299 					ZoteroPane_Local.viewItems([item], event);
   3300 				}
   3301 			}
   3302 		}
   3303 	}
   3304 	
   3305 	
   3306 	this.openPreferences = function (paneID, action) {
   3307 		Zotero.warn("ZoteroPane.openPreferences() is deprecated"
   3308 			+ " -- use Zotero.Utilities.Internal.openPreferences() instead");
   3309 		Zotero.Utilities.Internal.openPreferences(paneID, { action });
   3310 	}
   3311 	
   3312 	
   3313 	/*
   3314 	 * Loads a URL following the standard modifier key behavior
   3315 	 *  (e.g. meta-click == new background tab, meta-shift-click == new front tab,
   3316 	 *  shift-click == new window, no modifier == frontmost tab
   3317 	 */
   3318 	this.loadURI = function (uris, event) {
   3319 		if(typeof uris === "string") {
   3320 			uris = [uris];
   3321 		}
   3322 		
   3323 		for (let i = 0; i < uris.length; i++) {
   3324 			let uri = uris[i];
   3325 			// Ignore javascript: and data: URIs
   3326 			if (uri.match(/^(javascript|data):/)) {
   3327 				return;
   3328 			}
   3329 			
   3330 			if (Zotero.isStandalone) {
   3331 				if(uri.match(/^https?/)) {
   3332 					this.launchURL(uri);
   3333 					continue;
   3334 				}
   3335 				
   3336 				// Handle no-content zotero: URLs (e.g., zotero://select) without opening viewer
   3337 				if (uri.startsWith('zotero:')) {
   3338 					let nsIURI = Services.io.newURI(uri, null, null);
   3339 					let handler = Components.classes["@mozilla.org/network/protocol;1?name=zotero"]
   3340 						.getService();
   3341 					let extension = handler.wrappedJSObject.getExtension(nsIURI);
   3342 					if (extension.noContent) {
   3343 						extension.doAction(nsIURI);
   3344 						return;
   3345 					}
   3346 				}
   3347 				
   3348 				Zotero.openInViewer(uri);
   3349 				return;
   3350 			}
   3351 			
   3352 			// Open in new tab
   3353 			var openInNewTab = event && (event.metaKey || (!Zotero.isMac && event.ctrlKey));
   3354 			if (event && event.shiftKey && !openInNewTab) {
   3355 				window.open(uri, "zotero-loaded-page",
   3356 					"menubar=yes,location=yes,toolbar=yes,personalbar=yes,resizable=yes,scrollbars=yes,status=yes");
   3357 			}
   3358 			else if (openInNewTab || !window.loadURI || uris.length > 1) {
   3359 				// if no gBrowser, find it
   3360 				if(!gBrowser) {
   3361 					let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
   3362 					var gBrowser = browserWindow.gBrowser;
   3363 				}
   3364 				
   3365 				// load in a new tab
   3366 				var tab = gBrowser.addTab(uri);
   3367 				var browser = gBrowser.getBrowserForTab(tab);
   3368 				
   3369 				if (event && event.shiftKey || !openInNewTab) {
   3370 					// if shift key is down, or we are opening in a new tab because there is no loadURI,
   3371 					// select new tab
   3372 					gBrowser.selectedTab = tab;
   3373 				}
   3374 			}
   3375 			else {
   3376 				window.loadURI(uri);
   3377 			}
   3378 		}
   3379 	}
   3380 	
   3381 	
   3382 	function setItemsPaneMessage(content, lock) {
   3383 		var elem = document.getElementById('zotero-items-pane-message-box');
   3384 		
   3385 		if (elem.getAttribute('locked') == 'true') {
   3386 			return;
   3387 		}
   3388 		
   3389 		elem.textContent = '';
   3390 		if (typeof content == 'string') {
   3391 			let contentParts = content.split("\n\n");
   3392 			for (let part of contentParts) {
   3393 				var desc = document.createElement('description');
   3394 				desc.appendChild(document.createTextNode(part));
   3395 				elem.appendChild(desc);
   3396 			}
   3397 		}
   3398 		else {
   3399 			elem.appendChild(content);
   3400 		}
   3401 		
   3402 		// Make message permanent
   3403 		if (lock) {
   3404 			elem.setAttribute('locked', true);
   3405 		}
   3406 		
   3407 		document.getElementById('zotero-items-pane-content').selectedIndex = 1;
   3408 	}
   3409 	
   3410 	
   3411 	function clearItemsPaneMessage() {
   3412 		// If message box is locked, don't clear
   3413 		var box = document.getElementById('zotero-items-pane-message-box');
   3414 		if (box.getAttribute('locked') == 'true') {
   3415 			return;
   3416 		}
   3417 		
   3418 		document.getElementById('zotero-items-pane-content').selectedIndex = 0;
   3419 	}
   3420 	
   3421 	
   3422 	this.setItemPaneMessage = function (content) {
   3423 		document.getElementById('zotero-item-pane-content').selectedIndex = 0;
   3424 		
   3425 		var elem = document.getElementById('zotero-item-pane-message-box');
   3426 		elem.textContent = '';
   3427 		if (typeof content == 'string') {
   3428 			let contentParts = content.split("\n\n");
   3429 			for (let part of contentParts) {
   3430 				let desc = document.createElement('description');
   3431 				desc.appendChild(document.createTextNode(part));
   3432 				elem.appendChild(desc);
   3433 			}
   3434 		}
   3435 		else {
   3436 			elem.appendChild(content);
   3437 		}
   3438 	}
   3439 	
   3440 	
   3441 	// Updates browser context menu options
   3442 	function contextPopupShowing()
   3443 	{
   3444 		if (!Zotero.Prefs.get('browserContentContextMenu')) {
   3445 			return;
   3446 		}
   3447 		
   3448 		var menuitem = document.getElementById("zotero-context-add-to-current-note");
   3449 		if (menuitem){
   3450 			var items = ZoteroPane_Local.getSelectedItems();
   3451 			if (ZoteroPane_Local.itemsView.selection && ZoteroPane_Local.itemsView.selection.count==1
   3452 				&& items[0] && items[0].isNote()
   3453 				&& window.gContextMenu.isTextSelected)
   3454 			{
   3455 				menuitem.hidden = false;
   3456 			}
   3457 			else
   3458 			{
   3459 				menuitem.hidden = true;
   3460 			}
   3461 		}
   3462 		
   3463 		var menuitem = document.getElementById("zotero-context-add-to-new-note");
   3464 		if (menuitem){
   3465 			if (window.gContextMenu.isTextSelected)
   3466 			{
   3467 				menuitem.hidden = false;
   3468 			}
   3469 			else
   3470 			{
   3471 				menuitem.hidden = true;
   3472 			}
   3473 		}
   3474 		
   3475 		var menuitem = document.getElementById("zotero-context-save-link-as-item");
   3476 		if (menuitem) {
   3477 			if (window.gContextMenu.onLink) {
   3478 				menuitem.hidden = false;
   3479 			}
   3480 			else {
   3481 				menuitem.hidden = true;
   3482 			}
   3483 		}
   3484 		
   3485 		var menuitem = document.getElementById("zotero-context-save-image-as-item");
   3486 		if (menuitem) {
   3487 			// Not using window.gContextMenu.hasBGImage -- if the user wants it,
   3488 			// they can use the Firefox option to view and then import from there
   3489 			if (window.gContextMenu.onImage) {
   3490 				menuitem.hidden = false;
   3491 			}
   3492 			else {
   3493 				menuitem.hidden = true;
   3494 			}
   3495 		}
   3496 		
   3497 		// If Zotero is locked or library is read-only, disable menu items
   3498 		var menu = document.getElementById('zotero-content-area-context-menu');
   3499 		var disabled = Zotero.locked;
   3500 		if (!disabled && self.collectionsView.selection && self.collectionsView.selection.count) {
   3501 			var collectionTreeRow = self.collectionsView.selectedTreeRow;
   3502 			disabled = !collectionTreeRow.editable;
   3503 		}
   3504 		for (let menuitem of menu.firstChild.childNodes) {
   3505 			menuitem.disabled = disabled;
   3506 		}
   3507 	}
   3508 	
   3509 	/**
   3510 	 * @return {Promise<Integer|null|false>} - The id of the new note in non-popup mode, null in
   3511 	 *     popup mode (where a note isn't created immediately), or false if library isn't editable
   3512 	 */
   3513 	this.newNote = Zotero.Promise.coroutine(function* (popup, parentKey, text, citeURI) {
   3514 		if (!this.canEdit()) {
   3515 			this.displayCannotEditLibraryMessage();
   3516 			return false;
   3517 		}
   3518 		
   3519 		if (popup) {
   3520 			// TODO: _text_
   3521 			var c = this.getSelectedCollection();
   3522 			if (c) {
   3523 				this.openNoteWindow(null, c.id, parentKey);
   3524 			}
   3525 			else {
   3526 				this.openNoteWindow(null, null, parentKey);
   3527 			}
   3528 			return null;
   3529 		}
   3530 		
   3531 		if (!text) {
   3532 			text = '';
   3533 		}
   3534 		text = text.trim();
   3535 		
   3536 		if (text) {
   3537 			text = '<blockquote'
   3538 					+ (citeURI ? ' cite="' + citeURI + '"' : '')
   3539 					+ '>' + Zotero.Utilities.text2html(text) + "</blockquote>";
   3540 		}
   3541 		
   3542 		var item = new Zotero.Item('note');
   3543 		item.libraryID = this.getSelectedLibraryID();
   3544 		item.setNote(text);
   3545 		if (parentKey) {
   3546 			item.parentKey = parentKey;
   3547 		}
   3548 		else if (this.collectionsView.selectedTreeRow.isCollection()) {
   3549 			item.addToCollection(this.collectionsView.selectedTreeRow.ref.id);
   3550 		}
   3551 		var itemID = yield item.saveTx();
   3552 		
   3553 		yield this.selectItem(itemID);
   3554 		
   3555 		document.getElementById('zotero-note-editor').focus();
   3556 		
   3557 		return itemID;
   3558 	});
   3559 	
   3560 	
   3561 	/**
   3562 	 * Creates a child note for the selected item or the selected item's parent
   3563 	 *
   3564 	 * @return {Promise}
   3565 	 */
   3566 	this.newChildNote = function (popup) {
   3567 		var selected = this.getSelectedItems()[0];
   3568 		var parentKey = selected.parentItemKey;
   3569 		parentKey = parentKey ? parentKey : selected.key;
   3570 		this.newNote(popup, parentKey);
   3571 	}
   3572 	
   3573 	
   3574 	this.addSelectedTextToCurrentNote = Zotero.Promise.coroutine(function* () {
   3575 		if (!this.canEdit()) {
   3576 			this.displayCannotEditLibraryMessage();
   3577 			return;
   3578 		}
   3579 		
   3580 		var text = event.currentTarget.ownerDocument.popupNode.ownerDocument.defaultView.getSelection().toString();
   3581 		var uri = event.currentTarget.ownerDocument.popupNode.ownerDocument.location.href;
   3582 		
   3583 		if (!text) {
   3584 			return false;
   3585 		}
   3586 		
   3587 		text = text.trim();
   3588 		
   3589 		if (!text.length) {
   3590 			return false;
   3591 		}
   3592 		
   3593 		text = '<blockquote' + (uri ? ' cite="' + uri + '"' : '') + '>'
   3594 			+ Zotero.Utilities.text2html(text) + "</blockquote>";
   3595 		
   3596 		var items = this.getSelectedItems();
   3597 		
   3598 		if (this.itemsView.selection.count == 1 && items[0] && items[0].isNote()) {
   3599 			var note = items[0].getNote()
   3600 			
   3601 			items[0].setNote(note + text);
   3602 			yield items[0].saveTx();
   3603 			
   3604 			var noteElem = document.getElementById('zotero-note-editor')
   3605 			noteElem.focus();
   3606 			return true;
   3607 		}
   3608 		
   3609 		return false;
   3610 	});
   3611 	
   3612 	
   3613 	this.createItemAndNoteFromSelectedText = Zotero.Promise.coroutine(function* (event) {
   3614 		var str = event.currentTarget.ownerDocument.popupNode.ownerDocument.defaultView.getSelection().toString();
   3615 		var uri = event.currentTarget.ownerDocument.popupNode.ownerDocument.location.href;
   3616 		var item = yield ZoteroPane.addItemFromPage();
   3617 		if (item) {
   3618 			return ZoteroPane.newNote(false, item.key, str, uri)
   3619 		}
   3620 	});
   3621 	
   3622 	
   3623 	
   3624 	this.openNoteWindow = function (itemID, col, parentKey) {
   3625 		if (!this.canEdit()) {
   3626 			this.displayCannotEditLibraryMessage();
   3627 			return;
   3628 		}
   3629 		
   3630 		var name = null;
   3631 		
   3632 		if (itemID) {
   3633 			let w = this.findNoteWindow(itemID);
   3634 			if (w) {
   3635 				w.focus();
   3636 				return;
   3637 			}
   3638 			
   3639 			// Create a name for this window so we can focus it later
   3640 			//
   3641 			// Collection is only used on new notes, so we don't need to
   3642 			// include it in the name
   3643 			name = 'zotero-note-' + itemID;
   3644 		}
   3645 		
   3646 		var io = { itemID: itemID, collectionID: col, parentItemKey: parentKey };
   3647 		window.openDialog('chrome://zotero/content/note.xul', name, 'chrome,resizable,centerscreen,dialog=false', io);
   3648 	}
   3649 	
   3650 	
   3651 	this.findNoteWindow = function (itemID) {
   3652 		var name = 'zotero-note-' + itemID;
   3653 		var wm = Services.wm;
   3654 		var e = wm.getEnumerator('zotero:note');
   3655 		while (e.hasMoreElements()) {
   3656 			var w = e.getNext();
   3657 			if (w.name == name) {
   3658 				return w;
   3659 			}
   3660 		}
   3661 	};
   3662 	
   3663 	
   3664 	this.onNoteWindowClosed = async function (itemID, noteText) {
   3665 		var item = Zotero.Items.get(itemID);
   3666 		item.setNote(noteText);
   3667 		await item.saveTx();
   3668 		
   3669 		// If note is still selected, show the editor again when the note window closes
   3670 		var selectedItems = this.getSelectedItems(true);
   3671 		if (selectedItems.length == 1 && itemID == selectedItems[0]) {
   3672 			ZoteroItemPane.onNoteSelected(item, this.collectionsView.editable);
   3673 		}
   3674 	};
   3675 	
   3676 	
   3677 	this.addAttachmentFromURI = Zotero.Promise.method(function (link, itemID) {
   3678 		if (!this.canEdit()) {
   3679 			this.displayCannotEditLibraryMessage();
   3680 			return;
   3681 		}
   3682 		
   3683 		var io = {};
   3684 		window.openDialog('chrome://zotero/content/attachLink.xul',
   3685 			'zotero-attach-uri-dialog', 'centerscreen, modal', io);
   3686 		if (!io.out) return;
   3687 		return Zotero.Attachments.linkFromURL({
   3688 			url: io.out.link,
   3689 			parentItemID: itemID,
   3690 			title: io.out.title
   3691 		});
   3692 	});
   3693 	
   3694 	
   3695 	this.addAttachmentFromDialog = Zotero.Promise.coroutine(function* (link, parentItemID) {
   3696 		if (!this.canEdit()) {
   3697 			this.displayCannotEditLibraryMessage();
   3698 			return;
   3699 		}
   3700 		
   3701 		var collectionTreeRow = this.getCollectionTreeRow();
   3702 		if (link) {
   3703 			if (collectionTreeRow.isWithinGroup()) {
   3704 				Zotero.alert(null, "", "Linked files cannot be added to group libraries.");
   3705 				return;
   3706 			}
   3707 			else if (collectionTreeRow.isPublications()) {
   3708 				Zotero.alert(
   3709 					null,
   3710 					Zotero.getString('general.error'),
   3711 					Zotero.getString('publications.error.linkedFilesCannotBeAdded')
   3712 				);
   3713 				return;
   3714 			}
   3715 		}
   3716 		
   3717 		// TODO: disable in menu
   3718 		if (!this.canEditFiles()) {
   3719 			this.displayCannotEditLibraryFilesMessage();
   3720 			return;
   3721 		}
   3722 		
   3723 		var libraryID = collectionTreeRow.ref.libraryID;
   3724 		
   3725 		var nsIFilePicker = Components.interfaces.nsIFilePicker;
   3726 		var fp = Components.classes["@mozilla.org/filepicker;1"]
   3727         					.createInstance(nsIFilePicker);
   3728 		fp.init(window, Zotero.getString('pane.item.attachments.select'), nsIFilePicker.modeOpenMultiple);
   3729 		fp.appendFilters(nsIFilePicker.filterAll);
   3730 		
   3731 		if (fp.show() != nsIFilePicker.returnOK) {
   3732 			return;
   3733 		}
   3734 		
   3735 		var enumerator = fp.files;
   3736 		var files = [];
   3737 		while (enumerator.hasMoreElements()) {
   3738 			let file = enumerator.getNext();
   3739 			file.QueryInterface(Components.interfaces.nsIFile);
   3740 			files.push(file.path);
   3741 		}
   3742 		
   3743 		var addedItems = [];
   3744 		var collection;
   3745 		var fileBaseName;
   3746 		if (parentItemID) {
   3747 			// If only one item is being added, automatic renaming is enabled, and the parent item
   3748 			// doesn't have any other non-HTML file attachments, rename the file.
   3749 			// This should be kept in sync with itemTreeView::drop().
   3750 			if (files.length == 1 && Zotero.Prefs.get('autoRenameFiles')) {
   3751 				let parentItem = Zotero.Items.get(parentItemID);
   3752 				if (!parentItem.numNonHTMLFileAttachments()) {
   3753 					fileBaseName = yield Zotero.Attachments.getRenamedFileBaseNameIfAllowedType(
   3754 						parentItem, files[0]
   3755 					);
   3756 				}
   3757 			}
   3758 		}
   3759 		// If not adding to an item, add to the current collection
   3760 		else {
   3761 			collection = this.getSelectedCollection(true);
   3762 		}
   3763 		
   3764 		for (let file of files) {
   3765 			let item;
   3766 			
   3767 			if (link) {
   3768 				// Rename linked file, with unique suffix if necessary
   3769 				try {
   3770 					if (fileBaseName) {
   3771 						let ext = Zotero.File.getExtension(file);
   3772 						let newName = yield Zotero.File.rename(
   3773 							file,
   3774 							fileBaseName + (ext ? '.' + ext : ''),
   3775 							{
   3776 								unique: true
   3777 							}
   3778 						);
   3779 						// Update path in case the name was changed to be unique
   3780 						file = OS.Path.join(OS.Path.dirname(file), newName);
   3781 					}
   3782 				}
   3783 				catch (e) {
   3784 					Zotero.logError(e);
   3785 				}
   3786 				
   3787 				item = yield Zotero.Attachments.linkFromFile({
   3788 					file,
   3789 					parentItemID,
   3790 					collections: collection ? [collection] : undefined
   3791 				});
   3792 			}
   3793 			else {
   3794 				if (file.endsWith(".lnk")) {
   3795 					let win = Services.wm.getMostRecentWindow("navigator:browser");
   3796 					win.ZoteroPane.displayCannotAddShortcutMessage(file);
   3797 					continue;
   3798 				}
   3799 				
   3800 				item = yield Zotero.Attachments.importFromFile({
   3801 					file,
   3802 					libraryID,
   3803 					fileBaseName,
   3804 					parentItemID,
   3805 					collections: collection ? [collection] : undefined
   3806 				});
   3807 			}
   3808 			
   3809 			addedItems.push(item);
   3810 		}
   3811 		
   3812 		// Automatically retrieve metadata for top-level PDFs
   3813 		if (!parentItemID) {
   3814 			Zotero.RecognizePDF.autoRecognizeItems(addedItems);
   3815 		}
   3816 	});
   3817 	
   3818 	
   3819 	/**
   3820 	 * @return {Promise<Zotero.Item>|false}
   3821 	 */
   3822 	this.addItemFromPage = Zotero.Promise.method(function (itemType, saveSnapshot, row) {
   3823 		if (row == undefined && this.collectionsView && this.collectionsView.selection) {
   3824 			row = this.collectionsView.selection.currentIndex;
   3825 		}
   3826 		
   3827 		if (row !== undefined) {
   3828 			if (!this.canEdit(row)) {
   3829 				this.displayCannotEditLibraryMessage();
   3830 				return false;
   3831 			}
   3832 			
   3833 			var collectionTreeRow = this.collectionsView.getRow(row);
   3834 			if (collectionTreeRow.isPublications()) {
   3835 				this.displayCannotAddToMyPublicationsMessage();
   3836 				return false;
   3837 			}
   3838 		}
   3839 		
   3840 		return this.addItemFromDocument(window.content.document, itemType, saveSnapshot, row);
   3841 	});
   3842 	
   3843 	/**
   3844 	 * Shows progress dialog for a webpage/snapshot save request
   3845 	 */
   3846 	function _showPageSaveStatus(title) {
   3847 		var progressWin = new Zotero.ProgressWindow();
   3848 		progressWin.changeHeadline(Zotero.getString('ingester.scraping'));
   3849 		var icon = 'chrome://zotero/skin/treeitem-webpage.png';
   3850 		progressWin.addLines(title, icon)
   3851 		progressWin.show();
   3852 		progressWin.startCloseTimer();
   3853 	}
   3854 	
   3855 	/**
   3856 	 * @param	{Document}			doc
   3857 	 * @param	{String|Integer}	[itemType='webpage']	Item type id or name
   3858 	 * @param	{Boolean}			[saveSnapshot]			Force saving or non-saving of a snapshot,
   3859 	 *														regardless of automaticSnapshots pref
   3860 	 * @return {Promise<Zotero.Item>|false}
   3861 	 */
   3862 	this.addItemFromDocument = Zotero.Promise.coroutine(function* (doc, itemType, saveSnapshot, row) {
   3863 		_showPageSaveStatus(doc.title);
   3864 		
   3865 		// Save snapshot if explicitly enabled or automatically pref is set and not explicitly disabled
   3866 		saveSnapshot = saveSnapshot || (saveSnapshot !== false && Zotero.Prefs.get('automaticSnapshots'));
   3867 		
   3868 		// TODO: this, needless to say, is a temporary hack
   3869 		if (itemType == 'temporaryPDFHack') {
   3870 			itemType = null;
   3871 			var isPDF = false;
   3872 			if (doc.title.indexOf('application/pdf') != -1 || Zotero.Attachments.isPDFJS(doc)
   3873 					|| doc.contentType == 'application/pdf') {
   3874 				isPDF = true;
   3875 			}
   3876 			else {
   3877 				var ios = Components.classes["@mozilla.org/network/io-service;1"].
   3878 							getService(Components.interfaces.nsIIOService);
   3879 				try {
   3880 					var uri = ios.newURI(doc.location, null, null);
   3881 					if (uri.fileName && uri.fileName.match(/pdf$/)) {
   3882 						isPDF = true;
   3883 					}
   3884 				}
   3885 				catch (e) {
   3886 					Zotero.debug(e);
   3887 					Components.utils.reportError(e);
   3888 				}
   3889 			}
   3890 			
   3891 			if (isPDF && saveSnapshot) {
   3892 				//
   3893 				// Duplicate newItem() checks here
   3894 				//
   3895 				if (Zotero.DB.inTransaction()) {
   3896 					yield Zotero.DB.waitForTransaction();
   3897 				}
   3898 				
   3899 				// Currently selected row
   3900 				if (row === undefined && this.collectionsView && this.collectionsView.selection) {
   3901 					row = this.collectionsView.selection.currentIndex;
   3902 				}
   3903 				
   3904 				if (row && !this.canEdit(row)) {
   3905 					this.displayCannotEditLibraryMessage();
   3906 					return false;
   3907 				}
   3908 				
   3909 				if (row !== undefined) {
   3910 					var collectionTreeRow = this.collectionsView.getRow(row);
   3911 					var libraryID = collectionTreeRow.ref.libraryID;
   3912 				}
   3913 				else {
   3914 					var libraryID = Zotero.Libraries.userLibraryID;
   3915 					var collectionTreeRow = null;
   3916 				}
   3917 				//
   3918 				//
   3919 				//
   3920 				
   3921 				if (row && !this.canEditFiles(row)) {
   3922 					this.displayCannotEditLibraryFilesMessage();
   3923 					return false;
   3924 				}
   3925 				
   3926 				if (collectionTreeRow && collectionTreeRow.isCollection()) {
   3927 					var collectionID = collectionTreeRow.ref.id;
   3928 				}
   3929 				else {
   3930 					var collectionID = false;
   3931 				}
   3932 				
   3933 				let item = yield Zotero.Attachments.importFromDocument({
   3934 					libraryID: libraryID,
   3935 					document: doc,
   3936 					collections: collectionID ? [collectionID] : []
   3937 				});
   3938 				
   3939 				yield this.selectItem(item.id);
   3940 				return false;
   3941 			}
   3942 		}
   3943 		
   3944 		// Save web page item by default
   3945 		if (!itemType) {
   3946 			itemType = 'webpage';
   3947 		}
   3948 		var data = {
   3949 			title: doc.title,
   3950 			url: doc.location.href,
   3951 			accessDate: "CURRENT_TIMESTAMP"
   3952 		}
   3953 		itemType = Zotero.ItemTypes.getID(itemType);
   3954 		var item = yield this.newItem(itemType, data, row);
   3955 		var filesEditable = Zotero.Libraries.get(item.libraryID).filesEditable;
   3956 		
   3957 		if (saveSnapshot) {
   3958 			var link = false;
   3959 			
   3960 			if (link) {
   3961 				yield Zotero.Attachments.linkFromDocument({
   3962 					document: doc,
   3963 					parentItemID: item.id
   3964 				});
   3965 			}
   3966 			else if (filesEditable) {
   3967 				yield Zotero.Attachments.importFromDocument({
   3968 					document: doc,
   3969 					parentItemID: item.id
   3970 				});
   3971 			}
   3972 		}
   3973 		
   3974 		return item;
   3975 	});
   3976 	
   3977 	
   3978 	/**
   3979 	 * @return {Zotero.Item|false} - The saved item, or false if item can't be saved
   3980 	 */
   3981 	this.addItemFromURL = Zotero.Promise.coroutine(function* (url, itemType, saveSnapshot, row) {
   3982 		if (window.content && url == window.content.document.location.href) {
   3983 			return this.addItemFromPage(itemType, saveSnapshot, row);
   3984 		}
   3985 		
   3986 		url = Zotero.Utilities.resolveIntermediateURL(url);
   3987 		
   3988 		let [mimeType, hasNativeHandler] = yield Zotero.MIME.getMIMETypeFromURL(url);
   3989 		
   3990 		// If native type, save using a hidden browser
   3991 		if (hasNativeHandler) {
   3992 			var deferred = Zotero.Promise.defer();
   3993 			
   3994 			var processor = function (doc) {
   3995 				return ZoteroPane_Local.addItemFromDocument(doc, itemType, saveSnapshot, row)
   3996 				.then(function (item) {
   3997 					deferred.resolve(item)
   3998 				});
   3999 			};
   4000 			var done = function () {}
   4001 			var exception = function (e) {
   4002 				Zotero.debug(e, 1);
   4003 				deferred.reject(e);
   4004 			}
   4005 			Zotero.HTTP.loadDocuments([url], processor, done, exception);
   4006 			
   4007 			return deferred.promise;
   4008 		}
   4009 		// Otherwise create placeholder item, attach attachment, and update from that
   4010 		else {
   4011 			// TODO: this, needless to say, is a temporary hack
   4012 			if (itemType == 'temporaryPDFHack') {
   4013 				itemType = null;
   4014 				
   4015 				if (mimeType == 'application/pdf') {
   4016 					//
   4017 					// Duplicate newItem() checks here
   4018 					//
   4019 					if (Zotero.DB.inTransaction()) {
   4020 						yield Zotero.DB.waitForTransaction();
   4021 					}
   4022 					
   4023 					// Currently selected row
   4024 					if (row === undefined) {
   4025 						row = ZoteroPane_Local.collectionsView.selection.currentIndex;
   4026 					}
   4027 					
   4028 					if (!ZoteroPane_Local.canEdit(row)) {
   4029 						ZoteroPane_Local.displayCannotEditLibraryMessage();
   4030 						return false;
   4031 					}
   4032 					
   4033 					if (row !== undefined) {
   4034 						var collectionTreeRow = ZoteroPane_Local.collectionsView.getRow(row);
   4035 						var libraryID = collectionTreeRow.ref.libraryID;
   4036 					}
   4037 					else {
   4038 						var libraryID = Zotero.Libraries.userLibraryID;
   4039 						var collectionTreeRow = null;
   4040 					}
   4041 					//
   4042 					//
   4043 					//
   4044 					
   4045 					if (!ZoteroPane_Local.canEditFiles(row)) {
   4046 						ZoteroPane_Local.displayCannotEditLibraryFilesMessage();
   4047 						return false;
   4048 					}
   4049 					
   4050 					if (collectionTreeRow && collectionTreeRow.isCollection()) {
   4051 						var collectionID = collectionTreeRow.ref.id;
   4052 					}
   4053 					else {
   4054 						var collectionID = false;
   4055 					}
   4056 					
   4057 					let attachmentItem = yield Zotero.Attachments.importFromURL({
   4058 						libraryID,
   4059 						url,
   4060 						collections: collectionID ? [collectionID] : undefined,
   4061 						contentType: mimeType
   4062 					});
   4063 					this.selectItem(attachmentItem.id)
   4064 					return attachmentItem;
   4065 				}
   4066 			}
   4067 			
   4068 			if (!itemType) {
   4069 				itemType = 'webpage';
   4070 			}
   4071 			
   4072 			var item = yield ZoteroPane_Local.newItem(itemType, {}, row)
   4073 			var filesEditable = Zotero.Libraries.get(item.libraryID).filesEditable;
   4074 			
   4075 			// Save snapshot if explicitly enabled or automatically pref is set and not explicitly disabled
   4076 			if (saveSnapshot || (saveSnapshot !== false && Zotero.Prefs.get('automaticSnapshots'))) {
   4077 				var link = false;
   4078 				
   4079 				if (link) {
   4080 					//Zotero.Attachments.linkFromURL(doc, item.id);
   4081 				}
   4082 				else if (filesEditable) {
   4083 					var attachmentItem = yield Zotero.Attachments.importFromURL({
   4084 						url,
   4085 						parentItemID: item.id,
   4086 						contentType: mimeType
   4087 					});
   4088 					if (attachmentItem) {
   4089 						item.setField('title', attachmentItem.getField('title'));
   4090 						item.setField('url', attachmentItem.getField('url'));
   4091 						item.setField('accessDate', attachmentItem.getField('accessDate'));
   4092 						yield item.saveTx();
   4093 					}
   4094 				}
   4095 			}
   4096 			
   4097 			return item;
   4098 		}
   4099 	});
   4100 	
   4101 	
   4102 	/*
   4103 	 * Create an attachment from the current page
   4104 	 *
   4105 	 * |itemID|    -- itemID of parent item
   4106 	 * |link|      -- create web link instead of snapshot
   4107 	 */
   4108 	this.addAttachmentFromPage = Zotero.Promise.coroutine(function* (link, itemID) {
   4109 		if (Zotero.DB.inTransaction()) {
   4110 			yield Zotero.DB.waitForTransaction();
   4111 		}
   4112 		
   4113 		if (typeof itemID != 'number') {
   4114 			throw new Error("itemID must be an integer");
   4115 		}
   4116 		
   4117 		var progressWin = new Zotero.ProgressWindow();
   4118 		progressWin.changeHeadline(Zotero.getString('save.' + (link ? 'link' : 'attachment')));
   4119 		var type = link ? 'web-link' : 'snapshot';
   4120 		var icon = 'chrome://zotero/skin/treeitem-attachment-' + type + '.png';
   4121 		progressWin.addLines(window.content.document.title, icon)
   4122 		progressWin.show();
   4123 		progressWin.startCloseTimer();
   4124 		
   4125 		if (link) {
   4126 			return Zotero.Attachments.linkFromDocument({
   4127 				document: window.content.document,
   4128 				parentItemID: itemID
   4129 			});
   4130 		}
   4131 		return Zotero.Attachments.importFromDocument({
   4132 			document: window.content.document,
   4133 			parentItemID: itemID
   4134 		});
   4135 	});
   4136 	
   4137 	
   4138 	this.viewItems = Zotero.Promise.coroutine(function* (items, event) {
   4139 		if (items.length > 1) {
   4140 			if (!event || (!event.metaKey && !event.shiftKey)) {
   4141 				event = { metaKey: true, shiftKey: true };
   4142 			}
   4143 		}
   4144 		
   4145 		for (let i = 0; i < items.length; i++) {
   4146 			let item = items[i];
   4147 			if (item.isRegularItem()) {
   4148 				// Prefer local file attachments
   4149 				var uri = Components.classes["@mozilla.org/network/standard-url;1"]
   4150 							.createInstance(Components.interfaces.nsIURI);
   4151 				let attachment = yield item.getBestAttachment();
   4152 				if (attachment) {
   4153 					yield this.viewAttachment(attachment.id, event);
   4154 					continue;
   4155 				}
   4156 				
   4157 				// Fall back to URI field, then DOI
   4158 				var uri = item.getField('url');
   4159 				if (!uri) {
   4160 					var doi = item.getField('DOI');
   4161 					if (doi) {
   4162 						// Pull out DOI, in case there's a prefix
   4163 						doi = Zotero.Utilities.cleanDOI(doi);
   4164 						if (doi) {
   4165 							uri = "http://dx.doi.org/" + encodeURIComponent(doi);
   4166 						}
   4167 					}
   4168 				}
   4169 				
   4170 				// Fall back to first attachment link
   4171 				if (!uri) {
   4172 					let attachmentID = item.getAttachments()[0];
   4173 					if (attachmentID) {
   4174 						let attachment = yield Zotero.Items.getAsync(attachmentID);
   4175 						if (attachment) uri = attachment.getField('url');
   4176 					}
   4177 				}
   4178 				
   4179 				if (uri) {
   4180 					this.loadURI(uri, event);
   4181 				}
   4182 			}
   4183 			else if (item.isNote()) {
   4184 				if (!this.collectionsView.editable) {
   4185 					continue;
   4186 				}
   4187 				document.getElementById('zotero-view-note-button').doCommand();
   4188 			}
   4189 			else if (item.isAttachment()) {
   4190 				yield this.viewAttachment(item.id, event);
   4191 			}
   4192 		}
   4193 	});
   4194 	
   4195 	
   4196 	this.viewAttachment = Zotero.serial(Zotero.Promise.coroutine(function* (itemIDs, event, noLocateOnMissing, forceExternalViewer) {
   4197 		// If view isn't editable, don't show Locate button, since the updated
   4198 		// path couldn't be sent back up
   4199 		if (!this.collectionsView.editable) {
   4200 			noLocateOnMissing = true;
   4201 		}
   4202 		
   4203 		if(typeof itemIDs != "object") itemIDs = [itemIDs];
   4204 		
   4205 		// If multiple items, set up event so we open in new tab
   4206 		if(itemIDs.length > 1) {
   4207 			if(!event || (!event.metaKey && !event.shiftKey)) {
   4208 				event = {"metaKey":true, "shiftKey":true};
   4209 			}
   4210 		}
   4211 		
   4212 		for (let i = 0; i < itemIDs.length; i++) {
   4213 			let itemID = itemIDs[i];
   4214 			var item = yield Zotero.Items.getAsync(itemID);
   4215 			if (!item.isAttachment()) {
   4216 				throw new Error("Item " + itemID + " is not an attachment");
   4217 			}
   4218 			
   4219 			if (item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
   4220 				this.loadURI(item.getField('url'), event);
   4221 				continue;
   4222 			}
   4223 			
   4224 			var path = yield item.getFilePathAsync();
   4225 			if (path) {
   4226 				let file = Zotero.File.pathToFile(path);
   4227 				
   4228 				Zotero.debug("Opening " + path);
   4229 				
   4230 				if(forceExternalViewer !== undefined) {
   4231 					var externalViewer = forceExternalViewer;
   4232 				} else {
   4233 					var mimeType = yield Zotero.MIME.getMIMETypeFromFile(file);
   4234 					
   4235 					//var mimeType = attachment.attachmentMIMEType;
   4236 					// TODO: update DB with new info if changed?
   4237 					
   4238 					var ext = Zotero.File.getExtension(file);
   4239 					var externalViewer = Zotero.isStandalone || (!Zotero.MIME.hasNativeHandler(mimeType, ext) &&
   4240 						(!Zotero.MIME.hasInternalHandler(mimeType, ext) || Zotero.Prefs.get('launchNonNativeFiles')));
   4241 				}
   4242 				
   4243 				if (!externalViewer) {
   4244 					let url = Services.io.newFileURI(file).spec;
   4245 					this.loadURI(url, event);
   4246 				}
   4247 				else {
   4248 					Zotero.Notifier.trigger('open', 'file', itemID);
   4249 					
   4250 					// Custom PDF handler
   4251 					if (item.attachmentContentType === 'application/pdf') {
   4252 						let pdfHandler  = Zotero.Prefs.get("fileHandler.pdf");
   4253 						if (pdfHandler) {
   4254 							if (yield OS.File.exists(pdfHandler)) {
   4255 								Zotero.launchFileWithApplication(file.path, pdfHandler);
   4256 								continue;
   4257 							}
   4258 							else {
   4259 								Zotero.logError(`${pdfHandler} not found -- launching file normally`);
   4260 							}
   4261 						}
   4262 					}
   4263 					
   4264 					Zotero.launchFile(file);
   4265 				}
   4266 			}
   4267 			else {
   4268 				if (!item.isImportedAttachment()
   4269 						|| !Zotero.Sync.Storage.Local.getEnabledForLibrary(item.libraryID)) {
   4270 					this.showAttachmentNotFoundDialog(itemID, noLocateOnMissing);
   4271 					return;
   4272 				}
   4273 				
   4274 				try {
   4275 					yield Zotero.Sync.Runner.downloadFile(item);
   4276 				}
   4277 				catch (e) {
   4278 					// TODO: show error somewhere else
   4279 					Zotero.debug(e, 1);
   4280 					ZoteroPane_Local.syncAlert(e);
   4281 					return;
   4282 				}
   4283 				
   4284 				if (!(yield item.getFilePathAsync())) {
   4285 					ZoteroPane_Local.showAttachmentNotFoundDialog(item.id, noLocateOnMissing, true);
   4286 					return;
   4287 				}
   4288 				
   4289 				// check if unchanged?
   4290 				// maybe not necessary, since we'll get an error if there's an error
   4291 				
   4292 				Zotero.Notifier.trigger('redraw', 'item', []);
   4293 				// Retry after download
   4294 				i--;
   4295 			}
   4296 		}
   4297 	}));
   4298 	
   4299 	
   4300 	/**
   4301 	 * @deprecated
   4302 	 */
   4303 	this.launchFile = function (file) {
   4304 		Zotero.debug("ZoteroPane.launchFile() is deprecated -- use Zotero.launchFile()", 2);
   4305 		Zotero.launchFile(file);
   4306 	}
   4307 	
   4308 	
   4309 	/**
   4310 	 * @deprecated
   4311 	 */
   4312 	this.launchURL = function (url) {
   4313 		Zotero.debug("ZoteroPane.launchURL() is deprecated -- use Zotero.launchURL()", 2);
   4314 		return Zotero.launchURL(url);
   4315 	}
   4316 	
   4317 	
   4318 	function viewSelectedAttachment(event, noLocateOnMissing)
   4319 	{
   4320 		if (this.itemsView && this.itemsView.selection.count == 1) {
   4321 			this.viewAttachment(this.getSelectedItems(true)[0], event, noLocateOnMissing);
   4322 		}
   4323 	}
   4324 	
   4325 	
   4326 	this.showAttachmentInFilesystem = Zotero.Promise.coroutine(function* (itemID, noLocateOnMissing) {
   4327 		var attachment = yield Zotero.Items.getAsync(itemID)
   4328 		if (attachment.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
   4329 			var path = yield attachment.getFilePathAsync();
   4330 			if (path) {
   4331 				let file = Zotero.File.pathToFile(path);
   4332 				try {
   4333 					Zotero.debug("Revealing " + file.path);
   4334 					file.reveal();
   4335 				}
   4336 				catch (e) {
   4337 					// On platforms that don't support nsILocalFile.reveal() (e.g. Linux),
   4338 					// launch the parent directory
   4339 					var parent = file.parent.QueryInterface(Components.interfaces.nsILocalFile);
   4340 					Zotero.launchFile(parent);
   4341 				}
   4342 				Zotero.Notifier.trigger('open', 'file', attachment.id);
   4343 			}
   4344 			else {
   4345 				this.showAttachmentNotFoundDialog(attachment.id, noLocateOnMissing)
   4346 			}
   4347 		}
   4348 	});
   4349 	
   4350 	
   4351 	this.showPublicationsWizard = function (items) {
   4352 		var io = {
   4353 			hasFiles: false,
   4354 			hasNotes: false,
   4355 			hasRights: null // 'all', 'some', or 'none'
   4356 		};
   4357 		var allItemsHaveRights = true;
   4358 		var noItemsHaveRights = true;
   4359 		// Determine whether any/all items have files, notes, or Rights values
   4360 		for (let i = 0; i < items.length; i++) {
   4361 			let item = items[i];
   4362 			
   4363 			// Files
   4364 			if (!io.hasFiles && item.numAttachments()) {
   4365 				let attachmentIDs = item.getAttachments();
   4366 				io.hasFiles = Zotero.Items.get(attachmentIDs).some(
   4367 					attachment => attachment.isFileAttachment()
   4368 				);
   4369 			}
   4370 			// Notes
   4371 			if (!io.hasNotes && item.numNotes()) {
   4372 				io.hasNotes = true;
   4373 			}
   4374 			// Rights
   4375 			if (item.getField('rights')) {
   4376 				noItemsHaveRights = false;
   4377 			}
   4378 			else {
   4379 				allItemsHaveRights = false;
   4380 			}
   4381 		}
   4382 		io.hasRights = allItemsHaveRights ? 'all' : (noItemsHaveRights ? 'none' : 'some');
   4383 		window.openDialog('chrome://zotero/content/publicationsDialog.xul','','chrome,modal', io);
   4384 		return io.license ? io : false;
   4385 	};
   4386 	
   4387 	
   4388 	/**
   4389 	 * Test if the user can edit the currently selected view
   4390 	 *
   4391 	 * @param	{Integer}	[row]
   4392 	 *
   4393 	 * @return	{Boolean}		TRUE if user can edit, FALSE if not
   4394 	 */
   4395 	this.canEdit = function (row) {
   4396 		// Currently selected row
   4397 		if (row === undefined) {
   4398 			row = this.collectionsView.selection.currentIndex;
   4399 		}
   4400 		
   4401 		var collectionTreeRow = this.collectionsView.getRow(row);
   4402 		return collectionTreeRow.editable;
   4403 	}
   4404 	
   4405 	
   4406 	/**
   4407 	 * Test if the user can edit the parent library of the selected view
   4408 	 *
   4409 	 * @param	{Integer}	[row]
   4410 	 * @return	{Boolean}		TRUE if user can edit, FALSE if not
   4411 	 */
   4412 	this.canEditLibrary = function (row) {
   4413 		// Currently selected row
   4414 		if (row === undefined) {
   4415 			row = this.collectionsView.selection.currentIndex;
   4416 		}
   4417 		
   4418 		var collectionTreeRow = this.collectionsView.getRow(row);
   4419 		return Zotero.Libraries.get(collectionTreeRow.ref.libraryID).editable;
   4420 	}
   4421 	
   4422 	
   4423 	/**
   4424 	 * Test if the user can edit the currently selected library/collection
   4425 	 *
   4426 	 * @param	{Integer}	[row]
   4427 	 *
   4428 	 * @return	{Boolean}		TRUE if user can edit, FALSE if not
   4429 	 */
   4430 	this.canEditFiles = function (row) {
   4431 		// Currently selected row
   4432 		if (row === undefined) {
   4433 			row = this.collectionsView.selection.currentIndex;
   4434 		}
   4435 		
   4436 		var collectionTreeRow = this.collectionsView.getRow(row);
   4437 		return collectionTreeRow.filesEditable;
   4438 	}
   4439 	
   4440 	
   4441 	this.displayCannotEditLibraryMessage = function () {
   4442 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   4443 								.getService(Components.interfaces.nsIPromptService);
   4444 		ps.alert(null, "", Zotero.getString('save.error.cannotMakeChangesToCollection'));
   4445 	}
   4446 	
   4447 	
   4448 	this.displayCannotEditLibraryFilesMessage = function () {
   4449 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   4450 								.getService(Components.interfaces.nsIPromptService);
   4451 		ps.alert(null, "", Zotero.getString('save.error.cannotAddFilesToCollection'));
   4452 	}
   4453 	
   4454 	
   4455 	this.displayCannotAddToMyPublicationsMessage = function () {
   4456 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   4457 								.getService(Components.interfaces.nsIPromptService);
   4458 		ps.alert(null, "", Zotero.getString('save.error.cannotAddToMyPublications'));
   4459 	}
   4460 	
   4461 	
   4462 	// TODO: Figure out a functioning way to get the original path and just copy the real file
   4463 	this.displayCannotAddShortcutMessage = function (path) {
   4464 		Zotero.alert(
   4465 			null,
   4466 			Zotero.getString("general.error"),
   4467 			Zotero.getString("file.error.cannotAddShortcut") + (path ? "\n\n" + path : "")
   4468 		);
   4469 	}
   4470 	
   4471 	
   4472 	this.showAttachmentNotFoundDialog = function (itemID, noLocate, notOnServer) {
   4473 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
   4474 				createInstance(Components.interfaces.nsIPromptService);
   4475 		
   4476 		var title = Zotero.getString('pane.item.attachments.fileNotFound.title');
   4477 		var text = Zotero.getString('pane.item.attachments.fileNotFound.text1') + "\n\n"
   4478 			+ Zotero.getString(
   4479 				'pane.item.attachments.fileNotFound.text2' + (notOnServer ? '.notOnServer' : ''),
   4480 				[ZOTERO_CONFIG.CLIENT_NAME, ZOTERO_CONFIG.DOMAIN_NAME]
   4481 			);
   4482 		var supportURL = Zotero.getString('pane.item.attachments.fileNotFound.supportURL');
   4483 		
   4484 		// Don't show Locate button
   4485 		if (noLocate) {
   4486 			let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_OK)
   4487 				+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
   4488 			let index = ps.confirmEx(null,
   4489 				title,
   4490 				text,
   4491 				buttonFlags,
   4492 				null,
   4493 				Zotero.getString('general.moreInformation'),
   4494 				null, null, {}
   4495 			);
   4496 			if (index == 1) {
   4497 				this.loadURI(supportURL, { metaKey: true, shiftKey: true });
   4498 			}
   4499 			return;
   4500 		}
   4501 		
   4502 		var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
   4503 			+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
   4504 			+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING);
   4505 		var index = ps.confirmEx(null,
   4506 			title,
   4507 			text,
   4508 			buttonFlags,
   4509 			Zotero.getString('general.locate'),
   4510 			null,
   4511 			Zotero.getString('general.moreInformation'), null, {}
   4512 		);
   4513 		
   4514 		if (index == 0) {
   4515 			this.relinkAttachment(itemID);
   4516 		}
   4517 		else if (index == 2) {
   4518 			this.loadURI(supportURL, { metaKey: true, shiftKey: true });
   4519 		}
   4520 	}
   4521 	
   4522 	
   4523 	this.syncAlert = function (e) {
   4524 		e = Zotero.Sync.Runner.parseError(e);
   4525 		
   4526 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   4527 					.getService(Components.interfaces.nsIPromptService);
   4528 		var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK
   4529 							+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING;
   4530 		
   4531 		// Warning
   4532 		if (e.errorType == 'warning') {
   4533 			var title = Zotero.getString('general.warning');
   4534 			
   4535 			// If secondary button not specified, just use an alert
   4536 			if (e.buttonText) {
   4537 				var buttonText = e.buttonText;
   4538 			}
   4539 			else {
   4540 				ps.alert(null, title, e.message);
   4541 				return;
   4542 			}
   4543 			
   4544 			var index = ps.confirmEx(
   4545 				null,
   4546 				title,
   4547 				e.message,
   4548 				buttonFlags,
   4549 				"",
   4550 				buttonText,
   4551 				"", null, {}
   4552 			);
   4553 			
   4554 			if (index == 1) {
   4555 				setTimeout(function () { buttonCallback(); }, 1);
   4556 			}
   4557 		}
   4558 		// Error
   4559 		else if (e.errorType == 'error') {
   4560 			var title = Zotero.getString('general.error');
   4561 			
   4562 			// If secondary button is explicitly null, just use an alert
   4563 			if (buttonText === null) {
   4564 				ps.alert(null, title, e.message);
   4565 				return;
   4566 			}
   4567 			
   4568 			if (typeof buttonText == 'undefined') {
   4569 				var buttonText = Zotero.getString('errorReport.reportError');
   4570 				var buttonCallback = function () {
   4571 					ZoteroPane.reportErrors();
   4572 				};
   4573 			}
   4574 			else {
   4575 				var buttonText = e.buttonText;
   4576 				var buttonCallback = e.buttonCallback;
   4577 			}
   4578 			
   4579 			var index = ps.confirmEx(
   4580 				null,
   4581 				title,
   4582 				e.message,
   4583 				buttonFlags,
   4584 				"",
   4585 				buttonText,
   4586 				"", null, {}
   4587 			);
   4588 			
   4589 			if (index == 1) {
   4590 				setTimeout(function () { buttonCallback(); }, 1);
   4591 			}
   4592 		}
   4593 		// Upgrade
   4594 		else if (e.errorType == 'upgrade') {
   4595 			ps.alert(null, "", e.message);
   4596 		}
   4597 	};
   4598 	
   4599 	
   4600 	this.recognizeSelected = function() {
   4601 		Zotero.RecognizePDF.recognizeItems(ZoteroPane.getSelectedItems());
   4602 		Zotero_RecognizePDF_Dialog.open();
   4603 	};
   4604 	
   4605 	
   4606 	this.unrecognizeSelected = async function () {
   4607 		var items = ZoteroPane.getSelectedItems();
   4608 		for (let item of items) {
   4609 			await Zotero.RecognizePDF.unrecognize(item);
   4610 		}
   4611 	};
   4612 	
   4613 	
   4614 	this.reportMetadataForSelected = async function () {
   4615 		let items = ZoteroPane.getSelectedItems();
   4616 		if(!items.length) return;
   4617 		
   4618 		let input = {value: ''};
   4619 		Services.prompt.prompt(
   4620 			null,
   4621 			Zotero.getString('recognizePDF.reportMetadata'),
   4622 			Zotero.getString('general.describeProblem'),
   4623 			input, null, {}
   4624 		);
   4625 		
   4626 		try {
   4627 			await Zotero.RecognizePDF.report(items[0], input.value);
   4628 			Zotero.alert(
   4629 				window,
   4630 				Zotero.getString('general.submitted'),
   4631 				Zotero.getString('general.thanksForHelpingImprove', Zotero.clientName)
   4632 			);
   4633 		}
   4634 		catch (e) {
   4635 			Zotero.logError(e);
   4636 			Zotero.alert(
   4637 				window,
   4638 				Zotero.getString('general.error'),
   4639 				Zotero.getString('general.invalidResponseServer')
   4640 			);
   4641 		}
   4642 	};
   4643 	
   4644 	
   4645 	this.createParentItemsFromSelected = Zotero.Promise.coroutine(function* () {
   4646 		if (!this.canEdit()) {
   4647 			this.displayCannotEditLibraryMessage();
   4648 			return;
   4649 		}
   4650 		
   4651 		var items = this.getSelectedItems();
   4652 		for (var i=0; i<items.length; i++) {
   4653 			var item = items[i];
   4654 			if (!item.isTopLevelItem() || item.isRegularItem()) {
   4655 				throw('Item ' + itemID + ' is not a top-level attachment or note in ZoteroPane_Local.createParentItemsFromSelected()');
   4656 			}
   4657 			
   4658 			yield Zotero.DB.executeTransaction(function* () {
   4659 				// TODO: remove once there are no top-level web attachments
   4660 				if (item.isWebAttachment()) {
   4661 					var parent = new Zotero.Item('webpage');
   4662 				}
   4663 				else {
   4664 					var parent = new Zotero.Item('document');
   4665 				}
   4666 				parent.libraryID = item.libraryID;
   4667 				parent.setField('title', item.getField('title'));
   4668 				if (item.isWebAttachment()) {
   4669 					parent.setField('accessDate', item.getField('accessDate'));
   4670 					parent.setField('url', item.getField('url'));
   4671 				}
   4672 				var itemID = yield parent.save();
   4673 				item.parentID = itemID;
   4674 				yield item.save();
   4675 			});
   4676 		}
   4677 	});
   4678 	
   4679 	
   4680 	this.renameSelectedAttachmentsFromParents = Zotero.Promise.coroutine(function* () {
   4681 		// TEMP: fix
   4682 		
   4683 		if (!this.canEdit()) {
   4684 			this.displayCannotEditLibraryMessage();
   4685 			return;
   4686 		}
   4687 		
   4688 		var items = this.getSelectedItems();
   4689 		
   4690 		for (var i=0; i<items.length; i++) {
   4691 			var item = items[i];
   4692 			
   4693 			if (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
   4694 				throw('Item ' + itemID + ' is not a child file attachment in ZoteroPane_Local.renameAttachmentFromParent()');
   4695 			}
   4696 			
   4697 			var file = item.getFile();
   4698 			if (!file) {
   4699 				continue;
   4700 			}
   4701 			
   4702 			let parentItemID = item.parentItemID;
   4703 			let parentItem = yield Zotero.Items.getAsync(parentItemID);
   4704 			var newName = Zotero.Attachments.getFileBaseNameFromItem(parentItem);
   4705 			
   4706 			var ext = file.leafName.match(/\.[^\.]+$/);
   4707 			if (ext) {
   4708 				newName = newName + ext;
   4709 			}
   4710 			
   4711 			var renamed = yield item.renameAttachmentFile(newName, false, true);
   4712 			if (renamed !== true) {
   4713 				Zotero.debug("Could not rename file (" + renamed + ")");
   4714 				continue;
   4715 			}
   4716 			
   4717 			item.setField('title', newName);
   4718 			yield item.saveTx();
   4719 		}
   4720 		
   4721 		return true;
   4722 	});
   4723 	
   4724 	
   4725 	this.relinkAttachment = Zotero.Promise.coroutine(function* (itemID) {
   4726 		if (!this.canEdit()) {
   4727 			this.displayCannotEditLibraryMessage();
   4728 			return;
   4729 		}
   4730 		
   4731 		var item = Zotero.Items.get(itemID);
   4732 		if (!item) {
   4733 			throw new Error('Item ' + itemID + ' not found in ZoteroPane_Local.relinkAttachment()');
   4734 		}
   4735 		
   4736 		while (true) {
   4737 			var nsIFilePicker = Components.interfaces.nsIFilePicker;
   4738 			var fp = Components.classes["@mozilla.org/filepicker;1"]
   4739 						.createInstance(nsIFilePicker);
   4740 			fp.init(window, Zotero.getString('pane.item.attachments.select'), nsIFilePicker.modeOpen);
   4741 			
   4742 			var file = item.getFilePath();
   4743 			if (!file) {
   4744 				Zotero.debug("Invalid path", 2);
   4745 				break;
   4746 			}
   4747 			
   4748 			var dir = yield Zotero.File.getClosestDirectory(file);
   4749 			if (dir) {
   4750 				fp.displayDirectory = Zotero.File.pathToFile(dir);
   4751 			}
   4752 			
   4753 			fp.appendFilters(Components.interfaces.nsIFilePicker.filterAll);
   4754 			
   4755 			if (fp.show() == nsIFilePicker.returnOK) {
   4756 				let file = fp.file;
   4757 				file.QueryInterface(Components.interfaces.nsILocalFile);
   4758 				
   4759 				// Disallow hidden files
   4760 				// TODO: Display a message
   4761 				if (file.leafName.startsWith('.')) {
   4762 					continue;
   4763 				}
   4764 				
   4765 				// Disallow Windows shortcuts
   4766 				if (file.leafName.endsWith(".lnk")) {
   4767 					this.displayCannotAddShortcutMessage(file.path);
   4768 					continue;
   4769 				}
   4770 				
   4771 				yield item.relinkAttachmentFile(file.path);
   4772 				break;
   4773 			}
   4774 			
   4775 			break;
   4776 		}
   4777 	});
   4778 	
   4779 	
   4780 	this.updateReadLabel = function () {
   4781 		var items = this.getSelectedItems();
   4782 		var isUnread = false;
   4783 		for (let item of items) {
   4784 			if (!item.isRead) {
   4785 				isUnread = true;
   4786 				break;
   4787 			}
   4788 		}
   4789 		ZoteroItemPane.setReadLabel(!isUnread);
   4790 	};
   4791 	
   4792 	
   4793 	var itemReadPromise;
   4794 	this.startItemReadTimeout = function (feedItemID) {
   4795 		if (itemReadPromise) {
   4796 			itemReadPromise.cancel();
   4797 		}
   4798 		
   4799 		const FEED_READ_TIMEOUT = 1000;
   4800 		
   4801 		itemReadPromise = Zotero.Promise.delay(FEED_READ_TIMEOUT)
   4802 		.then(async function () {
   4803 			itemReadPromise = null;
   4804 			
   4805 			// Check to make sure we're still on the same item
   4806 			var items = this.getSelectedItems();
   4807 			if (items.length != 1 || items[0].id != feedItemID) {
   4808 								Zotero.debug(items.length);
   4809 				Zotero.debug(items[0].id);
   4810 				Zotero.debug(feedItemID);
   4811 
   4812 				return;
   4813 			}
   4814 			var feedItem = items[0];
   4815 			if (!(feedItem instanceof Zotero.FeedItem)) {
   4816 				throw new Zotero.Promise.CancellationError('Not a FeedItem');
   4817 			}
   4818 			if (feedItem.isRead) {
   4819 				return;
   4820 			}
   4821 			
   4822 			await feedItem.toggleRead(true);
   4823 			ZoteroItemPane.setReadLabel(true);
   4824 		}.bind(this))
   4825 		.catch(function (e) {
   4826 			if (e instanceof Zotero.Promise.CancellationError) {
   4827 				Zotero.debug(e.message);
   4828 				return;
   4829 			}
   4830 			Zotero.logError(e);
   4831 		});
   4832 	}
   4833 	
   4834 	
   4835 	function reportErrors() {
   4836 		var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
   4837 				   .getService(Components.interfaces.nsIWindowWatcher);
   4838 		var data = {
   4839 			msg: Zotero.getString('errorReport.followingReportWillBeSubmitted'),
   4840 			errorData: Zotero.getErrors(true),
   4841 			askForSteps: true
   4842 		};
   4843 		var io = { wrappedJSObject: { Zotero: Zotero, data:  data } };
   4844 		var win = ww.openWindow(null, "chrome://zotero/content/errorReport.xul",
   4845 					"zotero-error-report", "chrome,centerscreen,modal", io);
   4846 	}
   4847 	
   4848 	/*
   4849 	 * Display an error message saying that an error has occurred and Firefox
   4850 	 * needs to be restarted.
   4851 	 *
   4852 	 * If |popup| is TRUE, display in popup progress window; otherwise, display
   4853 	 * as items pane message
   4854 	 */
   4855 	function displayErrorMessage(popup) {
   4856 		var reportErrorsStr = Zotero.getString('errorReport.reportErrors');
   4857 		var reportInstructions =
   4858 			Zotero.getString('errorReport.reportInstructions', reportErrorsStr)
   4859 		
   4860 		// Display as popup progress window
   4861 		if (popup) {
   4862 			var pw = new Zotero.ProgressWindow();
   4863 			pw.changeHeadline(Zotero.getString('general.errorHasOccurred'));
   4864 			var msg = Zotero.getString('general.pleaseRestart', Zotero.appName) + ' '
   4865 				+ reportInstructions;
   4866 			pw.addDescription(msg);
   4867 			pw.show();
   4868 			pw.startCloseTimer(8000);
   4869 		}
   4870 		// Display as items pane message
   4871 		else {
   4872 			var msg = Zotero.getString('general.errorHasOccurred') + ' '
   4873 				+ Zotero.getString('general.pleaseRestart', Zotero.appName) + '\n\n'
   4874 				+ reportInstructions;
   4875 			self.setItemsPaneMessage(msg, true);
   4876 		}
   4877 		Zotero.debug(msg, 1);
   4878 	}
   4879 	
   4880 	this.displayStartupError = function(asPaneMessage) {
   4881 		if (Zotero) {
   4882 			var errMsg = Zotero.startupError;
   4883 			var errFunc = Zotero.startupErrorHandler;
   4884 		}
   4885 		
   4886 		var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"]
   4887 			.getService(Components.interfaces.nsIStringBundleService);
   4888 		var src = 'chrome://zotero/locale/zotero.properties';
   4889 		var stringBundle = stringBundleService.createBundle(src);
   4890 		
   4891 		var title = stringBundle.GetStringFromName('general.error');
   4892 		if (!errMsg) {
   4893 			var errMsg = stringBundle.GetStringFromName('startupError');
   4894 		}
   4895 		
   4896 		if (errFunc) {
   4897 			errFunc();
   4898 		}
   4899 		else {
   4900 			// TODO: Add a better error page/window here with reporting
   4901 			// instructions
   4902 			// window.loadURI('chrome://zotero/content/error.xul');
   4903 			//if(asPaneMessage) {
   4904 			//	ZoteroPane_Local.setItemsPaneMessage(errMsg, true);
   4905 			//} else {
   4906 				var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   4907 										.getService(Components.interfaces.nsIPromptService);
   4908 				ps.alert(null, title, errMsg);
   4909 			//}
   4910 		}
   4911 	}
   4912 	
   4913 	/**
   4914 	 * Sets the layout to either a three-vertical-pane layout and a layout where itemsPane is above itemPane
   4915 	 */
   4916 	this.updateLayout = function() {
   4917 		var layoutSwitcher = document.getElementById("zotero-layout-switcher");
   4918 		var itemsSplitter = document.getElementById("zotero-items-splitter");
   4919 
   4920 		if(Zotero.Prefs.get("layout") === "stacked") { // itemsPane above itemPane
   4921 			layoutSwitcher.setAttribute("orient", "vertical");
   4922 			itemsSplitter.setAttribute("orient", "vertical");
   4923 		} else {  // three-vertical-pane
   4924 			layoutSwitcher.setAttribute("orient", "horizontal");
   4925 			itemsSplitter.setAttribute("orient", "horizontal");
   4926 		}
   4927 
   4928 		this.updateToolbarPosition();
   4929 	}
   4930 	/**
   4931 	 * Shows the Zotero pane, making it visible if it is not and switching to the appropriate tab
   4932 	 * if necessary.
   4933 	 */
   4934 	this.show = function() {
   4935 		if(window.ZoteroOverlay) {
   4936 			if (!this.isShowing()) {
   4937 				ZoteroOverlay.toggleDisplay();
   4938 			}
   4939 		}
   4940 	}
   4941 		
   4942 	/**
   4943 	 * Unserializes zotero-persist elements from preferences
   4944 	 */
   4945 	this.unserializePersist = function () {
   4946 		_unserialized = true;
   4947 		var serializedValues = Zotero.Prefs.get("pane.persist");
   4948 		if(!serializedValues) return;
   4949 		serializedValues = JSON.parse(serializedValues);
   4950 		for(var id in serializedValues) {
   4951 			var el = document.getElementById(id);
   4952 			if(!el) return;
   4953 			var elValues = serializedValues[id];
   4954 			for(var attr in elValues) {
   4955 				// TEMP: For now, ignore persisted collapsed state for item pane splitter
   4956 				if (el.id == 'zotero-items-splitter' && attr == 'state') continue;
   4957 				// And don't restore to min-width if splitter was collapsed
   4958 				if (el.id == 'zotero-item-pane' && attr == 'width' && elValues[attr] == 250
   4959 						&& 'zotero-items-splitter' in serializedValues
   4960 						&& serializedValues['zotero-items-splitter'].state == 'collapsed') {
   4961 					continue;
   4962 				}
   4963 				el.setAttribute(attr, elValues[attr]);
   4964 			}
   4965 		}
   4966 		
   4967 		if(this.itemsView) {
   4968 			// may not yet be initialized
   4969 			try {
   4970 				this.itemsView.sort();
   4971 			} catch(e) {};
   4972 		}
   4973 	};
   4974 
   4975 	/**
   4976 	 * Serializes zotero-persist elements to preferences
   4977 	 */
   4978 	this.serializePersist = function() {
   4979 		if(!_unserialized) return;
   4980 		var serializedValues = {};
   4981 		for (let el of document.getElementsByAttribute("zotero-persist", "*")) {
   4982 			if(!el.getAttribute) continue;
   4983 			var id = el.getAttribute("id");
   4984 			if(!id) continue;
   4985 			var elValues = {};
   4986 			for (let attr of el.getAttribute("zotero-persist").split(/[\s,]+/)) {
   4987 				if (el.hasAttribute(attr)) {
   4988 					elValues[attr] = el.getAttribute(attr);
   4989 				}
   4990 			}
   4991 			serializedValues[id] = elValues;
   4992 		}
   4993 		Zotero.Prefs.set("pane.persist", JSON.stringify(serializedValues));
   4994 	}
   4995 	
   4996 	
   4997 	this.updateWindow = function () {
   4998 		var zoteroPane = document.getElementById('zotero-pane');
   4999 		// Must match value in overlay.css
   5000 		var breakpoint = 1000;
   5001 		var className = `width-${breakpoint}`;
   5002 		if (window.innerWidth >= breakpoint) {
   5003 			zoteroPane.classList.add(className);
   5004 		}
   5005 		else {
   5006 			zoteroPane.classList.remove(className);
   5007 		}
   5008 	};
   5009 	
   5010 	
   5011 	/**
   5012 	 * Moves around the toolbar when the user moves around the pane
   5013 	 */
   5014 	this.updateToolbarPosition = function() {
   5015 		var paneStack = document.getElementById("zotero-pane-stack");
   5016 		if(paneStack.hidden) return;
   5017 
   5018 		var stackedLayout = Zotero.Prefs.get("layout") === "stacked";
   5019 
   5020 		var collectionsPane = document.getElementById("zotero-collections-pane");
   5021 		var collectionsToolbar = document.getElementById("zotero-collections-toolbar");
   5022 		var itemsPane = document.getElementById("zotero-items-pane");
   5023 		var itemsToolbar = document.getElementById("zotero-items-toolbar");
   5024 		var itemPane = document.getElementById("zotero-item-pane");
   5025 		var itemToolbar = document.getElementById("zotero-item-toolbar");
   5026 		
   5027 		collectionsToolbar.style.width = collectionsPane.boxObject.width + 'px';
   5028 		
   5029 		if (stackedLayout || itemPane.collapsed) {
   5030 		// The itemsToolbar and itemToolbar share the same space, and it seems best to use some flex attribute from right (because there might be other icons appearing or vanishing).
   5031 			itemsToolbar.setAttribute("flex", "1");
   5032 			itemToolbar.setAttribute("flex", "0");
   5033 		} else {
   5034 			var itemsToolbarWidth = itemsPane.boxObject.width;
   5035 
   5036 			if (collectionsPane.collapsed) {
   5037 				itemsToolbarWidth -= collectionsToolbar.boxObject.width;
   5038 			}
   5039 			// Not sure why this is necessary, but it keeps the search bar from overflowing into the
   5040 			// right-hand pane
   5041 			else {
   5042 				itemsToolbarWidth -= 8;
   5043 			}
   5044 			
   5045 			itemsToolbar.style.width = itemsToolbarWidth + "px";
   5046 			itemsToolbar.setAttribute("flex", "0");
   5047 			itemToolbar.setAttribute("flex", "1");
   5048 		}
   5049 		
   5050 		// Allow item pane to shrink to available height in stacked mode, but don't expand to be too
   5051 		// wide when there's no persisted width in non-stacked mode
   5052 		itemPane.setAttribute("flex", stackedLayout ? 1 : 0);
   5053 	}
   5054 	
   5055 	/**
   5056 	 * Opens the about dialog
   5057 	 */
   5058 	this.openAboutDialog = function() {
   5059 		window.openDialog('chrome://zotero/content/about.xul', 'about', 'chrome');
   5060 	}
   5061 	
   5062 	/**
   5063 	 * Adds or removes a function to be called when Zotero is reloaded by switching into or out of
   5064 	 * the connector
   5065 	 */
   5066 	this.addReloadListener = function(/** @param {Function} **/func) {
   5067 		if(_reloadFunctions.indexOf(func) === -1) _reloadFunctions.push(func);
   5068 	}
   5069 	
   5070 	/**
   5071 	 * Adds or removes a function to be called just before Zotero is reloaded by switching into or
   5072 	 * out of the connector
   5073 	 */
   5074 	this.addBeforeReloadListener = function(/** @param {Function} **/func) {
   5075 		if(_beforeReloadFunctions.indexOf(func) === -1) _beforeReloadFunctions.push(func);
   5076 	}
   5077 	
   5078 	/**
   5079 	 * Implements nsIObserver for Zotero reload
   5080 	 */
   5081 	var _reloadObserver = {	
   5082 		/**
   5083 		 * Called when Zotero is reloaded (i.e., if it is switched into or out of connector mode)
   5084 		 */
   5085 		"observe":function(aSubject, aTopic, aData) {
   5086 			if(aTopic == "zotero-reloaded") {
   5087 				Zotero.debug("Reloading Zotero pane");
   5088 				for (let func of _reloadFunctions) func(aData);
   5089 			} else if(aTopic == "zotero-before-reload") {
   5090 				Zotero.debug("Zotero pane caught before-reload event");
   5091 				for (let func of _beforeReloadFunctions) func(aData);
   5092 			}
   5093 		}
   5094 	};
   5095 }
   5096 
   5097 /**
   5098  * Keep track of which ZoteroPane was local (since ZoteroPane object might get swapped out for a
   5099  * tab's ZoteroPane)
   5100  */
   5101 var ZoteroPane_Local = ZoteroPane;