UrlbarSearchOneOffs.sys.mjs (11086B)
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 { SearchOneOffs } from "moz-src:///browser/components/search/SearchOneOffs.sys.mjs"; 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 const lazy = XPCOMUtils.declareLazy({ 9 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 10 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 11 }); 12 13 /** 14 * @import {LegacySearchOneOffButton} from "moz-src:///browser/components/search/SearchOneOffs.sys.mjs" 15 * @import {UrlbarView} from "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs" 16 */ 17 18 /** 19 * The one-off search buttons in the urlbar. 20 */ 21 export class UrlbarSearchOneOffs extends SearchOneOffs { 22 /** 23 * Constructor. 24 * 25 * @param {UrlbarView} view 26 * The parent UrlbarView. 27 */ 28 constructor(view) { 29 super(view.panel.querySelector(".search-one-offs")); 30 this.view = view; 31 this.input = view.input; 32 lazy.UrlbarPrefs.addObserver(this); 33 // Override the SearchOneOffs.sys.mjs value for the Address Bar. 34 this.disableOneOffsHorizontalKeyNavigation = true; 35 } 36 37 /** 38 * Returns the local search mode one-off buttons. 39 * 40 * @returns {Array} 41 * The local one-off buttons. 42 */ 43 get localButtons() { 44 return this.getSelectableButtons(false).filter(b => b.source); 45 } 46 47 /** 48 * Invoked when Web provided search engines list changes. 49 */ 50 updateWebEngines() { 51 this.invalidateCache(); 52 if (this.view.isOpen) { 53 this._rebuild(); 54 } 55 } 56 57 /** 58 * Enables (shows) or disables (hides) the one-offs. 59 * 60 * @param {boolean} enable 61 * True to enable, false to disable. 62 */ 63 enable(enable) { 64 if (lazy.UrlbarPrefs.getScotchBonnetPref("scotchBonnet.disableOneOffs")) { 65 enable = false; 66 } 67 if (enable) { 68 this.telemetryOrigin = "urlbar"; 69 this.style.display = ""; 70 this.textbox = this.view.input.inputField; 71 if (this.view.isOpen) { 72 this._rebuild(); 73 } 74 this.view.controller.addListener(this); 75 } else { 76 this.telemetryOrigin = null; 77 this.style.display = "none"; 78 this.textbox = null; 79 this.view.controller.removeListener(this); 80 } 81 } 82 83 /** 84 * Query listener method. Delegates to the superclass. 85 */ 86 onViewOpen() { 87 this._on_popupshowing(); 88 } 89 90 /** 91 * Query listener method. Delegates to the superclass. 92 */ 93 onViewClose() { 94 this._on_popuphidden(); 95 } 96 97 /** 98 * @returns {boolean} 99 * True if the one-offs are connected to a view. 100 */ 101 get hasView() { 102 // Return true if the one-offs are enabled. We set style.display = "none" 103 // when they're disabled, and we hide the container when there are no 104 // engines to show. 105 return this.style.display != "none" && !this.container.hidden; 106 } 107 108 /** 109 * @returns {boolean} 110 * True if the view is open. 111 */ 112 get isViewOpen() { 113 return this.view.isOpen; 114 } 115 116 /** 117 * The selected one-off including the search-settings button. 118 * 119 * @param {?LegacySearchOneOffButton} button 120 * The selected one-off button. Null if no one-off is selected. 121 */ 122 set selectedButton(button) { 123 if (this.selectedButton == button) { 124 return; 125 } 126 127 super.selectedButton = button; 128 129 let expectedSearchMode; 130 if (button && button != this.view.oneOffSearchButtons.settingsButton) { 131 expectedSearchMode = { 132 engineName: button.engine?.name, 133 source: button.source, 134 entry: "oneoff", 135 }; 136 this.input.searchMode = expectedSearchMode; 137 } else if (this.input.searchMode) { 138 // Restore the previous state. We do this only if we're in search mode, as 139 // an optimization in the common case of cycling through normal results. 140 this.input.restoreSearchModeState(); 141 } 142 } 143 144 get selectedButton() { 145 return super.selectedButton; 146 } 147 148 /** 149 * The selected index in the view or -1 if there is no selection. 150 * 151 * @returns {number} 152 */ 153 get selectedViewIndex() { 154 return this.view.selectedRowIndex; 155 } 156 set selectedViewIndex(val) { 157 this.view.selectedRowIndex = val; 158 } 159 160 /** 161 * Closes the view. 162 */ 163 closeView() { 164 if (this.view) { 165 this.view.close(); 166 } 167 } 168 169 /** 170 * Called when a one-off is clicked. 171 * 172 * @param {MouseEvent|KeyboardEvent} event 173 * The event that triggered the pick. 174 * @param {object} searchMode 175 * Used by UrlbarInput.setSearchMode to enter search mode. See setSearchMode 176 * documentation for details. 177 */ 178 handleSearchCommand(event, searchMode) { 179 // The settings button and adding engines are a special case and executed 180 // immediately. 181 if ( 182 this.selectedButton == this.view.oneOffSearchButtons.settingsButton || 183 this.selectedButton.classList.contains( 184 "searchbar-engine-one-off-add-engine" 185 ) 186 ) { 187 this.input.controller.engagementEvent.discard(); 188 this.selectedButton.doCommand(); 189 this.selectedButton = null; 190 return; 191 } 192 193 // We allow autofill in local but not remote search modes. 194 let startQueryParams = { 195 allowAutofill: 196 !searchMode.engineName && 197 searchMode.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 198 event, 199 }; 200 201 let userTypedSearchString = 202 this.input.value && this.input.getAttribute("pageproxystate") != "valid"; 203 let engine = Services.search.getEngineByName(searchMode.engineName); 204 205 let { where, params } = this._whereToOpen(event); 206 207 // Some key combinations should execute a search immediately. We handle 208 // these here, outside the switch statement. 209 if ( 210 userTypedSearchString && 211 engine && 212 (event.shiftKey || where != "current") 213 ) { 214 this.input.handleNavigation({ 215 event, 216 oneOffParams: { 217 openWhere: where, 218 openParams: params, 219 engine: this.selectedButton.engine, 220 }, 221 }); 222 this.selectedButton = null; 223 return; 224 } 225 226 // Handle opening search mode in either the current tab or in a new tab. 227 switch (where) { 228 case "current": { 229 this.input.searchMode = searchMode; 230 this.input.startQuery(startQueryParams); 231 break; 232 } 233 case "tab": { 234 // We set this.selectedButton when switching tabs. If we entered search 235 // mode preview here, it could be cleared when this.selectedButton calls 236 // setSearchMode. 237 searchMode.isPreview = false; 238 239 let newTab = this.input.window.gBrowser.addTrustedTab("about:newtab"); 240 this.input.setSearchMode(searchMode, newTab.linkedBrowser); 241 if (userTypedSearchString) { 242 // Set the search string for the new tab. 243 newTab.linkedBrowser.userTypedValue = this.input.value; 244 } 245 if (!params?.inBackground) { 246 this.input.window.gBrowser.selectedTab = newTab; 247 newTab.ownerGlobal.gURLBar.startQuery(startQueryParams); 248 } 249 break; 250 } 251 default: { 252 this.input.searchMode = searchMode; 253 this.input.startQuery(startQueryParams); 254 this.input.select(); 255 break; 256 } 257 } 258 259 this.selectedButton = null; 260 } 261 262 /** 263 * Sets the tooltip for a one-off button with an engine. This should set 264 * either the `tooltiptext` attribute or the relevant l10n ID. 265 * 266 * @param {LegacySearchOneOffButton} button 267 * The one-off button. 268 */ 269 setTooltipForEngineButton(button) { 270 let aliases = button.engine.aliases; 271 if (!aliases.length) { 272 super.setTooltipForEngineButton(button); 273 return; 274 } 275 this.document.l10n.setAttributes( 276 button, 277 "search-one-offs-engine-with-alias", 278 { 279 engineName: button.engine.name, 280 alias: aliases[0], 281 } 282 ); 283 } 284 285 /** 286 * Overrides the willHide method in the superclass to account for the local 287 * search mode buttons. 288 * 289 * @returns {Promise<boolean>} 290 * True if we will hide the one-offs when they are requested. 291 */ 292 async willHide() { 293 // We need to call super.willHide() even when we return false below because 294 // it has the necessary side effect of creating this._engineInfo. 295 let superWillHide = await super.willHide(); 296 if ( 297 lazy.UrlbarUtils.LOCAL_SEARCH_MODES.some(m => 298 lazy.UrlbarPrefs.get(m.pref) 299 ) 300 ) { 301 return false; 302 } 303 return superWillHide; 304 } 305 306 /** 307 * Called when a pref tracked by UrlbarPrefs changes. 308 * 309 * @param {string} changedPref 310 * The name of the pref, relative to `browser.urlbar.` if the pref is in 311 * that branch. 312 */ 313 onPrefChanged(changedPref) { 314 // Invalidate the engine cache when the local-one-offs-related prefs change 315 // so that the one-offs rebuild themselves the next time the view opens. 316 if ( 317 [...lazy.UrlbarUtils.LOCAL_SEARCH_MODES.map(m => m.pref)].includes( 318 changedPref 319 ) 320 ) { 321 this.invalidateCache(); 322 } 323 } 324 325 /** 326 * Overrides _rebuildEngineList to add the local one-offs. 327 * 328 * @param {Array} engines 329 * The search engines to add. 330 * @param {Array} addEngines 331 * The engines that can be added. 332 */ 333 async _rebuildEngineList(engines, addEngines) { 334 await super._rebuildEngineList(engines, addEngines); 335 336 const messageIDs = { 337 actions: "search-one-offs-actions", 338 bookmarks: "search-one-offs-bookmarks", 339 history: "search-one-offs-history", 340 tabs: "search-one-offs-tabs", 341 }; 342 for (let { source, pref, restrict } of lazy.UrlbarUtils 343 .LOCAL_SEARCH_MODES) { 344 if (!lazy.UrlbarPrefs.get(pref)) { 345 continue; 346 } 347 let name = lazy.UrlbarUtils.getResultSourceName(source); 348 let button = this.document.createXULElement("button"); 349 button.id = `urlbar-engine-one-off-item-${name}`; 350 button.setAttribute("class", "searchbar-engine-one-off-item"); 351 button.setAttribute("tabindex", "-1"); 352 this.document.l10n.setAttributes(button, messageIDs[name], { 353 restrict, 354 }); 355 button.source = source; 356 this.buttons.appendChild(button); 357 } 358 } 359 360 /** 361 * Overrides the superclass's click listener to handle clicks on local 362 * one-offs in addition to engine one-offs. 363 * 364 * @param {MouseEvent} event 365 * The click event. 366 */ 367 _on_click(event) { 368 // Ignore right clicks. 369 if (event.button == 2) { 370 return; 371 } 372 373 let button = /** @type {LegacySearchOneOffButton} */ (event.originalTarget); 374 375 if (!button.engine && !button.source) { 376 return; 377 } 378 379 this.selectedButton = button; 380 this.handleSearchCommand(event, { 381 engineName: button.engine?.name, 382 source: button.source, 383 entry: "oneoff", 384 }); 385 } 386 387 /** 388 * Overrides the superclass's contextmenu listener to handle the context menu. 389 * 390 * @param {event} event 391 * The contextmenu event. 392 */ 393 _on_contextmenu(event) { 394 // Prevent the context menu from appearing. 395 event.preventDefault(); 396 } 397 }