tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 ]);