MacTouchBar.sys.mjs (19725B)
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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 11 UrlbarTokenizer: 12 "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", 13 }); 14 15 XPCOMUtils.defineLazyServiceGetter( 16 lazy, 17 "touchBarUpdater", 18 "@mozilla.org/widget/touchbarupdater;1", 19 Ci.nsITouchBarUpdater 20 ); 21 22 // For accessing TouchBarHelper methods from static contexts in this file. 23 XPCOMUtils.defineLazyServiceGetter( 24 lazy, 25 "touchBarHelper", 26 "@mozilla.org/widget/touchbarhelper;1", 27 Ci.nsITouchBarHelper 28 ); 29 30 /** 31 * Executes a XUL command on the top window. Called by the callbacks in each 32 * TouchBarInput. 33 * 34 * @param {string} commandName 35 * A XUL command. 36 */ 37 function execCommand(commandName) { 38 if (!TouchBarHelper.window) { 39 return; 40 } 41 let command = TouchBarHelper.window.document.getElementById(commandName); 42 if (command) { 43 command.doCommand(); 44 } 45 } 46 47 /** 48 * Static helper function to convert a hexadecimal string to its integer 49 * value. Used to convert colours to a format accepted by Apple's NSColor code. 50 * 51 * @param {string} hexString 52 * A hexadecimal string, optionally beginning with '#'. 53 */ 54 function hexToInt(hexString) { 55 if (!hexString) { 56 return null; 57 } 58 if (hexString.charAt(0) == "#") { 59 hexString = hexString.slice(1); 60 } 61 let val = parseInt(hexString, 16); 62 return isNaN(val) ? null : val; 63 } 64 65 const kInputTypes = { 66 BUTTON: "button", 67 LABEL: "label", 68 MAIN_BUTTON: "mainButton", 69 POPOVER: "popover", 70 SCROLLVIEW: "scrollView", 71 SCRUBBER: "scrubber", 72 }; 73 74 /** 75 * An object containing all implemented TouchBarInput objects. 76 */ 77 var gBuiltInInputs = { 78 Back: { 79 title: "back", 80 image: "chrome://browser/skin/back.svg", 81 type: kInputTypes.BUTTON, 82 callback: () => { 83 lazy.touchBarHelper.unfocusUrlbar(); 84 execCommand("Browser:Back"); 85 }, 86 }, 87 Forward: { 88 title: "forward", 89 image: "chrome://browser/skin/forward.svg", 90 type: kInputTypes.BUTTON, 91 callback: () => { 92 lazy.touchBarHelper.unfocusUrlbar(); 93 execCommand("Browser:Forward"); 94 }, 95 }, 96 Reload: { 97 title: "reload", 98 image: "chrome://global/skin/icons/reload.svg", 99 type: kInputTypes.BUTTON, 100 callback: () => { 101 lazy.touchBarHelper.unfocusUrlbar(); 102 execCommand("Browser:Reload"); 103 }, 104 }, 105 Home: { 106 title: "home", 107 image: "chrome://browser/skin/home.svg", 108 type: kInputTypes.BUTTON, 109 callback: () => { 110 let win = lazy.BrowserWindowTracker.getTopWindow(); 111 win.BrowserCommands.home(); 112 }, 113 }, 114 Fullscreen: { 115 title: "fullscreen", 116 image: "chrome://browser/skin/fullscreen.svg", 117 type: kInputTypes.BUTTON, 118 callback: () => execCommand("View:FullScreen"), 119 }, 120 Find: { 121 title: "find", 122 image: "chrome://global/skin/icons/search-glass.svg", 123 type: kInputTypes.BUTTON, 124 callback: () => execCommand("cmd_find"), 125 }, 126 NewTab: { 127 title: "new-tab", 128 image: "chrome://global/skin/icons/plus.svg", 129 type: kInputTypes.BUTTON, 130 callback: () => execCommand("cmd_newNavigatorTabNoEvent"), 131 }, 132 Sidebar: { 133 title: "open-sidebar", 134 image: "chrome://browser/skin/sidebars.svg", 135 type: kInputTypes.BUTTON, 136 callback: () => { 137 let win = lazy.BrowserWindowTracker.getTopWindow(); 138 win.SidebarController.toggle(); 139 }, 140 }, 141 AddBookmark: { 142 title: "add-bookmark", 143 image: "chrome://browser/skin/bookmark-hollow.svg", 144 type: kInputTypes.BUTTON, 145 callback: () => execCommand("Browser:AddBookmarkAs"), 146 }, 147 ReaderView: { 148 title: "reader-view", 149 image: "chrome://browser/skin/reader-mode.svg", 150 type: kInputTypes.BUTTON, 151 callback: () => execCommand("View:ReaderView"), 152 disabled: true, // Updated when the page is found to be Reader View-able. 153 }, 154 OpenLocation: { 155 key: "open-location", 156 title: "open-location", 157 image: "chrome://global/skin/icons/search-glass.svg", 158 type: kInputTypes.MAIN_BUTTON, 159 callback: () => lazy.touchBarHelper.toggleFocusUrlbar(), 160 }, 161 // This is a special-case `type: kInputTypes.SCRUBBER` element. 162 // Scrubbers are not yet generally implemented. 163 // See follow-up bug 1502539. 164 Share: { 165 title: "share", 166 image: "chrome://browser/skin/share.svg", 167 type: kInputTypes.SCRUBBER, 168 callback: () => execCommand("cmd_share"), 169 }, 170 SearchPopover: { 171 title: "search-popover", 172 image: "chrome://global/skin/icons/search-glass.svg", 173 type: kInputTypes.POPOVER, 174 children: { 175 SearchScrollViewLabel: { 176 title: "search-search-in", 177 type: kInputTypes.LABEL, 178 }, 179 SearchScrollView: { 180 key: "search-scrollview", 181 type: kInputTypes.SCROLLVIEW, 182 children: { 183 Bookmarks: { 184 title: "search-bookmarks", 185 type: kInputTypes.BUTTON, 186 callback: () => 187 lazy.touchBarHelper.insertRestrictionInUrlbar( 188 lazy.UrlbarTokenizer.RESTRICT.BOOKMARK 189 ), 190 }, 191 OpenTabs: { 192 title: "search-opentabs", 193 type: kInputTypes.BUTTON, 194 callback: () => 195 lazy.touchBarHelper.insertRestrictionInUrlbar( 196 lazy.UrlbarTokenizer.RESTRICT.OPENPAGE 197 ), 198 }, 199 History: { 200 title: "search-history", 201 type: kInputTypes.BUTTON, 202 callback: () => 203 lazy.touchBarHelper.insertRestrictionInUrlbar( 204 lazy.UrlbarTokenizer.RESTRICT.HISTORY 205 ), 206 }, 207 Tags: { 208 title: "search-tags", 209 type: kInputTypes.BUTTON, 210 callback: () => 211 lazy.touchBarHelper.insertRestrictionInUrlbar( 212 lazy.UrlbarTokenizer.RESTRICT.TAG 213 ), 214 }, 215 }, 216 }, 217 }, 218 }, 219 }; 220 221 // We create a new flat object to cache strings. Since gBuiltInInputs is a 222 // tree, caching/retrieval of localized strings would otherwise require tree 223 // traversal. 224 var localizedStrings = {}; 225 226 const kHelperObservers = new Set([ 227 "bookmark-icon-updated", 228 "fullscreen-painted", 229 "reader-mode-available", 230 "touchbar-location-change", 231 "quit-application", 232 "intl:app-locales-changed", 233 "urlbar-focus", 234 "urlbar-blur", 235 ]); 236 237 /** 238 * JS-implemented TouchBarHelper class. 239 * Provides services to the Mac Touch Bar. 240 */ 241 export class TouchBarHelper { 242 constructor() { 243 for (let topic of kHelperObservers) { 244 Services.obs.addObserver(this, topic); 245 } 246 // We cache our search popover since otherwise it is frequently 247 // created/destroyed for the urlbar-focus/blur events. 248 this._searchPopover = this.getTouchBarInput("SearchPopover"); 249 250 this._inputsNotUpdated = new Set(); 251 } 252 253 destructor() { 254 this._searchPopover = null; 255 for (let topic of kHelperObservers) { 256 Services.obs.removeObserver(this, topic); 257 } 258 } 259 260 get activeTitle() { 261 if (!TouchBarHelper.window) { 262 return ""; 263 } 264 let tabbrowser = TouchBarHelper.window.ownerGlobal.gBrowser; 265 let activeTitle; 266 if (tabbrowser) { 267 activeTitle = tabbrowser.selectedBrowser.contentTitle; 268 } 269 return activeTitle; 270 } 271 272 get allItems() { 273 let layoutItems = Cc["@mozilla.org/array;1"].createInstance( 274 Ci.nsIMutableArray 275 ); 276 277 let window = TouchBarHelper.window; 278 if ( 279 !window || 280 !window.isChromeWindow || 281 window.document.documentElement.getAttribute("windowtype") != 282 "navigator:browser" 283 ) { 284 return layoutItems; 285 } 286 287 // Every input must be updated at least once so that all assets (titles, 288 // icons) are loaded. We keep track of which inputs haven't updated and 289 // run an update on them ASAP. 290 this._inputsNotUpdated.clear(); 291 292 for (let inputName of Object.keys(gBuiltInInputs)) { 293 let input = this.getTouchBarInput(inputName); 294 if (!input) { 295 continue; 296 } 297 this._inputsNotUpdated.add(inputName); 298 layoutItems.appendElement(input); 299 } 300 301 return layoutItems; 302 } 303 304 static get window() { 305 return lazy.BrowserWindowTracker.getTopWindow(); 306 } 307 308 get document() { 309 if (!TouchBarHelper.window) { 310 return null; 311 } 312 return TouchBarHelper.window.document; 313 } 314 315 get isUrlbarFocused() { 316 if (!TouchBarHelper.window || !TouchBarHelper.window.gURLBar) { 317 return false; 318 } 319 return TouchBarHelper.window.gURLBar.focused; 320 } 321 322 toggleFocusUrlbar() { 323 if (this.isUrlbarFocused) { 324 this.unfocusUrlbar(); 325 } else { 326 execCommand("Browser:OpenLocation"); 327 } 328 } 329 330 unfocusUrlbar() { 331 if (!this.isUrlbarFocused) { 332 return; 333 } 334 TouchBarHelper.window.gURLBar.blur(); 335 } 336 337 static get baseWindow() { 338 return TouchBarHelper.window 339 ? TouchBarHelper.window.docShell.treeOwner.QueryInterface( 340 Ci.nsIBaseWindow 341 ) 342 : null; 343 } 344 345 getTouchBarInput(inputName) { 346 if (inputName == "SearchPopover" && this._searchPopover) { 347 return this._searchPopover; 348 } 349 350 if (!inputName || !gBuiltInInputs.hasOwnProperty(inputName)) { 351 return null; 352 } 353 354 let inputData = gBuiltInInputs[inputName]; 355 356 let item = new TouchBarInput(inputData); 357 358 // Skip localization if there is already a cached localized title or if 359 // no title is needed. 360 if ( 361 !inputData.hasOwnProperty("title") || 362 localizedStrings[inputData.title] 363 ) { 364 return item; 365 } 366 367 // Async l10n fills in the localized input labels after the initial load. 368 this._l10n.formatValue(inputData.title).then(result => { 369 item.title = result; 370 localizedStrings[inputData.title] = result; // Cache result. 371 // Checking TouchBarHelper.window since this callback can fire after all windows are closed. 372 if (TouchBarHelper.window) { 373 if (this._inputsNotUpdated) { 374 this._inputsNotUpdated.delete(inputName); 375 } 376 lazy.touchBarUpdater.updateTouchBarInputs(TouchBarHelper.baseWindow, [ 377 item, 378 ]); 379 } 380 }); 381 382 return item; 383 } 384 385 /** 386 * Fetches a specific Touch Bar Input by name and updates it on the Touch Bar. 387 * 388 * @param {...*} inputNames 389 * A key/keys to a value/values in the gBuiltInInputs object in this file. 390 */ 391 _updateTouchBarInputs(...inputNames) { 392 if (!TouchBarHelper.window || !inputNames.length) { 393 return; 394 } 395 396 let inputs = []; 397 for (let inputName of new Set([...inputNames, ...this._inputsNotUpdated])) { 398 let input = this.getTouchBarInput(inputName); 399 if (!input) { 400 continue; 401 } 402 403 this._inputsNotUpdated.delete(inputName); 404 inputs.push(input); 405 } 406 407 lazy.touchBarUpdater.updateTouchBarInputs( 408 TouchBarHelper.baseWindow, 409 inputs 410 ); 411 } 412 413 /** 414 * Inserts a restriction token into the Urlbar ahead of the current typed 415 * search term. 416 * 417 * @param {string} restrictionToken 418 * The restriction token to be inserted into the Urlbar. Preferably 419 * sourced from UrlbarTokenizer.RESTRICT. 420 */ 421 insertRestrictionInUrlbar(restrictionToken) { 422 if (!TouchBarHelper.window) { 423 return; 424 } 425 let searchString = ""; 426 if ( 427 TouchBarHelper.window.gURLBar.getAttribute("pageproxystate") != "valid" 428 ) { 429 searchString = TouchBarHelper.window.gURLBar.lastSearchString.trimStart(); 430 if ( 431 Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(searchString[0]) 432 ) { 433 searchString = searchString.substring(1).trimStart(); 434 } 435 } 436 437 TouchBarHelper.window.gURLBar.search( 438 `${restrictionToken} ${searchString}`, 439 { searchModeEntry: "touchbar" } 440 ); 441 } 442 443 observe(subject, topic, data) { 444 switch (topic) { 445 case "touchbar-location-change": { 446 let updatedInputs = ["Back", "Forward"]; 447 gBuiltInInputs.Back.disabled = 448 !TouchBarHelper.window.gBrowser.canGoBack; 449 gBuiltInInputs.Forward.disabled = 450 !TouchBarHelper.window.gBrowser.canGoForward; 451 if (subject.QueryInterface(Ci.nsIWebProgress)?.isTopLevel) { 452 this.activeUrl = data; 453 // ReaderView button is disabled on every toplevel location change 454 // since Reader View must determine if the new page can be Reader 455 // Viewed. 456 updatedInputs.push("ReaderView"); 457 gBuiltInInputs.ReaderView.disabled = !data.startsWith("about:reader"); 458 } 459 this._updateTouchBarInputs(...updatedInputs); 460 break; 461 } 462 case "fullscreen-painted": 463 if (TouchBarHelper.window.document.fullscreenElement) { 464 gBuiltInInputs.OpenLocation.title = "touchbar-fullscreen-exit"; 465 gBuiltInInputs.OpenLocation.image = 466 "chrome://browser/skin/fullscreen-exit.svg"; 467 gBuiltInInputs.OpenLocation.callback = () => { 468 TouchBarHelper.window.windowUtils.exitFullscreen(); 469 }; 470 } else { 471 gBuiltInInputs.OpenLocation.title = "open-location"; 472 gBuiltInInputs.OpenLocation.image = 473 "chrome://global/skin/icons/search-glass.svg"; 474 gBuiltInInputs.OpenLocation.callback = () => 475 execCommand("Browser:OpenLocation", "OpenLocation"); 476 } 477 this._updateTouchBarInputs("OpenLocation"); 478 break; 479 case "bookmark-icon-updated": 480 gBuiltInInputs.AddBookmark.image = 481 data == "starred" 482 ? "chrome://browser/skin/bookmark.svg" 483 : "chrome://browser/skin/bookmark-hollow.svg"; 484 this._updateTouchBarInputs("AddBookmark"); 485 break; 486 case "reader-mode-available": 487 gBuiltInInputs.ReaderView.disabled = false; 488 this._updateTouchBarInputs("ReaderView"); 489 break; 490 case "urlbar-focus": 491 if (!this._searchPopover) { 492 this._searchPopover = this.getTouchBarInput("SearchPopover"); 493 } 494 lazy.touchBarUpdater.showPopover( 495 TouchBarHelper.baseWindow, 496 this._searchPopover, 497 true 498 ); 499 break; 500 case "urlbar-blur": 501 if (!this._searchPopover) { 502 this._searchPopover = this.getTouchBarInput("SearchPopover"); 503 } 504 lazy.touchBarUpdater.showPopover( 505 TouchBarHelper.baseWindow, 506 this._searchPopover, 507 false 508 ); 509 break; 510 case "intl:app-locales-changed": 511 this._searchPopover = null; 512 localizedStrings = {}; 513 514 // This event can fire before this._l10n updates to switch languages, 515 // so all the new translations are in the old language. To avoid this, 516 // we need to reinitialize this._l10n. 517 this._l10n = new Localization(["browser/touchbar/touchbar.ftl"]); 518 helperProto._l10n = this._l10n; 519 520 this._updateTouchBarInputs(...Object.keys(gBuiltInInputs)); 521 break; 522 case "quit-application": 523 this.destructor(); 524 break; 525 } 526 } 527 } 528 529 const helperProto = TouchBarHelper.prototype; 530 helperProto.QueryInterface = ChromeUtils.generateQI(["nsITouchBarHelper"]); 531 helperProto._l10n = new Localization(["browser/touchbar/touchbar.ftl"]); 532 533 /** 534 * A representation of a Touch Bar input. 535 * 536 * @param {object} input 537 * An object representing a Touch Bar Input. 538 * Contains listed properties. 539 * @param {string} input.title 540 * The lookup key for the button's localized text title. 541 * @param {string} input.image 542 * A URL pointing to an SVG internal to Firefox. 543 * @param {string} input.type 544 * The type of Touch Bar input represented by the object. 545 * Must be a value from kInputTypes. 546 * @param {Function} input.callback 547 * A callback invoked when a touchbar item is touched. 548 * @param {string} [input.color] 549 * A string in hex format specifying the button's background color. 550 * If omitted, the default background color is used. 551 * @param {bool} [input.disabled] 552 * If `true`, the Touch Bar input is greyed out and inoperable. 553 * @param {Array} [input.children] 554 * An array of input objects that will be displayed as children of 555 * this input. Available only for types KInputTypes.POPOVER and 556 * kInputTypes.SCROLLVIEW. 557 */ 558 export class TouchBarInput { 559 constructor(input) { 560 this._key = input.key || input.title; 561 this._title = localizedStrings[input.title] || ""; 562 this._image = input.image; 563 this._type = input.type; 564 this._callback = input.callback; 565 this._color = hexToInt(input.color); 566 this._disabled = input.hasOwnProperty("disabled") ? input.disabled : false; 567 if (input.children) { 568 this._children = []; 569 let toLocalize = []; 570 for (let childData of Object.values(input.children)) { 571 let initializedChild = new TouchBarInput(childData); 572 if (!initializedChild) { 573 continue; 574 } 575 // Children's types are prepended by the parent's type. This is so we 576 // can uniquely identify a child input from a standalone input with 577 // the same name. (e.g. a button called "back" in a popover would be a 578 // "popover-button.back" vs. a "button.back"). 579 initializedChild.type = input.type + "-" + initializedChild.type; 580 this._children.push(initializedChild); 581 // Skip l10n for inputs without a title or those already localized. 582 if (childData.title && !localizedStrings[childData.title]) { 583 toLocalize.push(initializedChild); 584 } 585 } 586 this._localizeChildren(toLocalize); 587 } 588 } 589 590 get key() { 591 return this._key; 592 } 593 get title() { 594 return this._title; 595 } 596 set title(title) { 597 this._title = title; 598 } 599 get image() { 600 return this._image ? Services.io.newURI(this._image) : null; 601 } 602 set image(image) { 603 this._image = image; 604 } 605 get type() { 606 return this._type == "" ? "button" : this._type; 607 } 608 set type(type) { 609 this._type = type; 610 } 611 get callback() { 612 return this._callback; 613 } 614 set callback(callback) { 615 this._callback = callback; 616 } 617 get color() { 618 return this._color; 619 } 620 set color(color) { 621 this._color = this.hexToInt(color); 622 } 623 get disabled() { 624 return this._disabled || false; 625 } 626 set disabled(disabled) { 627 this._disabled = disabled; 628 } 629 get children() { 630 if (!this._children) { 631 return null; 632 } 633 let children = Cc["@mozilla.org/array;1"].createInstance( 634 Ci.nsIMutableArray 635 ); 636 for (let child of this._children) { 637 children.appendElement(child); 638 } 639 return children; 640 } 641 642 /** 643 * Apply Fluent l10n to child inputs. 644 * 645 * @param {Array} children 646 * An array of initialized TouchBarInputs. 647 */ 648 async _localizeChildren(children) { 649 if (!children || !children.length) { 650 return; 651 } 652 653 let titles = await helperProto._l10n.formatValues( 654 children.map(child => ({ id: child.key })) 655 ); 656 // In the TouchBarInput constuctor, we filtered so children contains only 657 // those inputs with titles to be localized. We can be confident that the 658 // results in titles match up with the inputs to be localized. 659 children.forEach(function (child, index) { 660 child.title = titles[index]; 661 localizedStrings[child.key] = child.title; 662 }); 663 664 lazy.touchBarUpdater.updateTouchBarInputs( 665 TouchBarHelper.baseWindow, 666 children 667 ); 668 } 669 } 670 671 TouchBarInput.prototype.QueryInterface = ChromeUtils.generateQI([ 672 "nsITouchBarInput", 673 ]);