search.js (48183B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* import-globals-from extensionControlled.js */ 6 /* import-globals-from preferences.js */ 7 8 const lazy = {}; 9 10 ChromeUtils.defineESModuleGetters(lazy, { 11 AddonSearchEngine: 12 "moz-src:///toolkit/components/search/AddonSearchEngine.sys.mjs", 13 CustomizableUI: 14 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 15 QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs", 16 SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs", 17 SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", 18 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 19 UserSearchEngine: 20 "moz-src:///toolkit/components/search/UserSearchEngine.sys.mjs", 21 }); 22 23 Preferences.addAll([ 24 { id: "browser.search.suggest.enabled", type: "bool" }, 25 { id: "browser.urlbar.suggest.searches", type: "bool" }, 26 { id: "browser.search.suggest.enabled.private", type: "bool" }, 27 { id: "browser.urlbar.showSearchSuggestionsFirst", type: "bool" }, 28 { id: "browser.urlbar.showSearchTerms.enabled", type: "bool" }, 29 { id: "browser.urlbar.showSearchTerms.featureGate", type: "bool" }, 30 { id: "browser.search.separatePrivateDefault", type: "bool" }, 31 { id: "browser.search.separatePrivateDefault.ui.enabled", type: "bool" }, 32 { id: "browser.urlbar.suggest.trending", type: "bool" }, 33 { id: "browser.urlbar.trending.featureGate", type: "bool" }, 34 { id: "browser.urlbar.recentsearches.featureGate", type: "bool" }, 35 { id: "browser.urlbar.suggest.recentsearches", type: "bool" }, 36 { id: "browser.urlbar.scotchBonnet.enableOverride", type: "bool" }, 37 38 // Suggest Section. 39 { id: "browser.urlbar.suggest.bookmark", type: "bool" }, 40 { id: "browser.urlbar.suggest.clipboard", type: "bool" }, 41 { id: "browser.urlbar.clipboard.featureGate", type: "bool" }, 42 { id: "browser.urlbar.suggest.history", type: "bool" }, 43 { id: "browser.urlbar.suggest.openpage", type: "bool" }, 44 { id: "browser.urlbar.suggest.topsites", type: "bool" }, 45 { id: "browser.urlbar.suggest.engines", type: "bool" }, 46 { id: "browser.urlbar.quickactions.showPrefs", type: "bool" }, 47 { id: "browser.urlbar.suggest.quickactions", type: "bool" }, 48 { id: "browser.urlbar.quicksuggest.settingsUi", type: "int" }, 49 { id: "browser.urlbar.quicksuggest.enabled", type: "bool" }, 50 { id: "browser.urlbar.suggest.quicksuggest.all", type: "bool" }, 51 { id: "browser.urlbar.suggest.quicksuggest.sponsored", type: "bool" }, 52 { id: "browser.urlbar.quicksuggest.online.enabled", type: "bool" }, 53 ]); 54 55 /** 56 * Generates the config needed to populate the dropdowns for the user's 57 * default search engine and separate private default search engine. 58 * 59 * @param {object} options 60 * Options for creating the config. 61 * @param {string} options.settingId 62 * The id for the particular setting. 63 * @param {() => Promise<nsISearchEngine>} options.getEngine 64 * The method used to get the engine from the Search Service. 65 * @param {(id: string) => Promise<void>} options.setEngine 66 * The method used to set a new engine. 67 * @returns {PreferencesSettingsConfig} 68 */ 69 function createSearchEngineConfig({ settingId, getEngine, setEngine }) { 70 return class extends Preferences.AsyncSetting { 71 static id = settingId; 72 ENGINE_MODIFIED = "browser-search-engine-modified"; 73 iconMap = new Map(); 74 75 /** @type {{options: PreferencesSettingsConfig[]}} */ 76 defaultGetControlConfig = { options: [] }; 77 78 async get() { 79 let engine = await getEngine(); 80 return engine.id; 81 } 82 83 async set(id) { 84 await setEngine(id); 85 } 86 87 async getControlConfig() { 88 let engines = await Services.search.getVisibleEngines(); 89 await Promise.allSettled(engines.map(e => this.loadEngineIcon(e))); 90 return { 91 options: engines.map(engine => ({ 92 value: engine.id, 93 iconSrc: this.getEngineIcon(engine), 94 controlAttrs: { 95 label: engine.name, 96 }, 97 })), 98 }; 99 } 100 101 getEngineIcon(engine) { 102 return this.iconMap.get(engine.id); 103 } 104 105 getPlaceholderIcon() { 106 return window.devicePixelRatio > 1 107 ? "chrome://browser/skin/search-engine-placeholder@2x.png" 108 : "chrome://browser/skin/search-engine-placeholder.png"; 109 } 110 111 async loadEngineIcon(engine) { 112 try { 113 let iconURL = await engine.getIconURL(); 114 let url = iconURL ?? this.getPlaceholderIcon(); 115 this.iconMap.set(engine.id, url); 116 return url; 117 } catch (error) { 118 console.warn(`Failed to load icon for engine ${engine.name}:`, error); 119 let placeholderIcon = this.getPlaceholderIcon(); 120 this.iconMap.set(engine.id, placeholderIcon); 121 return placeholderIcon; 122 } 123 } 124 125 setup() { 126 Services.obs.addObserver(this, this.ENGINE_MODIFIED); 127 return () => Services.obs.removeObserver(this, this.ENGINE_MODIFIED); 128 } 129 130 observe(subject, topic, data) { 131 if (topic == this.ENGINE_MODIFIED) { 132 let engine = subject.QueryInterface(Ci.nsISearchEngine); 133 134 // Clean up cache for removed engines. 135 if (data == "engine-removed") { 136 this.iconMap.delete(engine.id); 137 } 138 139 // Always emit change for any change that could affect the engine list 140 // or default. 141 this.emitChange(); 142 } 143 } 144 }; 145 } 146 147 Preferences.addSetting( 148 createSearchEngineConfig({ 149 settingId: "defaultEngineNormal", 150 getEngine: () => Services.search.getDefault(), 151 setEngine: id => 152 Services.search.setDefault( 153 Services.search.getEngineById(id), 154 Ci.nsISearchService.CHANGE_REASON_USER 155 ), 156 }) 157 ); 158 159 Preferences.addSetting({ 160 id: "scotchBonnetEnabled", 161 pref: "browser.urlbar.scotchBonnet.enableOverride", 162 }); 163 164 Preferences.addSetting({ 165 id: "showSearchTermsFeatureGate", 166 pref: "browser.urlbar.showSearchTerms.featureGate", 167 }); 168 169 Preferences.addSetting({ 170 id: "searchShowSearchTermCheckbox", 171 pref: "browser.urlbar.showSearchTerms.enabled", 172 deps: ["scotchBonnetEnabled", "showSearchTermsFeatureGate"], 173 visible: ({ scotchBonnetEnabled, showSearchTermsFeatureGate }) => { 174 if (lazy.CustomizableUI.getPlacementOfWidget("search-container")) { 175 return false; 176 } 177 return showSearchTermsFeatureGate.value || scotchBonnetEnabled.value; 178 }, 179 setup: onChange => { 180 // Add observer of CustomizableUI as showSearchTerms checkbox should be 181 // hidden while searchbar is enabled. 182 let customizableUIListener = { 183 onWidgetAfterDOMChange: node => { 184 if (node.id == "search-container") { 185 onChange(); 186 } 187 }, 188 }; 189 lazy.CustomizableUI.addListener(customizableUIListener); 190 return () => lazy.CustomizableUI.removeListener(customizableUIListener); 191 }, 192 }); 193 194 Preferences.addSetting({ 195 id: "separatePrivateDefaultUI", 196 pref: "browser.search.separatePrivateDefault.ui.enabled", 197 onUserChange: () => { 198 gSearchPane._engineStore.notifyRebuildViews(); 199 }, 200 }); 201 202 Preferences.addSetting({ 203 id: "browserSeparateDefaultEngine", 204 pref: "browser.search.separatePrivateDefault", 205 deps: ["separatePrivateDefaultUI"], 206 visible: ({ separatePrivateDefaultUI }) => { 207 return separatePrivateDefaultUI.value; 208 }, 209 onUserChange: () => { 210 gSearchPane._engineStore.notifyRebuildViews(); 211 }, 212 }); 213 214 Preferences.addSetting( 215 createSearchEngineConfig({ 216 settingId: "defaultPrivateEngine", 217 getEngine: () => Services.search.getDefaultPrivate(), 218 setEngine: id => 219 Services.search.setDefaultPrivate( 220 Services.search.getEngineById(id), 221 Ci.nsISearchService.CHANGE_REASON_USER 222 ), 223 }) 224 ); 225 226 Preferences.addSetting({ 227 id: "searchSuggestionsEnabledPref", 228 pref: "browser.search.suggest.enabled", 229 }); 230 231 Preferences.addSetting({ 232 id: "permanentPBEnabledPref", 233 pref: "browser.privatebrowsing.autostart", 234 }); 235 236 Preferences.addSetting({ 237 id: "urlbarSuggestionsEnabledPref", 238 pref: "browser.urlbar.suggest.searches", 239 }); 240 241 Preferences.addSetting({ 242 id: "trendingFeaturegatePref", 243 pref: "browser.urlbar.trending.featureGate", 244 }); 245 246 // The show search suggestion box behaves differently depending on whether the 247 // separate search bar is shown. When the separate search bar is shown, it 248 // controls just the search suggestion preference, and the 249 // `urlBarSuggestionCheckbox` handles the urlbar suggestions. When the separate 250 // search bar is not shown, this checkbox toggles both preferences to ensure 251 // that the urlbar suggestion preference is set correctly, since that will be 252 // the only bar visible. 253 Preferences.addSetting({ 254 id: "suggestionsInSearchFieldsCheckbox", 255 deps: ["searchSuggestionsEnabledPref", "urlbarSuggestionsEnabledPref"], 256 get(_, deps) { 257 let searchBarVisible = 258 !!lazy.CustomizableUI.getPlacementOfWidget("search-container"); 259 return ( 260 deps.searchSuggestionsEnabledPref.value && 261 (searchBarVisible || deps.urlbarSuggestionsEnabledPref.value) 262 ); 263 }, 264 set(newCheckedValue, deps) { 265 let searchBarVisible = 266 !!lazy.CustomizableUI.getPlacementOfWidget("search-container"); 267 if (!searchBarVisible) { 268 deps.urlbarSuggestionsEnabledPref.value = newCheckedValue; 269 } 270 deps.searchSuggestionsEnabledPref.value = newCheckedValue; 271 return newCheckedValue; 272 }, 273 }); 274 275 Preferences.addSetting({ 276 id: "urlBarSuggestionCheckbox", 277 deps: [ 278 "urlbarSuggestionsEnabledPref", 279 "suggestionsInSearchFieldsCheckbox", 280 "searchSuggestionsEnabledPref", 281 "permanentPBEnabledPref", 282 ], 283 get: (_, deps) => { 284 let searchBarVisible = 285 !!lazy.CustomizableUI.getPlacementOfWidget("search-container"); 286 if ( 287 deps.suggestionsInSearchFieldsCheckbox.value && 288 searchBarVisible && 289 deps.urlbarSuggestionsEnabledPref.value 290 ) { 291 return true; 292 } 293 return false; 294 }, 295 set: (newCheckedValue, deps, setting) => { 296 if (setting.disabled) { 297 deps.urlbarSuggestionsEnabledPref.value = false; 298 return false; 299 } 300 301 let searchBarVisible = 302 !!lazy.CustomizableUI.getPlacementOfWidget("search-container"); 303 if (deps.suggestionsInSearchFieldsCheckbox.value && searchBarVisible) { 304 deps.urlbarSuggestionsEnabledPref.value = newCheckedValue; 305 } 306 return newCheckedValue; 307 }, 308 setup: onChange => { 309 // Add observer of CustomizableUI as checkbox should be hidden while 310 // searchbar is enabled. 311 let customizableUIListener = { 312 onWidgetAfterDOMChange: node => { 313 if (node.id == "search-container") { 314 onChange(); 315 } 316 }, 317 }; 318 lazy.CustomizableUI.addListener(customizableUIListener); 319 return () => lazy.CustomizableUI.removeListener(customizableUIListener); 320 }, 321 disabled: deps => { 322 return ( 323 !deps.searchSuggestionsEnabledPref.value || 324 deps.permanentPBEnabledPref.value 325 ); 326 }, 327 visible: () => { 328 let searchBarVisible = 329 !!lazy.CustomizableUI.getPlacementOfWidget("search-container"); 330 return searchBarVisible; 331 }, 332 }); 333 334 Preferences.addSetting({ 335 id: "showSearchSuggestionsFirstCheckbox", 336 pref: "browser.urlbar.showSearchSuggestionsFirst", 337 deps: [ 338 "suggestionsInSearchFieldsCheckbox", 339 "urlbarSuggestionsEnabledPref", 340 "searchSuggestionsEnabledPref", 341 "permanentPBEnabledPref", 342 ], 343 get: (newCheckedValue, deps) => { 344 if (!deps.searchSuggestionsEnabledPref.value) { 345 return false; 346 } 347 return deps.urlbarSuggestionsEnabledPref.value ? newCheckedValue : false; 348 }, 349 disabled: deps => { 350 return ( 351 !deps.suggestionsInSearchFieldsCheckbox.value || 352 !deps.urlbarSuggestionsEnabledPref.value || 353 deps.permanentPBEnabledPref.value 354 ); 355 }, 356 }); 357 358 Preferences.addSetting({ 359 id: "showSearchSuggestionsPrivateWindowsCheckbox", 360 pref: "browser.search.suggest.enabled.private", 361 deps: ["searchSuggestionsEnabledPref"], 362 disabled: deps => { 363 return !deps.searchSuggestionsEnabledPref.value; 364 }, 365 }); 366 367 Preferences.addSetting({ 368 id: "showTrendingSuggestionsCheckbox", 369 pref: "browser.urlbar.suggest.trending", 370 deps: [ 371 "searchSuggestionsEnabledPref", 372 "permanentPBEnabledPref", 373 // Required to dynamically update the disabled state when the default engine is changed. 374 "defaultEngineNormal", 375 "trendingFeaturegatePref", 376 ], 377 visible: deps => deps.trendingFeaturegatePref.value, 378 disabled: deps => { 379 let trendingSupported = Services.search.defaultEngine.supportsResponseType( 380 lazy.SearchUtils.URL_TYPE.TRENDING_JSON 381 ); 382 return ( 383 !deps.searchSuggestionsEnabledPref.value || 384 deps.permanentPBEnabledPref.value || 385 !trendingSupported 386 ); 387 }, 388 }); 389 390 Preferences.addSetting({ 391 id: "urlBarSuggestionPermanentPBMessage", 392 deps: ["urlBarSuggestionCheckbox", "permanentPBEnabledPref"], 393 visible: deps => { 394 return ( 395 deps.urlBarSuggestionCheckbox.visible && deps.permanentPBEnabledPref.value 396 ); 397 }, 398 }); 399 400 Preferences.addSetting({ 401 id: "quickSuggestEnabledPref", 402 pref: "browser.urlbar.quicksuggest.enabled", 403 }); 404 405 Preferences.addSetting({ 406 id: "quickSuggestSettingsUiPref", 407 pref: "browser.urlbar.quicksuggest.settingsUi", 408 }); 409 410 Preferences.addSetting({ 411 id: "nimbusListener", 412 setup(onChange) { 413 NimbusFeatures.urlbar.onUpdate(onChange); 414 return () => NimbusFeatures.urlbar.offUpdate(onChange); 415 }, 416 }); 417 418 Preferences.addSetting({ 419 id: "locationBarGroupHeader", 420 deps: [ 421 "quickSuggestEnabledPref", 422 "quickSuggestSettingsUiPref", 423 "nimbusListener", 424 ], 425 getControlConfig(config) { 426 let l10nId = 427 lazy.UrlbarPrefs.get("quickSuggestEnabled") && 428 lazy.UrlbarPrefs.get("quickSuggestSettingsUi") != 429 lazy.QuickSuggest.SETTINGS_UI.NONE 430 ? "addressbar-header-firefox-suggest-2" 431 : "addressbar-header-1"; 432 433 return { ...config, l10nId }; 434 }, 435 }); 436 437 Preferences.addSetting({ 438 id: "historySuggestion", 439 pref: "browser.urlbar.suggest.history", 440 }); 441 442 Preferences.addSetting({ 443 id: "bookmarkSuggestion", 444 pref: "browser.urlbar.suggest.bookmark", 445 }); 446 447 Preferences.addSetting({ 448 id: "clipboardFeaturegate", 449 pref: "browser.urlbar.clipboard.featureGate", 450 }); 451 452 Preferences.addSetting({ 453 id: "clipboardSuggestion", 454 pref: "browser.urlbar.suggest.clipboard", 455 deps: ["clipboardFeaturegate"], 456 visible: deps => { 457 return deps.clipboardFeaturegate.value; 458 }, 459 }); 460 461 Preferences.addSetting({ 462 id: "openpageSuggestion", 463 pref: "browser.urlbar.suggest.openpage", 464 }); 465 466 Preferences.addSetting({ 467 id: "topSitesSuggestion", 468 pref: "browser.urlbar.suggest.topsites", 469 }); 470 471 Preferences.addSetting({ 472 id: "enableRecentSearchesFeatureGate", 473 pref: "browser.urlbar.recentsearches.featureGate", 474 }); 475 476 Preferences.addSetting({ 477 id: "enableRecentSearches", 478 pref: "browser.urlbar.suggest.recentsearches", 479 deps: ["enableRecentSearchesFeatureGate"], 480 visible: deps => { 481 return deps.enableRecentSearchesFeatureGate.value; 482 }, 483 }); 484 485 Preferences.addSetting({ 486 id: "enginesSuggestion", 487 pref: "browser.urlbar.suggest.engines", 488 }); 489 490 Preferences.addSetting({ 491 id: "quickActionsShowPrefs", 492 pref: "browser.urlbar.quickactions.showPrefs", 493 }); 494 495 Preferences.addSetting({ 496 id: "enableQuickActions", 497 pref: "browser.urlbar.suggest.quickactions", 498 deps: ["quickActionsShowPrefs", "scotchBonnetEnabled"], 499 visible: deps => { 500 return deps.quickActionsShowPrefs.value || deps.scotchBonnetEnabled.value; 501 }, 502 }); 503 504 Preferences.addSetting({ 505 id: "firefoxSuggestAll", 506 pref: "browser.urlbar.suggest.quicksuggest.all", 507 }); 508 509 Preferences.addSetting({ 510 id: "firefoxSuggestSponsored", 511 pref: "browser.urlbar.suggest.quicksuggest.sponsored", 512 deps: ["firefoxSuggestAll"], 513 disabled: deps => { 514 return !deps.firefoxSuggestAll.value; 515 }, 516 }); 517 518 Preferences.addSetting({ 519 id: "firefoxSuggestOnlineEnabledToggle", 520 pref: "browser.urlbar.quicksuggest.online.enabled", 521 deps: [ 522 "firefoxSuggestAll", 523 "quickSuggestEnabledPref", 524 "quickSuggestSettingsUiPref", 525 "nimbusListener", 526 ], 527 visible: () => { 528 return ( 529 lazy.UrlbarPrefs.get("quickSuggestSettingsUi") == 530 lazy.QuickSuggest.SETTINGS_UI.FULL 531 ); 532 }, 533 disabled: deps => { 534 return !deps.firefoxSuggestAll.value; 535 }, 536 }); 537 538 Preferences.addSetting( 539 class extends Preferences.AsyncSetting { 540 static id = "restoreDismissedSuggestions"; 541 setup() { 542 Services.obs.addObserver( 543 this.emitChange, 544 "quicksuggest-dismissals-changed" 545 ); 546 return () => { 547 Services.obs.removeObserver( 548 this.emitChange, 549 "quicksuggest-dismissals-changed" 550 ); 551 }; 552 } 553 async disabled() { 554 return !(await lazy.QuickSuggest.canClearDismissedSuggestions()); 555 } 556 onUserClick() { 557 lazy.QuickSuggest.clearDismissedSuggestions(); 558 } 559 } 560 ); 561 562 Preferences.addSetting({ 563 id: "dismissedSuggestionsDescription", 564 }); 565 566 const ENGINE_FLAVOR = "text/x-moz-search-engine"; 567 const SEARCH_TYPE = "default_search"; 568 const SEARCH_KEY = "defaultSearch"; 569 570 var gEngineView = null; 571 572 var gSearchPane = { 573 _engineStore: null, 574 575 init() { 576 initSettingGroup("defaultEngine"); 577 initSettingGroup("searchSuggestions"); 578 initSettingGroup("firefoxSuggest"); 579 this._engineStore = new EngineStore(); 580 gEngineView = new EngineView(this._engineStore); 581 582 this._engineStore.init().catch(console.error); 583 584 if ( 585 Services.policies && 586 !Services.policies.isAllowed("installSearchEngine") 587 ) { 588 document.getElementById("addEnginesBox").hidden = true; 589 } else { 590 let addEnginesLink = document.getElementById("addEngines"); 591 addEnginesLink.setAttribute("href", lazy.SearchUIUtils.searchEnginesURL); 592 } 593 594 window.addEventListener("command", this); 595 596 Services.obs.addObserver(this, "browser-search-engine-modified"); 597 Services.obs.addObserver(this, "intl:app-locales-changed"); 598 window.addEventListener("unload", () => { 599 Services.obs.removeObserver(this, "browser-search-engine-modified"); 600 Services.obs.removeObserver(this, "intl:app-locales-changed"); 601 }); 602 }, 603 604 // ADDRESS BAR 605 handleEvent(aEvent) { 606 if (aEvent.type != "command") { 607 return; 608 } 609 switch (aEvent.target.id) { 610 case "": 611 if (aEvent.target.parentNode && aEvent.target.parentNode.parentNode) { 612 if (aEvent.target.parentNode.parentNode.id == "defaultEngine") { 613 gSearchPane.setDefaultEngine(); 614 } else if ( 615 aEvent.target.parentNode.parentNode.id == "defaultPrivateEngine" 616 ) { 617 gSearchPane.setDefaultPrivateEngine(); 618 } 619 } 620 break; 621 default: 622 gEngineView.handleEvent(aEvent); 623 } 624 }, 625 626 /** 627 * Handle when the app locale is changed. 628 */ 629 async appLocalesChanged() { 630 await document.l10n.ready; 631 await gEngineView.loadL10nNames(); 632 }, 633 634 /** 635 * nsIObserver implementation. 636 */ 637 observe(subject, topic, data) { 638 switch (topic) { 639 case "intl:app-locales-changed": { 640 this.appLocalesChanged(); 641 break; 642 } 643 case "browser-search-engine-modified": { 644 let engine = subject.QueryInterface(Ci.nsISearchEngine); 645 switch (data) { 646 case "engine-default": { 647 // Pass through to the engine store to handle updates. 648 this._engineStore.browserSearchEngineModified(engine, data); 649 break; 650 } 651 default: 652 this._engineStore.browserSearchEngineModified(engine, data); 653 } 654 break; 655 } 656 } 657 }, 658 659 showRestoreDefaults(aEnable) { 660 document.getElementById("restoreDefaultSearchEngines").disabled = !aEnable; 661 }, 662 663 async setDefaultEngine() { 664 await Services.search.setDefault( 665 document.getElementById("defaultEngine").selectedItem.engine, 666 Ci.nsISearchService.CHANGE_REASON_USER 667 ); 668 if (ExtensionSettingsStore.getSetting(SEARCH_TYPE, SEARCH_KEY) !== null) { 669 ExtensionSettingsStore.select( 670 ExtensionSettingsStore.SETTING_USER_SET, 671 SEARCH_TYPE, 672 SEARCH_KEY 673 ); 674 } 675 }, 676 677 async setDefaultPrivateEngine() { 678 await Services.search.setDefaultPrivate( 679 document.getElementById("defaultPrivateEngine").selectedItem.engine, 680 Ci.nsISearchService.CHANGE_REASON_USER 681 ); 682 }, 683 }; 684 685 /** 686 * Keeps track of the search engine objects and notifies the views for updates. 687 */ 688 class EngineStore { 689 /** 690 * A list of engines that are currently visible in the UI. 691 * 692 * @type {object[]} 693 */ 694 engines = []; 695 696 /** 697 * A list of listeners to be notified when the engine list changes. 698 * 699 * @type {object[]} 700 */ 701 #listeners = []; 702 703 async init() { 704 let engines = await Services.search.getEngines(); 705 706 let visibleEngines = engines.filter(e => !e.hidden); 707 for (let engine of visibleEngines) { 708 this.addEngine(engine); 709 } 710 this.notifyRowCountChanged(0, visibleEngines.length); 711 712 gSearchPane.showRestoreDefaults( 713 engines.some(e => e.isAppProvided && e.hidden) 714 ); 715 } 716 717 /** 718 * Adds a listener to be notified when the engine list changes. 719 * 720 * @param {object} aListener 721 */ 722 addListener(aListener) { 723 this.#listeners.push(aListener); 724 } 725 726 /** 727 * Notifies all listeners that the engine list has changed and they should 728 * rebuild. 729 */ 730 notifyRebuildViews() { 731 for (let listener of this.#listeners) { 732 try { 733 listener.rebuild(this.engines); 734 } catch (ex) { 735 console.error("Error notifying EngineStore listener", ex); 736 } 737 } 738 } 739 740 /** 741 * Notifies all listeners that the number of engines in the list has changed. 742 * 743 * @param {number} index 744 * @param {number} count 745 */ 746 notifyRowCountChanged(index, count) { 747 for (let listener of this.#listeners) { 748 listener.rowCountChanged(index, count, this.engines); 749 } 750 } 751 752 /** 753 * Notifies all listeners that the default engine has changed. 754 * 755 * @param {string} type 756 * @param {object} engine 757 */ 758 notifyDefaultEngineChanged(type, engine) { 759 for (let listener of this.#listeners) { 760 if ("defaultEngineChanged" in listener) { 761 listener.defaultEngineChanged(type, engine, this.engines); 762 } 763 } 764 } 765 766 notifyEngineIconUpdated(engine) { 767 // Check the engine is still in the list. 768 let index = this._getIndexForEngine(engine); 769 if (index != -1) { 770 for (let listener of this.#listeners) { 771 listener.engineIconUpdated(index, this.engines); 772 } 773 } 774 } 775 776 _getIndexForEngine(aEngine) { 777 return this.engines.indexOf(aEngine); 778 } 779 780 _getEngineByName(aName) { 781 return this.engines.find(engine => engine.name == aName); 782 } 783 784 /** 785 * Converts an nsISearchEngine object into an Engine Store 786 * search engine object. 787 * 788 * @param {nsISearchEngine} aEngine 789 * The search engine to convert. 790 * @returns {object} 791 * The EngineStore search engine object. 792 */ 793 _cloneEngine(aEngine) { 794 var clonedObj = { 795 iconURL: null, 796 }; 797 for (let i of ["id", "name", "alias", "hidden", "isAppProvided"]) { 798 clonedObj[i] = aEngine[i]; 799 } 800 clonedObj.isAddonEngine = 801 aEngine.wrappedJSObject instanceof lazy.AddonSearchEngine; 802 clonedObj.isUserEngine = 803 aEngine.wrappedJSObject instanceof lazy.UserSearchEngine; 804 clonedObj.originalEngine = aEngine; 805 806 // Trigger getting the iconURL for this engine. 807 aEngine.getIconURL().then(iconURL => { 808 if (iconURL) { 809 clonedObj.iconURL = iconURL; 810 } else if (window.devicePixelRatio > 1) { 811 clonedObj.iconURL = 812 "chrome://browser/skin/search-engine-placeholder@2x.png"; 813 } else { 814 clonedObj.iconURL = 815 "chrome://browser/skin/search-engine-placeholder.png"; 816 } 817 818 this.notifyEngineIconUpdated(clonedObj); 819 }); 820 821 return clonedObj; 822 } 823 824 // Callback for Array's some(). A thisObj must be passed to some() 825 _isSameEngine(aEngineClone) { 826 return aEngineClone.originalEngine.id == this.originalEngine.id; 827 } 828 829 addEngine(aEngine) { 830 this.engines.push(this._cloneEngine(aEngine)); 831 } 832 833 updateEngine(newEngine) { 834 let engineToUpdate = this.engines.findIndex( 835 e => e.originalEngine.id == newEngine.id 836 ); 837 if (engineToUpdate != -1) { 838 this.engines[engineToUpdate] = this._cloneEngine(newEngine); 839 } 840 } 841 842 moveEngine(aEngine, aNewIndex) { 843 if (aNewIndex < 0 || aNewIndex > this.engines.length - 1) { 844 throw new Error("ES_moveEngine: invalid aNewIndex!"); 845 } 846 var index = this._getIndexForEngine(aEngine); 847 if (index == -1) { 848 throw new Error("ES_moveEngine: invalid engine?"); 849 } 850 851 if (index == aNewIndex) { 852 return Promise.resolve(); 853 } // nothing to do 854 855 // Move the engine in our internal store 856 var removedEngine = this.engines.splice(index, 1)[0]; 857 this.engines.splice(aNewIndex, 0, removedEngine); 858 859 return Services.search.moveEngine(aEngine.originalEngine, aNewIndex); 860 } 861 862 /** 863 * Called when a search engine is removed. 864 * 865 * @param {nsISearchEngine} aEngine 866 * The Engine being removed. Note that this is an nsISearchEngine object. 867 */ 868 removeEngine(aEngine) { 869 if (this.engines.length == 1) { 870 throw new Error("Cannot remove last engine!"); 871 } 872 873 let engineId = aEngine.id; 874 let index = this.engines.findIndex(element => element.id == engineId); 875 876 if (index == -1) { 877 throw new Error("invalid engine?"); 878 } 879 880 this.engines.splice(index, 1)[0]; 881 882 if (aEngine.isAppProvided) { 883 gSearchPane.showRestoreDefaults(true); 884 } 885 886 this.notifyRowCountChanged(index, -1); 887 888 document.getElementById("engineList").focus(); 889 } 890 891 /** 892 * Update the default engine UI and engine tree view as appropriate when engine changes 893 * or locale changes occur. 894 * 895 * @param {nsISearchEngine} engine 896 * @param {string} data 897 */ 898 browserSearchEngineModified(engine, data) { 899 engine.QueryInterface(Ci.nsISearchEngine); 900 switch (data) { 901 case "engine-added": 902 this.addEngine(engine); 903 this.notifyRowCountChanged(gEngineView.lastEngineIndex, 1); 904 break; 905 case "engine-changed": 906 case "engine-icon-changed": 907 this.updateEngine(engine); 908 this.notifyRebuildViews(); 909 break; 910 case "engine-removed": 911 this.removeEngine(engine); 912 break; 913 case "engine-default": 914 this.notifyDefaultEngineChanged("normal", engine); 915 break; 916 case "engine-default-private": 917 this.notifyDefaultEngineChanged("private", engine); 918 break; 919 } 920 } 921 922 async restoreDefaultEngines() { 923 var added = 0; 924 // _cloneEngine is necessary here because all functions in 925 // this file work on EngineStore search engine objects. 926 let appProvidedEngines = ( 927 await Services.search.getAppProvidedEngines() 928 ).map(this._cloneEngine, this); 929 930 for (var i = 0; i < appProvidedEngines.length; ++i) { 931 var e = appProvidedEngines[i]; 932 933 // If the engine is already in the list, just move it. 934 if (this.engines.some(this._isSameEngine, e)) { 935 await this.moveEngine(this._getEngineByName(e.name), i); 936 } else { 937 // Otherwise, add it back to our internal store 938 939 // The search service removes the alias when an engine is hidden, 940 // so clear any alias we may have cached before unhiding the engine. 941 e.alias = ""; 942 943 this.engines.splice(i, 0, e); 944 let engine = e.originalEngine; 945 engine.hidden = false; 946 await Services.search.moveEngine(engine, i); 947 added++; 948 } 949 } 950 951 // We can't do this as part of the loop above because the indices are 952 // used for moving engines. 953 let policyRemovedEngineNames = 954 Services.policies.getActivePolicies()?.SearchEngines?.Remove || []; 955 for (let engineName of policyRemovedEngineNames) { 956 let engine = Services.search.getEngineByName(engineName); 957 if (engine) { 958 try { 959 await Services.search.removeEngine( 960 engine, 961 Ci.nsISearchService.CHANGE_REASON_ENTERPRISE 962 ); 963 } catch (ex) { 964 // Engine might not exist 965 } 966 } 967 } 968 969 Services.search.resetToAppDefaultEngine(); 970 gSearchPane.showRestoreDefaults(false); 971 this.notifyRebuildViews(); 972 return added; 973 } 974 975 changeEngine(aEngine, aProp, aNewValue) { 976 var index = this._getIndexForEngine(aEngine); 977 if (index == -1) { 978 throw new Error("invalid engine?"); 979 } 980 981 this.engines[index][aProp] = aNewValue; 982 aEngine.originalEngine[aProp] = aNewValue; 983 } 984 } 985 986 /** 987 * Manages the view of the Search Shortcuts tree on the search pane of preferences. 988 */ 989 class EngineView { 990 _engineStore; 991 _engineList = null; 992 tree = null; 993 994 /** 995 * @param {EngineStore} aEngineStore 996 */ 997 constructor(aEngineStore) { 998 this._engineStore = aEngineStore; 999 this._engineList = document.getElementById("engineList"); 1000 this._engineList.view = this; 1001 1002 lazy.UrlbarPrefs.addObserver(this); 1003 aEngineStore.addListener(this); 1004 1005 this.loadL10nNames(); 1006 this.#addListeners(); 1007 } 1008 1009 async loadL10nNames() { 1010 // This maps local shortcut sources to their l10n names. The names are needed 1011 // by getCellText. Getting the names is async but getCellText is not, so we 1012 // cache them here to retrieve them syncronously in getCellText. 1013 this._localShortcutL10nNames = new Map(); 1014 1015 let getIDs = (suffix = "") => 1016 UrlbarUtils.LOCAL_SEARCH_MODES.map(mode => { 1017 let name = UrlbarUtils.getResultSourceName(mode.source); 1018 return { id: `urlbar-search-mode-${name}${suffix}` }; 1019 }); 1020 1021 try { 1022 let localizedIDs = getIDs(); 1023 let englishIDs = getIDs("-en"); 1024 1025 let englishSearchStrings = new Localization([ 1026 "preview/enUS-searchFeatures.ftl", 1027 ]); 1028 let localizedNames = await document.l10n.formatValues(localizedIDs); 1029 let englishNames = await englishSearchStrings.formatValues(englishIDs); 1030 1031 UrlbarUtils.LOCAL_SEARCH_MODES.forEach(({ source }, index) => { 1032 let localizedName = localizedNames[index]; 1033 let englishName = englishNames[index]; 1034 1035 // Add only the English name if localized and English are the same 1036 let names = 1037 localizedName === englishName 1038 ? [englishName] 1039 : [localizedName, englishName]; 1040 1041 this._localShortcutL10nNames.set(source, names); 1042 1043 // Invalidate the tree now that we have the names in case getCellText was 1044 // called before name retrieval finished. 1045 this.invalidate(); 1046 }); 1047 } catch (ex) { 1048 console.error("Error loading l10n names", ex); 1049 } 1050 } 1051 1052 #addListeners() { 1053 this._engineList.addEventListener("click", this); 1054 this._engineList.addEventListener("dragstart", this); 1055 this._engineList.addEventListener("keypress", this); 1056 this._engineList.addEventListener("select", this); 1057 this._engineList.addEventListener("dblclick", this); 1058 } 1059 1060 get lastEngineIndex() { 1061 return this._engineStore.engines.length - 1; 1062 } 1063 1064 get selectedIndex() { 1065 var seln = this.selection; 1066 if (seln.getRangeCount() > 0) { 1067 var min = {}; 1068 seln.getRangeAt(0, min, {}); 1069 return min.value; 1070 } 1071 return -1; 1072 } 1073 1074 get selectedEngine() { 1075 return this._engineStore.engines[this.selectedIndex]; 1076 } 1077 1078 // Helpers 1079 rebuild() { 1080 this.invalidate(); 1081 } 1082 1083 rowCountChanged(index, count) { 1084 if (!this.tree) { 1085 return; 1086 } 1087 this.tree.rowCountChanged(index, count); 1088 1089 // If we're removing elements, ensure that we still have a selection. 1090 if (count < 0) { 1091 this.selection.select(Math.min(index, this.rowCount - 1)); 1092 this.ensureRowIsVisible(this.currentIndex); 1093 } 1094 } 1095 1096 engineIconUpdated(index) { 1097 this.tree?.invalidateCell( 1098 index, 1099 this.tree.columns.getNamedColumn("engineName") 1100 ); 1101 } 1102 1103 invalidate() { 1104 this.tree?.invalidate(); 1105 } 1106 1107 ensureRowIsVisible(index) { 1108 this.tree.ensureRowIsVisible(index); 1109 } 1110 1111 getSourceIndexFromDrag(dataTransfer) { 1112 return parseInt(dataTransfer.getData(ENGINE_FLAVOR)); 1113 } 1114 1115 isCheckBox(index, column) { 1116 return column.id == "engineShown"; 1117 } 1118 1119 isEngineSelectedAndRemovable() { 1120 let defaultEngine = Services.search.defaultEngine; 1121 let defaultPrivateEngine = Services.search.defaultPrivateEngine; 1122 // We don't allow the last remaining engine to be removed, thus the 1123 // `this.lastEngineIndex != 0` check. 1124 // We don't allow the default engine to be removed. 1125 return ( 1126 this.selectedIndex != -1 && 1127 this.lastEngineIndex != 0 && 1128 !this._getLocalShortcut(this.selectedIndex) && 1129 this.selectedEngine.name != defaultEngine.name && 1130 this.selectedEngine.name != defaultPrivateEngine.name 1131 ); 1132 } 1133 1134 /** 1135 * Removes a search engine from the search service. 1136 * 1137 * Application provided engines are removed without confirmation since they 1138 * can easily be restored. Addon engines are not removed (see comment). 1139 * For other engine types, the user is prompted for confirmation. 1140 * 1141 * @param {object} engine 1142 * The search engine object from EngineStore to remove. 1143 */ 1144 async promptAndRemoveEngine(engine) { 1145 if (engine.isAppProvided) { 1146 Services.search.removeEngine( 1147 this.selectedEngine.originalEngine, 1148 Ci.nsISearchService.CHANGE_REASON_USER 1149 ); 1150 return; 1151 } 1152 1153 if (engine.isAddonEngine) { 1154 // Addon engines will re-appear after restarting, see Bug 1546652. 1155 // This should ideally prompt the user if they want to remove the addon. 1156 let msg = await document.l10n.formatValue("remove-addon-engine-alert"); 1157 alert(msg); 1158 return; 1159 } 1160 1161 let [body, removeLabel] = await document.l10n.formatValues([ 1162 "remove-engine-confirmation", 1163 "remove-engine-remove", 1164 ]); 1165 1166 let button = Services.prompt.confirmExBC( 1167 window.browsingContext, 1168 Services.prompt.MODAL_TYPE_CONTENT, 1169 null, 1170 body, 1171 (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) | 1172 (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1), 1173 removeLabel, 1174 null, 1175 null, 1176 null, 1177 {} 1178 ); 1179 1180 // Button 0 is the remove button. 1181 if (button == 0) { 1182 Services.search.removeEngine( 1183 this.selectedEngine.originalEngine, 1184 Ci.nsISearchService.CHANGE_REASON_USER 1185 ); 1186 } 1187 } 1188 1189 /** 1190 * Returns the local shortcut corresponding to a tree row, or null if the row 1191 * is not a local shortcut. 1192 * 1193 * @param {number} index 1194 * The tree row index. 1195 * @returns {object} 1196 * The local shortcut object or null if the row is not a local shortcut. 1197 */ 1198 _getLocalShortcut(index) { 1199 let engineCount = this._engineStore.engines.length; 1200 if (index < engineCount) { 1201 return null; 1202 } 1203 return UrlbarUtils.LOCAL_SEARCH_MODES[index - engineCount]; 1204 } 1205 1206 /** 1207 * Called by UrlbarPrefs when a urlbar pref changes. 1208 * 1209 * @param {string} pref 1210 * The name of the pref relative to the browser.urlbar branch. 1211 */ 1212 onPrefChanged(pref) { 1213 // If one of the local shortcut prefs was toggled, toggle its row's 1214 // checkbox. 1215 let parts = pref.split("."); 1216 if (parts[0] == "shortcuts" && parts[1] && parts.length == 2) { 1217 this.invalidate(); 1218 } 1219 } 1220 1221 handleEvent(aEvent) { 1222 switch (aEvent.type) { 1223 case "dblclick": 1224 if (aEvent.target.id == "engineChildren") { 1225 let cell = aEvent.target.parentNode.getCellAt( 1226 aEvent.clientX, 1227 aEvent.clientY 1228 ); 1229 if (cell.col?.id == "engineKeyword") { 1230 this.#startEditingAlias(this.selectedIndex); 1231 } 1232 } 1233 break; 1234 case "click": 1235 if ( 1236 aEvent.target.id != "engineChildren" && 1237 !aEvent.target.classList.contains("searchEngineAction") 1238 ) { 1239 // We don't want to toggle off selection while editing keyword 1240 // so proceed only when the input field is hidden. 1241 // We need to check that engineList.view is defined here 1242 // because the "click" event listener is on <window> and the 1243 // view might have been destroyed if the pane has been navigated 1244 // away from. 1245 if (this._engineList.inputField.hidden && this._engineList.view) { 1246 let selection = this._engineList.view.selection; 1247 if (selection?.count > 0) { 1248 selection.toggleSelect(selection.currentIndex); 1249 } 1250 this._engineList.blur(); 1251 } 1252 } 1253 break; 1254 case "command": 1255 switch (aEvent.target.id) { 1256 case "restoreDefaultSearchEngines": 1257 this.#onRestoreDefaults(); 1258 break; 1259 case "removeEngineButton": 1260 if (this.isEngineSelectedAndRemovable()) { 1261 this.promptAndRemoveEngine(this.selectedEngine); 1262 } 1263 break; 1264 case "editEngineButton": 1265 if (this.selectedEngine.isUserEngine) { 1266 let engine = this.selectedEngine.originalEngine.wrappedJSObject; 1267 gSubDialog.open( 1268 "chrome://browser/content/search/addEngine.xhtml", 1269 { features: "resizable=no, modal=yes" }, 1270 { engine, mode: "EDIT" } 1271 ); 1272 } 1273 break; 1274 case "addEngineButton": 1275 gSubDialog.open( 1276 "chrome://browser/content/search/addEngine.xhtml", 1277 { features: "resizable=no, modal=yes" }, 1278 { mode: "NEW" } 1279 ); 1280 break; 1281 } 1282 break; 1283 case "dragstart": 1284 if (aEvent.target.id == "engineChildren") { 1285 this.#onDragEngineStart(aEvent); 1286 } 1287 break; 1288 case "keypress": 1289 if (aEvent.target.id == "engineList") { 1290 this.#onTreeKeyPress(aEvent); 1291 } 1292 break; 1293 case "select": 1294 if (aEvent.target.id == "engineList") { 1295 this.#onTreeSelect(); 1296 } 1297 break; 1298 } 1299 } 1300 1301 /** 1302 * Called when the restore default engines button is clicked to reset the 1303 * list of engines to their defaults. 1304 */ 1305 async #onRestoreDefaults() { 1306 let num = await this._engineStore.restoreDefaultEngines(); 1307 this.rowCountChanged(0, num); 1308 } 1309 1310 #onDragEngineStart(event) { 1311 let selectedIndex = this.selectedIndex; 1312 1313 // Local shortcut rows can't be dragged or re-ordered. 1314 if (this._getLocalShortcut(selectedIndex)) { 1315 event.preventDefault(); 1316 return; 1317 } 1318 1319 let tree = document.getElementById("engineList"); 1320 let cell = tree.getCellAt(event.clientX, event.clientY); 1321 if (selectedIndex >= 0 && !this.isCheckBox(cell.row, cell.col)) { 1322 event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString()); 1323 event.dataTransfer.effectAllowed = "move"; 1324 } 1325 } 1326 1327 #onTreeSelect() { 1328 document.getElementById("removeEngineButton").disabled = 1329 !this.isEngineSelectedAndRemovable(); 1330 document.getElementById("editEngineButton").disabled = 1331 !this.selectedEngine?.isUserEngine; 1332 } 1333 1334 #onTreeKeyPress(aEvent) { 1335 let index = this.selectedIndex; 1336 let tree = document.getElementById("engineList"); 1337 if (tree.hasAttribute("editing")) { 1338 return; 1339 } 1340 1341 if (aEvent.charCode == KeyEvent.DOM_VK_SPACE) { 1342 // Space toggles the checkbox. 1343 let newValue = !this.getCellValue( 1344 index, 1345 tree.columns.getNamedColumn("engineShown") 1346 ); 1347 this.setCellValue( 1348 index, 1349 tree.columns.getFirstColumn(), 1350 newValue.toString() 1351 ); 1352 // Prevent page from scrolling on the space key. 1353 aEvent.preventDefault(); 1354 } else { 1355 let isMac = Services.appinfo.OS == "Darwin"; 1356 if ( 1357 (isMac && aEvent.keyCode == KeyEvent.DOM_VK_RETURN) || 1358 (!isMac && aEvent.keyCode == KeyEvent.DOM_VK_F2) 1359 ) { 1360 this.#startEditingAlias(index); 1361 } else if ( 1362 aEvent.keyCode == KeyEvent.DOM_VK_DELETE || 1363 (isMac && 1364 aEvent.shiftKey && 1365 aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE) 1366 ) { 1367 // Delete and Shift+Backspace (Mac) removes selected engine. 1368 if (this.isEngineSelectedAndRemovable()) { 1369 this.promptAndRemoveEngine(this.selectedEngine); 1370 } 1371 } 1372 } 1373 } 1374 1375 /** 1376 * Triggers editing of an alias in the tree. 1377 * 1378 * @param {number} index 1379 */ 1380 #startEditingAlias(index) { 1381 // Local shortcut aliases can't be edited. 1382 if (this._getLocalShortcut(index)) { 1383 return; 1384 } 1385 1386 let engine = this._engineStore.engines[index]; 1387 this.tree.startEditing(index, this.tree.columns.getLastColumn()); 1388 this.tree.inputField.value = engine.alias || ""; 1389 this.tree.inputField.select(); 1390 } 1391 1392 /** 1393 * Triggers editing of an engine name in the tree. 1394 * 1395 * @param {number} index 1396 */ 1397 #startEditingName(index) { 1398 let engine = this._engineStore.engines[index]; 1399 if (!engine.isUserEngine) { 1400 return; 1401 } 1402 1403 this.tree.startEditing( 1404 index, 1405 this.tree.columns.getNamedColumn("engineName") 1406 ); 1407 this.tree.inputField.value = engine.name; 1408 this.tree.inputField.select(); 1409 } 1410 1411 // nsITreeView 1412 get rowCount() { 1413 let localModes = UrlbarUtils.LOCAL_SEARCH_MODES; 1414 if (!lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) { 1415 localModes = localModes.filter( 1416 mode => mode.source != UrlbarUtils.RESULT_SOURCE.ACTIONS 1417 ); 1418 } 1419 return this._engineStore.engines.length + localModes.length; 1420 } 1421 1422 getImageSrc(index, column) { 1423 if (column.id == "engineName") { 1424 let shortcut = this._getLocalShortcut(index); 1425 if (shortcut) { 1426 return shortcut.icon; 1427 } 1428 1429 return this._engineStore.engines[index].iconURL; 1430 } 1431 1432 return ""; 1433 } 1434 1435 getCellText(index, column) { 1436 if (column.id == "engineName") { 1437 let shortcut = this._getLocalShortcut(index); 1438 if (shortcut) { 1439 return this._localShortcutL10nNames.get(shortcut.source)[0] || ""; 1440 } 1441 return this._engineStore.engines[index].name; 1442 } else if (column.id == "engineKeyword") { 1443 let shortcut = this._getLocalShortcut(index); 1444 if (shortcut) { 1445 if ( 1446 lazy.UrlbarPrefs.getScotchBonnetPref( 1447 "searchRestrictKeywords.featureGate" 1448 ) 1449 ) { 1450 let keywords = this._localShortcutL10nNames 1451 .get(shortcut.source) 1452 .map(keyword => `@${keyword.toLowerCase()}`) 1453 .join(", "); 1454 1455 return `${keywords}, ${shortcut.restrict}`; 1456 } 1457 1458 return shortcut.restrict; 1459 } 1460 return this._engineStore.engines[index].originalEngine.aliases.join(", "); 1461 } 1462 return ""; 1463 } 1464 1465 setTree(tree) { 1466 this.tree = tree; 1467 } 1468 1469 canDrop(targetIndex, orientation, dataTransfer) { 1470 var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); 1471 return ( 1472 sourceIndex != -1 && 1473 sourceIndex != targetIndex && 1474 sourceIndex != targetIndex + orientation && 1475 // Local shortcut rows can't be dragged or dropped on. 1476 targetIndex < this._engineStore.engines.length 1477 ); 1478 } 1479 1480 async drop(dropIndex, orientation, dataTransfer) { 1481 // Local shortcut rows can't be dragged or dropped on. This can sometimes 1482 // be reached even though canDrop returns false for these rows. 1483 if (this._engineStore.engines.length <= dropIndex) { 1484 return; 1485 } 1486 1487 var sourceIndex = this.getSourceIndexFromDrag(dataTransfer); 1488 var sourceEngine = this._engineStore.engines[sourceIndex]; 1489 1490 const nsITreeView = Ci.nsITreeView; 1491 if (dropIndex > sourceIndex) { 1492 if (orientation == nsITreeView.DROP_BEFORE) { 1493 dropIndex--; 1494 } 1495 } else if (orientation == nsITreeView.DROP_AFTER) { 1496 dropIndex++; 1497 } 1498 1499 await this._engineStore.moveEngine(sourceEngine, dropIndex); 1500 gSearchPane.showRestoreDefaults(true); 1501 1502 // Redraw, and adjust selection 1503 this.invalidate(); 1504 this.selection.select(dropIndex); 1505 } 1506 1507 selection = null; 1508 getRowProperties() { 1509 return ""; 1510 } 1511 getCellProperties(index, column) { 1512 if (column.id == "engineName") { 1513 // For local shortcut rows, return the result source name so we can style 1514 // the icons in CSS. 1515 let shortcut = this._getLocalShortcut(index); 1516 if (shortcut) { 1517 return UrlbarUtils.getResultSourceName(shortcut.source); 1518 } 1519 } 1520 return ""; 1521 } 1522 getColumnProperties() { 1523 return ""; 1524 } 1525 isContainer() { 1526 return false; 1527 } 1528 isContainerOpen() { 1529 return false; 1530 } 1531 isContainerEmpty() { 1532 return false; 1533 } 1534 isSeparator() { 1535 return false; 1536 } 1537 isSorted() { 1538 return false; 1539 } 1540 getParentIndex() { 1541 return -1; 1542 } 1543 hasNextSibling() { 1544 return false; 1545 } 1546 getLevel() { 1547 return 0; 1548 } 1549 getCellValue(index, column) { 1550 if (column.id == "engineShown") { 1551 let shortcut = this._getLocalShortcut(index); 1552 if (shortcut) { 1553 return lazy.UrlbarPrefs.get(shortcut.pref); 1554 } 1555 return !this._engineStore.engines[index].originalEngine.hideOneOffButton; 1556 } 1557 return undefined; 1558 } 1559 toggleOpenState() {} 1560 cycleHeader() {} 1561 selectionChanged() {} 1562 cycleCell() {} 1563 isEditable(index, column) { 1564 return ( 1565 column.id == "engineShown" || 1566 (column.id == "engineKeyword" && !this._getLocalShortcut(index)) || 1567 (column.id == "engineName" && 1568 this._engineStore.engines[index].isUserEngine) 1569 ); 1570 } 1571 setCellValue(index, column, value) { 1572 if (column.id == "engineShown") { 1573 let shortcut = this._getLocalShortcut(index); 1574 if (shortcut) { 1575 lazy.UrlbarPrefs.set(shortcut.pref, value == "true"); 1576 this.invalidate(); 1577 return; 1578 } 1579 this._engineStore.engines[index].originalEngine.hideOneOffButton = 1580 value != "true"; 1581 this.invalidate(); 1582 } 1583 } 1584 async setCellText(index, column, value) { 1585 let engine = this._engineStore.engines[index]; 1586 if (column.id == "engineKeyword") { 1587 let valid = await this.#changeKeyword(engine, value); 1588 if (!valid) { 1589 this.#startEditingAlias(index); 1590 } 1591 } else if (column.id == "engineName" && engine.isUserEngine) { 1592 let valid = await this.#changeName(engine, value); 1593 if (!valid) { 1594 this.#startEditingName(index); 1595 } 1596 } 1597 } 1598 1599 /** 1600 * Handles changing the keyword for an engine. This will check for potentially 1601 * duplicate keywords and prompt the user if necessary. 1602 * 1603 * @param {object} aEngine 1604 * The engine to change. 1605 * @param {string} aNewKeyword 1606 * The new keyword. 1607 * @returns {Promise<boolean>} 1608 * Resolves to true if the keyword was changed. 1609 */ 1610 async #changeKeyword(aEngine, aNewKeyword) { 1611 let keyword = aNewKeyword.trim(); 1612 if (keyword) { 1613 let isBookmarkDuplicate = !!(await PlacesUtils.keywords.fetch(keyword)); 1614 1615 let dupEngine = await Services.search.getEngineByAlias(keyword); 1616 let isEngineDuplicate = dupEngine !== null && dupEngine.id != aEngine.id; 1617 1618 // Notify the user if they have chosen an existing engine/bookmark keyword 1619 if (isEngineDuplicate || isBookmarkDuplicate) { 1620 let msgid; 1621 if (isEngineDuplicate) { 1622 msgid = { 1623 id: "search-keyword-warning-engine", 1624 args: { name: dupEngine.name }, 1625 }; 1626 } else { 1627 msgid = { id: "search-keyword-warning-bookmark" }; 1628 } 1629 1630 let msg = await document.l10n.formatValue(msgid.id, msgid.args); 1631 alert(msg); 1632 return false; 1633 } 1634 } 1635 1636 this._engineStore.changeEngine(aEngine, "alias", keyword); 1637 this.invalidate(); 1638 return true; 1639 } 1640 1641 /** 1642 * Handles changing the name for a user engine. This will check for 1643 * duplicate names and warn the user if necessary. 1644 * 1645 * @param {object} aEngine 1646 * The user search engine to change. 1647 * @param {string} aNewName 1648 * The new name. 1649 * @returns {Promise<boolean>} 1650 * Resolves to true if the name was changed. 1651 */ 1652 async #changeName(aEngine, aNewName) { 1653 let valid = aEngine.originalEngine.wrappedJSObject.rename(aNewName); 1654 if (!valid) { 1655 let msg = await document.l10n.formatValue( 1656 "edit-engine-name-warning-duplicate", 1657 { name: aNewName } 1658 ); 1659 alert(msg); 1660 return false; 1661 } 1662 return true; 1663 } 1664 }