www

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

preferences_advanced.js (26484B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2006–2013 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 Components.utils.import("resource://gre/modules/Services.jsm");
     27 
     28 Zotero_Preferences.Advanced = {
     29 	_openURLResolvers: null,
     30 	
     31 	
     32 	init: function () {
     33 		Zotero_Preferences.Keys.init();
     34 		
     35 		// Show Memory Info button if the Error Console menu option is enabled
     36 		if (Zotero.Prefs.get('devtools.errorconsole.enabled', true)) {
     37 			document.getElementById('memory-info').hidden = false;
     38 		}
     39 		
     40 		this.onDataDirLoad();
     41 		this.refreshLocale();
     42 	},
     43 	
     44 	
     45 	updateTranslators: Zotero.Promise.coroutine(function* () {
     46 		var updated = yield Zotero.Schema.updateFromRepository(Zotero.Schema.REPO_UPDATE_MANUAL);
     47 		var button = document.getElementById('updateButton');
     48 		if (button) {
     49 			if (updated===-1) {
     50 				var label = Zotero.getString('zotero.preferences.update.upToDate');
     51 			}
     52 			else if (updated) {
     53 				var label = Zotero.getString('zotero.preferences.update.updated');
     54 			}
     55 			else {
     56 				var label = Zotero.getString('zotero.preferences.update.error');
     57 			}
     58 			button.setAttribute('label', label);
     59 			
     60 			if (updated && Zotero_Preferences.Cite) {
     61 				yield Zotero_Preferences.Cite.refreshStylesList();
     62 			}
     63 		}
     64 	}),
     65 	
     66 	
     67 	migrateDataDirectory: Zotero.Promise.coroutine(function* () {
     68 		var currentDir = Zotero.DataDirectory.dir;
     69 		var defaultDir = Zotero.DataDirectory.defaultDir;
     70 		if (currentDir == defaultDir) {
     71 			Zotero.debug("Already using default directory");
     72 			return;
     73 		}
     74 		
     75 		Components.utils.import("resource://zotero/config.js")
     76 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
     77 			.getService(Components.interfaces.nsIPromptService);
     78 		
     79 		// If there's a migration marker, point data directory back to the current location and remove
     80 		// it to trigger the migration again
     81 		var marker = OS.Path.join(defaultDir, Zotero.DataDirectory.MIGRATION_MARKER);
     82 		if (yield OS.File.exists(marker)) {
     83 			Zotero.Prefs.clear('dataDir');
     84 			Zotero.Prefs.clear('useDataDir');
     85 			yield OS.File.remove(marker);
     86 			try {
     87 				yield OS.File.remove(OS.Path.join(defaultDir, '.DS_Store'));
     88 			}
     89 			catch (e) {}
     90 		}
     91 		
     92 		// ~/Zotero exists and is non-empty
     93 		if ((yield OS.File.exists(defaultDir)) && !(yield Zotero.File.directoryIsEmpty(defaultDir))) {
     94 			let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
     95 				+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
     96 			let index = ps.confirmEx(
     97 				window,
     98 				Zotero.getString('general.error'),
     99 				Zotero.getString('zotero.preferences.advanced.migrateDataDir.directoryExists1', defaultDir)
    100 					+ "\n\n"
    101 					+ Zotero.getString('zotero.preferences.advanced.migrateDataDir.directoryExists2'),
    102 				buttonFlags,
    103 				Zotero.getString('general.showDirectory'),
    104 				null, null, null, {}
    105 			);
    106 			if (index == 0) {
    107 				yield Zotero.File.reveal(
    108 					// Windows opens the directory, which might be confusing here, so open parent instead
    109 					Zotero.isWin ? OS.Path.dirname(defaultDir) : defaultDir
    110 				);
    111 			}
    112 			return;
    113 		}
    114 		
    115 		var additionalText = '';
    116 		if (Zotero.isWin) {
    117 			try {
    118 				let numItems = yield Zotero.DB.valueQueryAsync(
    119 					"SELECT COUNT(*) FROM itemAttachments WHERE linkMode IN (?, ?)",
    120 					[Zotero.Attachments.LINK_MODE_IMPORTED_FILE, Zotero.Attachments.LINK_MODE_IMPORTED_URL]
    121 				);
    122 				if (numItems > 100) {
    123 					additionalText = '\n\n' + Zotero.getString(
    124 						'zotero.preferences.advanced.migrateDataDir.manualMigration',
    125 						[Zotero.appName, defaultDir, ZOTERO_CONFIG.CLIENT_NAME]
    126 					);
    127 				}
    128 			}
    129 			catch (e) {
    130 				Zotero.logError(e);
    131 			}
    132 		}
    133 		
    134 		// Prompt to restart
    135 		var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    136 					+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
    137 		var index = ps.confirmEx(window,
    138 			Zotero.getString('zotero.preferences.advanced.migrateDataDir.title'),
    139 			Zotero.getString(
    140 				'zotero.preferences.advanced.migrateDataDir.directoryWillBeMoved',
    141 				[ZOTERO_CONFIG.CLIENT_NAME, defaultDir]
    142 			) + '\n\n'
    143 			+ Zotero.getString(
    144 				'zotero.preferences.advanced.migrateDataDir.appMustBeRestarted', Zotero.appName
    145 			) + additionalText,
    146 			buttonFlags,
    147 			Zotero.getString('general.continue'),
    148 			null, null, null, {}
    149 		);
    150 		
    151 		if (index == 0) {
    152 			yield Zotero.DataDirectory.markForMigration(currentDir);
    153 			Zotero.Utilities.Internal.quitZotero(true);
    154 		}
    155 	}),
    156 	
    157 	
    158 	runIntegrityCheck: async function (button) {
    159 		button.disabled = true;
    160 		
    161 		try {
    162 			let ps = Services.prompt;
    163 			
    164 			var ok = await Zotero.DB.integrityCheck();
    165 			if (ok) {
    166 				ok = await Zotero.Schema.integrityCheck();
    167 				if (!ok) {
    168 					var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    169 						+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
    170 					var index = ps.confirmEx(window,
    171 						Zotero.getString('general.failed'),
    172 						Zotero.getString('db.integrityCheck.failed') + "\n\n" +
    173 							Zotero.getString('db.integrityCheck.repairAttempt') + " " +
    174 							Zotero.getString('db.integrityCheck.appRestartNeeded', Zotero.appName),
    175 						buttonFlags,
    176 						Zotero.getString('db.integrityCheck.fixAndRestart', Zotero.appName),
    177 						null, null, null, {}
    178 					);
    179 					
    180 					if (index == 0) {
    181 						// Safety first
    182 						await Zotero.DB.backupDatabase();
    183 						
    184 						// Fix the errors
    185 						await Zotero.Schema.integrityCheck(true);
    186 						
    187 						// And run the check again
    188 						ok = await Zotero.Schema.integrityCheck();
    189 						var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING);
    190 						if (ok) {
    191 							var str = 'success';
    192 							var msg = Zotero.getString('db.integrityCheck.errorsFixed');
    193 						}
    194 						else {
    195 							var str = 'failed';
    196 							var msg = Zotero.getString('db.integrityCheck.errorsNotFixed')
    197 										+ "\n\n" + Zotero.getString('db.integrityCheck.reportInForums');
    198 						}
    199 						
    200 						ps.confirmEx(window,
    201 							Zotero.getString('general.' + str),
    202 							msg,
    203 							buttonFlags,
    204 							Zotero.getString('general.restartApp', Zotero.appName),
    205 							null, null, null, {}
    206 						);
    207 						
    208 						var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
    209 								.getService(Components.interfaces.nsIAppStartup);
    210 						appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit
    211 							| Components.interfaces.nsIAppStartup.eRestart);
    212 					}
    213 					
    214 					return;
    215 				}
    216 				
    217 				try {
    218 					await Zotero.DB.vacuum();
    219 				}
    220 				catch (e) {
    221 					Zotero.logError(e);
    222 					ok = false;
    223 				}
    224 			}
    225 			var str = ok ? 'passed' : 'failed';
    226 			
    227 			ps.alert(window,
    228 				Zotero.getString('general.' + str),
    229 				Zotero.getString('db.integrityCheck.' + str)
    230 				+ (!ok ? "\n\n" + Zotero.getString('db.integrityCheck.dbRepairTool') : ''));
    231 		}
    232 		finally {
    233 			button.disabled = false;
    234 		}
    235 	},
    236 	
    237 	
    238 	resetTranslatorsAndStyles: function () {
    239 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    240 			.getService(Components.interfaces.nsIPromptService);
    241 		
    242 		var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    243 			+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
    244 		
    245 		var index = ps.confirmEx(null,
    246 			Zotero.getString('general.warning'),
    247 			Zotero.getString('zotero.preferences.advanced.resetTranslatorsAndStyles.changesLost'),
    248 			buttonFlags,
    249 			Zotero.getString('zotero.preferences.advanced.resetTranslatorsAndStyles'),
    250 			null, null, null, {});
    251 		
    252 		if (index == 0) {
    253 			Zotero.Schema.resetTranslatorsAndStyles()
    254 			.then(function () {
    255 				if (Zotero_Preferences.Export) {
    256 					Zotero_Preferences.Export.populateQuickCopyList();
    257 				}
    258 			});
    259 		}
    260 	},
    261 	
    262 	
    263 	resetTranslators: async function () {
    264 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    265 			.getService(Components.interfaces.nsIPromptService);
    266 		
    267 		var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    268 			+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
    269 		
    270 		var index = ps.confirmEx(null,
    271 			Zotero.getString('general.warning'),
    272 			Zotero.getString('zotero.preferences.advanced.resetTranslators.changesLost'),
    273 			buttonFlags,
    274 			Zotero.getString('zotero.preferences.advanced.resetTranslators'),
    275 			null, null, null, {});
    276 		
    277 		if (index == 0) {
    278 			let button = document.getElementById('reset-translators-button');
    279 			button.disabled = true;
    280 			try {
    281 				await Zotero.Schema.resetTranslators();
    282 				if (Zotero_Preferences.Export) {
    283 					Zotero_Preferences.Export.populateQuickCopyList();
    284 				}
    285 			}
    286 			finally {
    287 				button.disabled = false;
    288 			}
    289 		}
    290 	},
    291 	
    292 	
    293 	resetStyles: async function () {
    294 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    295 			.getService(Components.interfaces.nsIPromptService);
    296 		
    297 		var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    298 			+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
    299 		
    300 		var index = ps.confirmEx(null,
    301 			Zotero.getString('general.warning'),
    302 			Zotero.getString('zotero.preferences.advanced.resetStyles.changesLost'),
    303 			buttonFlags,
    304 			Zotero.getString('zotero.preferences.advanced.resetStyles'),
    305 			null, null, null, {});
    306 		
    307 		if (index == 0) {
    308 			let button = document.getElementById('reset-styles-button');
    309 			button.disabled = true;
    310 			try {
    311 				await Zotero.Schema.resetStyles()
    312 				if (Zotero_Preferences.Export) {
    313 					Zotero_Preferences.Export.populateQuickCopyList();
    314 				}
    315 			}
    316 			finally {
    317 				button.disabled = false;
    318 			}
    319 		}
    320 	},
    321 	
    322 	
    323 	onDataDirLoad: function () {
    324 		var useDataDir = Zotero.Prefs.get('useDataDir');
    325 		var dataDir = Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir');
    326 		var currentDir = Zotero.DataDirectory.dir;
    327 		var defaultDataDir = Zotero.DataDirectory.defaultDir;
    328 		
    329 		if (Zotero.forceDataDir) {
    330 			document.getElementById('command-line-data-dir-path').textContent = currentDir;
    331 			document.getElementById('command-line-data-dir').hidden = false;
    332 			document.getElementById('data-dir').hidden = true;
    333 		}
    334 		
    335 		// Change "Use profile directory" label to home directory location unless using profile dir
    336 		if (useDataDir || currentDir == defaultDataDir) {
    337 			document.getElementById('default-data-dir').setAttribute(
    338 				'label', Zotero.getString('dataDir.default', Zotero.DataDirectory.defaultDir)
    339 			);
    340 		}
    341 		
    342 		// Don't show custom data dir as in-use if set to the default
    343 		if (dataDir == defaultDataDir) {
    344 			useDataDir = false;
    345 		}
    346 		
    347 		document.getElementById('data-dir-path').setAttribute('disabled', !useDataDir);
    348 		document.getElementById('migrate-data-dir').setAttribute(
    349 			'hidden', !Zotero.DataDirectory.canMigrate()
    350 		);
    351 		
    352 		return useDataDir;
    353 	},
    354 	
    355 	
    356 	onDataDirUpdate: Zotero.Promise.coroutine(function* (event, forceNew) {
    357 		var radiogroup = document.getElementById('data-dir');
    358 		var newUseDataDir = radiogroup.selectedIndex == 1;
    359 		
    360 		if (!forceNew && newUseDataDir && !this._usingDefaultDataDir()) {
    361 			return;
    362 		}
    363 		
    364 		// This call shows a filepicker if needed, forces a restart if required, and does nothing if
    365 		// cancel was pressed or value hasn't changed
    366 		yield Zotero.DataDirectory.choose(
    367 			true,
    368 			!newUseDataDir,
    369 			() => Zotero_Preferences.openURL('https://zotero.org/support/zotero_data')
    370 		);
    371 		radiogroup.selectedIndex = this._usingDefaultDataDir() ? 0 : 1;
    372 	}),
    373 	
    374 	
    375 	chooseDataDir: function(event) {
    376 		document.getElementById('data-dir').selectedIndex = 1;
    377 		this.onDataDirUpdate(event, true);
    378 	},
    379 	
    380 	
    381 	getDataDirPath: function () {
    382 		// TEMP: lastDataDir can be removed once old persistent descriptors have been
    383 		// converted, which they are in getZoteroDirectory() in 5.0
    384 		var prefValue = Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir');
    385 		
    386 		// Don't show path if the default
    387 		if (prefValue == Zotero.DataDirectory.defaultDir) {
    388 			return '';
    389 		}
    390 		
    391 		return prefValue || '';
    392 	},
    393 	
    394 	
    395 	_usingDefaultDataDir: function () {
    396 		// Legacy profile directory location
    397 		if (!Zotero.Prefs.get('useDataDir')) {
    398 			return true;
    399 		}
    400 		
    401 		var dataDir = Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir');
    402 		// Default home directory location
    403 		if (dataDir == Zotero.DataDirectory.defaultDir) {
    404 			return true;
    405 		}
    406 		
    407 		return false;
    408 	},
    409 	
    410 	
    411 	populateOpenURLResolvers: function () {
    412 		var openURLMenu = document.getElementById('openURLMenu');
    413 		
    414 		this._openURLResolvers = Zotero.OpenURL.discoverResolvers();
    415 		var i = 0;
    416 		for (let r of this._openURLResolvers) {
    417 			openURLMenu.insertItemAt(i, r.name);
    418 			if (r.url == Zotero.Prefs.get('openURL.resolver') && r.version == Zotero.Prefs.get('openURL.version')) {
    419 				openURLMenu.selectedIndex = i;
    420 			}
    421 			i++;
    422 		}
    423 		
    424 		var button = document.getElementById('openURLSearchButton');
    425 		switch (this._openURLResolvers.length) {
    426 			case 0:
    427 				var num = 'zero';
    428 				break;
    429 			case 1:
    430 				var num = 'singular';
    431 				break;
    432 			default:
    433 				var num = 'plural';
    434 		}
    435 		
    436 		button.setAttribute('label', Zotero.getString('zotero.preferences.openurl.resolversFound.' + num, this._openURLResolvers.length));
    437 	},
    438 	
    439 	
    440 	onOpenURLSelected: function () {
    441 		var openURLServerField = document.getElementById('openURLServerField');
    442 		var openURLVersionMenu = document.getElementById('openURLVersionMenu');
    443 		var openURLMenu = document.getElementById('openURLMenu');
    444 		
    445 		if(openURLMenu.value == "custom")
    446 		{
    447 			openURLServerField.focus();
    448 		}
    449 		else
    450 		{
    451 			openURLServerField.value = this._openURLResolvers[openURLMenu.selectedIndex]['url'];
    452 			openURLVersionMenu.value = this._openURLResolvers[openURLMenu.selectedIndex]['version'];
    453 			Zotero.Prefs.set("openURL.resolver", this._openURLResolvers[openURLMenu.selectedIndex]['url']);
    454 			Zotero.Prefs.set("openURL.version", this._openURLResolvers[openURLMenu.selectedIndex]['version']);
    455 		}
    456 	},
    457 	
    458 	onOpenURLCustomized: function () {
    459 		document.getElementById('openURLMenu').value = "custom";
    460 	},
    461 	
    462 	
    463 	_getAutomaticLocaleMenuLabel: function () {
    464 		return Zotero.getString(
    465 			'zotero.preferences.locale.automaticWithLocale',
    466 			Zotero.Locale.availableLocales[Zotero.locale] || Zotero.locale
    467 		);
    468 	},
    469 	
    470 	
    471 	refreshLocale: function () {
    472 		var matchOS = Zotero.Prefs.get('intl.locale.matchOS', true);
    473 		var autoLocaleName, currentValue;
    474 		
    475 		// If matching OS, get the name of the current locale
    476 		if (matchOS) {
    477 			autoLocaleName = this._getAutomaticLocaleMenuLabel();
    478 			currentValue = 'automatic';
    479 		}
    480 		// Otherwise get the name of the locale specified in the pref
    481 		else {
    482 			let branch = Services.prefs.getBranch("");
    483 			let locale = branch.getComplexValue(
    484 				'general.useragent.locale', Components.interfaces.nsIPrefLocalizedString
    485 			);
    486 			autoLocaleName = Zotero.getString('zotero.preferences.locale.automatic');
    487 			currentValue = locale;
    488 		}
    489 		
    490 		// Populate menu
    491 		var menu = document.getElementById('locale-menu');
    492 		var menupopup = menu.firstChild;
    493 		menupopup.textContent = '';
    494 		// Show "Automatic (English)", "Automatic (Français)", etc.
    495 		menu.appendItem(autoLocaleName, 'automatic');
    496 		menu.menupopup.appendChild(document.createElement('menuseparator'));
    497 		// Add all available locales
    498 		for (let locale in Zotero.Locale.availableLocales) {
    499 			menu.appendItem(Zotero.Locale.availableLocales[locale], locale);
    500 		}
    501 		menu.value = currentValue;
    502 	},
    503 	
    504 	onLocaleChange: function () {
    505 		var menu = document.getElementById('locale-menu');
    506 		if (menu.value == 'automatic') {
    507 			// Changed if not already set to automatic (unless we have the automatic locale name,
    508 			// meaning we just switched away to the same manual locale and back to automatic)
    509 			var changed = !Zotero.Prefs.get('intl.locale.matchOS', true)
    510 				&& menu.label != this._getAutomaticLocaleMenuLabel();
    511 			Zotero.Prefs.set('intl.locale.matchOS', true, true);
    512 		}
    513 		else {
    514 			// Changed if moving to a locale other than the current one
    515 			var changed = Zotero.locale != menu.value
    516 			Zotero.Prefs.set('intl.locale.matchOS', false, true);
    517 			Zotero.Prefs.set('general.useragent.locale', menu.value, true);
    518 		}
    519 		
    520 		if (!changed) {
    521 			return;
    522 		}
    523 		
    524 		var ps = Services.prompt;
    525 		var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
    526 			+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING;
    527 		var index = ps.confirmEx(null,
    528 			Zotero.getString('general.restartRequired'),
    529 			Zotero.getString('general.restartRequiredForChange', Zotero.appName),
    530 			buttonFlags,
    531 			Zotero.getString('general.restartNow'),
    532 			Zotero.getString('general.restartLater'),
    533 			null, null, {});
    534 		
    535 		if (index == 0) {
    536 			Zotero.Utilities.Internal.quitZotero(true);
    537 		}
    538 	}
    539 };
    540 
    541 
    542 Zotero_Preferences.Attachment_Base_Directory = {
    543 	getPath: function () {
    544 		var oldPath = Zotero.Prefs.get('baseAttachmentPath');
    545 		if (oldPath) {
    546 			try {
    547 				return OS.Path.normalize(oldPath);
    548 			}
    549 			catch (e) {
    550 				Zotero.logError(e);
    551 				return false;
    552 			}
    553 		}
    554 	},
    555 	
    556 	
    557 	choosePath: Zotero.Promise.coroutine(function* () {
    558 		var oldPath = this.getPath();
    559 		
    560 		//Prompt user to choose new base path
    561 		if (oldPath) {
    562 			var oldPathFile = Zotero.File.pathToFile(oldPath);
    563 		}
    564 		var nsIFilePicker = Components.interfaces.nsIFilePicker;
    565 		var fp = Components.classes["@mozilla.org/filepicker;1"]
    566 					.createInstance(nsIFilePicker);
    567 		if (oldPathFile) {
    568 			fp.displayDirectory = oldPathFile;
    569 		}
    570 		fp.init(window, Zotero.getString('attachmentBasePath.selectDir'), nsIFilePicker.modeGetFolder);
    571 		fp.appendFilters(nsIFilePicker.filterAll);
    572 		if (fp.show() != nsIFilePicker.returnOK) {
    573 			return false;
    574 		}
    575 		var newPath = OS.Path.normalize(fp.file.path);
    576 		
    577 		if (oldPath && oldPath == newPath) {
    578 			Zotero.debug("Base directory hasn't changed");
    579 			return false;
    580 		}
    581 		
    582 		return this.changePath(newPath);
    583 	}),
    584 	
    585 	
    586 	changePath: Zotero.Promise.coroutine(function* (basePath) {
    587 		// Find all current attachments with relative attachment paths
    588 		var sql = "SELECT itemID FROM itemAttachments WHERE linkMode=? AND path LIKE ?";
    589 		var params = [
    590 			Zotero.Attachments.LINK_MODE_LINKED_FILE,
    591 			Zotero.Attachments.BASE_PATH_PLACEHOLDER + "%"
    592 		];
    593 		var oldRelativeAttachmentIDs = yield Zotero.DB.columnQueryAsync(sql, params);
    594 		
    595 		//Find all attachments on the new base path
    596 		var sql = "SELECT itemID FROM itemAttachments WHERE linkMode=?";
    597 		var params = [Zotero.Attachments.LINK_MODE_LINKED_FILE];
    598 		var allAttachments = yield Zotero.DB.columnQueryAsync(sql, params);
    599 		var newAttachmentPaths = {};
    600 		var numNewAttachments = 0;
    601 		var numOldAttachments = 0;
    602 		for (let i=0; i<allAttachments.length; i++) {
    603 			let attachmentID = allAttachments[i];
    604 			let attachmentPath;
    605 			let relPath = false
    606 			
    607 			try {
    608 				let attachment = yield Zotero.Items.getAsync(attachmentID);
    609 				// This will return FALSE for relative paths if base directory
    610 				// isn't currently set
    611 				attachmentPath = attachment.getFilePath();
    612 				// Get existing relative path
    613 				let storedPath = attachment.attachmentPath;
    614 				if (storedPath.startsWith(Zotero.Attachments.BASE_PATH_PLACEHOLDER)) {
    615 					relPath = storedPath.substr(Zotero.Attachments.BASE_PATH_PLACEHOLDER.length);
    616 				}
    617 			}
    618 			catch (e) {
    619 				// Don't deal with bad attachment paths. Just skip them.
    620 				Zotero.debug(e, 2);
    621 				continue;
    622 			}
    623 			
    624 			// If a file with the same relative path exists within the new base directory,
    625 			// don't touch the attachment, since it will continue to work
    626 			if (relPath) {
    627 				if (yield OS.File.exists(OS.Path.join(basePath, relPath))) {
    628 					numNewAttachments++;
    629 					continue;
    630 				}
    631 			}
    632 			
    633 			// Files within the new base directory need to be updated to use
    634 			// relative paths (or, if the new base directory is an ancestor or
    635 			// descendant of the old one, new relative paths)
    636 			if (attachmentPath && Zotero.File.directoryContains(basePath, attachmentPath)) {
    637 				newAttachmentPaths[attachmentID] = relPath ? attachmentPath : null;
    638 				numNewAttachments++;
    639 			}
    640 			// Existing relative attachments not within the new base directory
    641 			// will be converted to absolute paths
    642 			else if (relPath && this.getPath()) {
    643 				newAttachmentPaths[attachmentID] = attachmentPath;
    644 				numOldAttachments++;
    645 			}
    646 		}
    647 		
    648 		//Confirm change of the base path
    649 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    650 			.getService(Components.interfaces.nsIPromptService);
    651 		
    652 		var chooseStrPrefix = 'attachmentBasePath.chooseNewPath.';
    653 		var clearStrPrefix = 'attachmentBasePath.clearBasePath.';
    654 		var title = Zotero.getString(chooseStrPrefix + 'title');
    655 		var msg1 = Zotero.getString(chooseStrPrefix + 'message') + "\n\n", msg2 = "", msg3 = "";
    656 		switch (numNewAttachments) {
    657 			case 0:
    658 				break;
    659 			
    660 			case 1:
    661 				msg2 += Zotero.getString(chooseStrPrefix + 'existingAttachments.singular') + " ";
    662 				break;
    663 			
    664 			default:
    665 				msg2 += Zotero.getString(chooseStrPrefix + 'existingAttachments.plural', numNewAttachments) + " ";
    666 		}
    667 		
    668 		switch (numOldAttachments) {
    669 			case 0:
    670 				break;
    671 			
    672 			case 1:
    673 				msg3 += Zotero.getString(clearStrPrefix + 'existingAttachments.singular');
    674 				break;
    675 			
    676 			default:
    677 				msg3 += Zotero.getString(clearStrPrefix + 'existingAttachments.plural', numOldAttachments);
    678 		}
    679 		
    680 		
    681 		var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    682 			+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
    683 		var index = ps.confirmEx(
    684 			null,
    685 			title,
    686 			(msg1 + msg2 + msg3).trim(),
    687 			buttonFlags,
    688 			Zotero.getString(chooseStrPrefix + 'button'),
    689 			null,
    690 			null,
    691 			null,
    692 			{}
    693 		);
    694 		
    695 		if (index == 1) {
    696 			return false;
    697 		}
    698 		
    699 		// Set new data directory
    700 		Zotero.debug("Setting new base directory");
    701 		Zotero.Prefs.set('baseAttachmentPath', basePath);
    702 		Zotero.Prefs.set('saveRelativeAttachmentPath', true);
    703 		// Resave all attachments on base path (so that their paths become relative)
    704 		// and all other relative attachments (so that their paths become absolute)
    705 		yield Zotero.Utilities.Internal.forEachChunkAsync(
    706 			Object.keys(newAttachmentPaths),
    707 			100,
    708 			function (chunk) {
    709 				return Zotero.DB.executeTransaction(function* () {
    710 					for (let id of chunk) {
    711 						let attachment = Zotero.Items.get(id);
    712 						if (newAttachmentPaths[id]) {
    713 							attachment.attachmentPath = newAttachmentPaths[id];
    714 						}
    715 						else {
    716 							attachment.attachmentPath = attachment.getFilePath();
    717 						}
    718 						yield attachment.save({
    719 							skipDateModifiedUpdate: true
    720 						});
    721 					}
    722 				})
    723 			}
    724 		);
    725 		
    726 		return true;
    727 	}),
    728 	
    729 	
    730 	clearPath: Zotero.Promise.coroutine(function* () {
    731 		// Find all current attachments with relative paths
    732 		var sql = "SELECT itemID FROM itemAttachments WHERE linkMode=? AND path LIKE ?";
    733 		var params = [
    734 			Zotero.Attachments.LINK_MODE_LINKED_FILE,
    735 			Zotero.Attachments.BASE_PATH_PLACEHOLDER + "%"
    736 		];
    737 		var relativeAttachmentIDs = yield Zotero.DB.columnQueryAsync(sql, params);
    738 		
    739 		// Prompt for confirmation
    740 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    741 			.getService(Components.interfaces.nsIPromptService);
    742 		
    743 		var strPrefix = 'attachmentBasePath.clearBasePath.';
    744 		var title = Zotero.getString(strPrefix + 'title');
    745 		var msg = Zotero.getString(strPrefix + 'message');
    746 		switch (relativeAttachmentIDs.length) {
    747 			case 0:
    748 				break;
    749 			
    750 			case 1:
    751 				msg += "\n\n" + Zotero.getString(strPrefix + 'existingAttachments.singular');
    752 				break;
    753 			
    754 			default:
    755 				msg += "\n\n" + Zotero.getString(strPrefix + 'existingAttachments.plural',
    756 					relativeAttachmentIDs.length);
    757 		}
    758 		
    759 		var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    760 			+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
    761 		var index = ps.confirmEx(
    762 			window,
    763 			title,
    764 			msg,
    765 			buttonFlags,
    766 			Zotero.getString(strPrefix + 'button'),
    767 			null,
    768 			null,
    769 			null,
    770 			{}
    771 		);
    772 		
    773 		if (index == 1) {
    774 			return false;
    775 		}
    776 		
    777 		// Disable relative path saving and then resave all relative
    778 		// attachments so that their absolute paths are stored
    779 		Zotero.debug('Clearing base directory');
    780 		Zotero.Prefs.set('saveRelativeAttachmentPath', false);
    781 		
    782 		yield Zotero.Utilities.Internal.forEachChunkAsync(
    783 			relativeAttachmentIDs,
    784 			100,
    785 			function (chunk) {
    786 				return Zotero.DB.executeTransaction(function* () {
    787 					for (let id of chunk) {
    788 						let attachment = yield Zotero.Items.getAsync(id);
    789 						attachment.attachmentPath = attachment.getFilePath();
    790 						yield attachment.save({
    791 							skipDateModifiedUpdate: true
    792 						});
    793 					}
    794 				}.bind(this));
    795 			}.bind(this)
    796 		);
    797 		
    798 		Zotero.Prefs.set('baseAttachmentPath', '');
    799 	}),
    800 	
    801 	
    802 	updateUI: Zotero.Promise.coroutine(function* () {
    803 		var filefield = document.getElementById('baseAttachmentPath');
    804 		var path = Zotero.Prefs.get('baseAttachmentPath');
    805 		Components.utils.import("resource://gre/modules/osfile.jsm");
    806 		if (yield OS.File.exists(path)) {
    807 			filefield.file = Zotero.File.pathToFile(path);
    808 			filefield.label = path;
    809 		}
    810 		else {
    811 			filefield.label = '';
    812 		}
    813 		document.getElementById('resetBasePath').disabled = !path;
    814 	})
    815 };
    816 
    817 
    818 Zotero_Preferences.Keys = {
    819 	init: function () {
    820 		var rows = document.getElementById('zotero-prefpane-advanced-keys-tab').getElementsByTagName('row');
    821 		for (var i=0; i<rows.length; i++) {
    822 			// Display the appropriate modifier keys for the platform
    823 			let label = rows[i].firstChild.nextSibling;
    824 			if (label.className == 'modifier') {
    825 				label.value = Zotero.isMac ? Zotero.getString('general.keys.cmdShift') : Zotero.getString('general.keys.ctrlShift');
    826 			}
    827 		}
    828 		
    829 		var textboxes = document.getElementById('zotero-keys-rows').getElementsByTagName('textbox');
    830 		for (let i=0; i<textboxes.length; i++) {
    831 			let textbox = textboxes[i];
    832 			textbox.value = textbox.value.toUpperCase();
    833 			// .value takes care of the initial value, and this takes care of direct pref changes
    834 			// while the window is open
    835 			textbox.setAttribute('onsyncfrompreference', 'return Zotero_Preferences.Keys.capitalizePref(this.id)');
    836 			textbox.setAttribute('oninput', 'this.value = this.value.toUpperCase()');
    837 		}
    838 	},
    839 	
    840 	
    841 	capitalizePref: function (id) {
    842 		var elem = document.getElementById(id);
    843 		var pref = document.getElementById(elem.getAttribute('preference'));
    844 		if (pref.value) {
    845 			return pref.value.toUpperCase();
    846 		}
    847 	}
    848 };