触控栏

触控栏是 2016 年发布的一些 MacBook Pro 上的硬件组件。它是一个位于键盘上方的显示屏,允许比普通键盘更灵活的输入类型。Apple 提供了触控栏 API,以便开发人员可以扩展触控栏以显示特定于其应用程序的输入。Firefox 使用这些 API 在触控栏中提供可自定义的行输入。

在 Apple 的文档中,“触控栏”一词指的是硬件。“一个触控栏”一词指的是显示在触控栏上的输入集合,而不是硬件。这意味着当用户切换上下文时,可以有多个“触控栏”切换显示。本文档中使用了相同的命名约定。

在本文档和代码中,“输入”一词用于指代触控栏中的交互式元素。它通常可以与“按钮”互换,但“输入”也可以指代触控栏中显示的任何元素。

触控栏绝不应提供不适用于没有触控栏的 Firefox 用户的功能。大多数 macOS Firefox 用户没有触控栏,有些用户选择禁用它。Apple 自身的 人机界面指南 (HIG) 禁止这种触控栏功能。在计划实施新的触控栏功能之前,请阅读 HIG 以获取更多设计注意事项。

如果您对触控栏有任何疑问,而本文档中没有解答,请随时联系 Harry Twyford (:harry on Slack)。他编写了本文档和 Firefox 的初始触控栏实现。

概述

Firefox 的触控栏实现包含 JavaScript 和 Cocoa (Objective-C++) 两部分。JavaScript 代码位于 browser/components/touchbar 中,Cocoa 代码位于 widget/cocoa 中,主要在 nsTouchBar.mm 中。Cocoa 代码是 Apple 触控栏 API 的使用者,并定义了哪些类型的触控栏输入可供其自身使用者使用。browser/components/touchbar 中的 JS 代码为 nsTouchBar.mm 提供服务,并定义了用户在触控栏中实际看到的输入。JS 和 Cocoa 之间存在双向通信:Cocoa 代码询问 JS 应该显示哪些输入,而 JS 则要求 Cocoa 代码在需要时更新这些输入。

JavaScript API

browser/components/touchbar/MacTouchBar.sys.mjs 定义了哪些特定输入可供用户使用,它们将具有什么图标,将执行什么操作,等等。输入在 gBuiltInInputs 对象中定义 该文件中的位置。在 gBuiltInInputs 中创建新对象时,可用属性在 TouchBarInput 的 JSDoc 中有说明

/**
 * A representation of a Touch Bar input.
 *     @param {string} input.title
 *            The lookup key for the button's localized text title.
 *     @param {string} input.image
 *            A URL pointing to an SVG internal to Firefox.
 *     @param {string} input.type
 *            The type of Touch Bar input represented by the object.
 *            Must be a value from kInputTypes.
 *     @param {Function} input.callback
 *            A callback invoked when a touchbar item is touched.
 *     @param {string} [input.color]
 *            A string in hex format specifying the button's background color.
 *            If omitted, the default background color is used.
 *     @param {bool} [input.disabled]
 *            If `true`, the Touch Bar input is greyed out and inoperable.
 *     @param {Array} [input.children]
 *            An array of input objects that will be displayed as children of
 *            this input. Available only for types KInputTypes.POPOVER and
 *            kInputTypes.SCROLLVIEW.
 */

需要对其中一些属性进行说明。

  • titlebrowser/locales/<LOCALE>/browser/touchbar/touchbar.ftl 中定义的 Fluent 翻译的键。

  • type 必须是 MacTouchBar.sys.mjskInputTypes 枚举中的一个值。例如,kInputTypes.BUTTON。有关输入类型的更多信息如下。

  • callback 指向一个 JavaScript 函数。可以执行任何 chrome 级别的 JavaScript。 execCommandMacTouchBar.sys.mjs 中的一个便捷方法,它将 XUL 命令作为字符串并执行该命令。例如,一个输入将 callback 设置为 execCommand("Browser:Back")

  • children 是一个对象的数组,这些对象与 gBuiltInInputs 的成员具有相同的属性。当与类型为 kInputTypes.SCROLLVIEW 的输入一起使用时,children 只能包含类型为 kInputTypes.BUTTON 的输入。当与类型为 kInputTypes.POPOVER 的输入一起使用时,可以使用除另一个 kInputTypes.POPOVER 之外的任何输入类型。

输入类型

按钮

一个简单的按钮。如果未指定 image,则按钮显示来自 title 的文本标签。如果同时指定了 imagetitle,则仅显示 image。当按下按钮时,将执行在 callback 中指定的动作。

注意

即使 title 不会显示在触控栏中,您仍然必须定义一个 title 属性。

主按钮

类似于按钮,但显示宽度为两倍。主按钮同时显示 title 中的字符串和 image 中的图标。在任何时间点,触控栏中只能显示一个主按钮,尽管这并非强制执行。

标签

一个非交互式文本标签。此输入仅采用属性 titletype

弹出窗口

最初在触控栏中表示为按钮,弹出窗口在按下时将显示一组完全不同的输入。这些不同的输入应在父级的 children 属性中定义。弹出窗口也可以通过编程方式显示和隐藏,方法是调用

gTouchBarUpdater.showPopover(
  TouchBarHelper.baseWindow,
  [POPOVER],
  {true | false}
);

其中第二个参数是弹出窗口 TouchBarInput 的引用,第三个参数是弹出窗口是否应该显示或隐藏。

滚动视图

滚动视图是按钮的滚动列表。按钮应在滚动视图的 children 数组中定义。

注意

在 Firefox 中,当地址栏获得焦点时,触控栏中会出现搜索快捷方式列表。这是包含在弹出窗口中的 ScrollView 的一个示例。当地址栏获得焦点时,弹出窗口会通过 gTouchBarUpdater.showPopover 以编程方式打开,当地址栏失去焦点时,它会隐藏。

示例

以下是一些 gBuiltInInputs 对象的示例。

一个简单的按钮

Back: {
  title: "back",
  image: "chrome://browser/skin/back.svg",
  type: kInputTypes.BUTTON,
  callback: () => execCommand("Browser:Back", "Back"),
},

使用标题、图标、类型和回调定义按钮。回调只是调用 XUL 命令以返回。

搜索弹出窗口

这是当地址栏获得焦点时占据触控栏的输入。

SearchPopover: {
  title: "search-popover",
  image: "chrome://global/skin/icons/search-glass.svg",
  type: kInputTypes.POPOVER,
  children: {
    SearchScrollViewLabel: {
      title: "search-search-in",
      type: kInputTypes.LABEL,
    },
    SearchScrollView: {
      key: "search-scrollview",
      type: kInputTypes.SCROLLVIEW,
      children: {
        Bookmarks: {
          title: "search-bookmarks",
          type: kInputTypes.BUTTON,
          callback: () =>
            gTouchBarHelper.insertRestrictionInUrlbar(
              UrlbarTokenizer.RESTRICT.BOOKMARK
            ),
        },
        History: {
          title: "search-history",
          type: kInputTypes.BUTTON,
          callback: () =>
            gTouchBarHelper.insertRestrictionInUrlbar(
              UrlbarTokenizer.RESTRICT.HISTORY
            ),
        },
        OpenTabs: {
          title: "search-opentabs",
          type: kInputTypes.BUTTON,
          callback: () =>
            gTouchBarHelper.insertRestrictionInUrlbar(
              UrlbarTokenizer.RESTRICT.OPENPAGE
            ),
        },
        Tags: {
          title: "search-tags",
          type: kInputTypes.BUTTON,
          callback: () =>
            gTouchBarHelper.insertRestrictionInUrlbar(
              UrlbarTokenizer.RESTRICT.TAG
            ),
        },
        Titles: {
          title: "search-titles",
          type: kInputTypes.BUTTON,
          callback: () =>
            gTouchBarHelper.insertRestrictionInUrlbar(
              UrlbarTokenizer.RESTRICT.TITLE
            ),
        },
      },
    },
  },
},

在顶层,定义了一个弹出窗口。这允许在单独的触控栏中显示一组子项。弹出窗口有两个子项:一个标签和一个滚动视图。滚动视图显示五个类似的按钮,这些按钮调用一个辅助方法以将搜索快捷方式符号插入地址栏。

添加新的输入

添加新的输入很容易:只需向 gBuiltInInputs 添加一个新对象即可。这将使输入在触控栏自定义窗口中可用(可从 Firefox 菜单栏项访问)。

如果您想将新输入添加到默认集中,请添加其标识符 此处,其中 type 是该文件中 kAllowedInputTypes 中的值,而 key 是您在 gBuiltInInputs 中为 title 设置的值。在更改默认输入集之前,您应该请求 UX 的批准。

如果您有兴趣为 Firefox 的触控栏 API 实现添加新功能,请继续阅读!

Cocoa API

Firefox 在其 Widget 中实现了 Apple 的 Touch Bar API:使用带有 nsTouchBar 类的 Cocoa 代码。 nsTouchBar 在 Apple 的 Touch Bar API 和 TouchBarHelper JavaScript API 之间进行接口交互。

了解 Touch Bar API 的最佳资源是 Apple 的 官方文档。此文档将介绍 Firefox 如何实现这些 API 以及如何扩展 nsTouchBar 以启用新的 Touch Bar 功能。

每个新的 Firefox 窗口都会初始化 nsTouchBar (链接)。函数 makeTouchBar 会自动在每个新的 NSWindow* 实例上查找。如果定义了 makeTouchBar,则该窗口将拥有一个新的 nsTouchBar 实例。

在撰写本文时,每个窗口都使用一组默认输入初始化 nsTouchBar。将来,除了主浏览器窗口之外的其他 Firefox 窗口(例如库窗口或开发者工具)可能会使用不同的输入集初始化 nsTouchBar

nsTouchBar 有两种不同的初始化方法:initinitWithInputs。前者是后者的便捷方法,使用 nil 参数调用 initWithInputs。发生这种情况时,将创建一个包含一组默认输入的 Touch Bar。 initWithInputs 还可以接受一个 NSArray<TouchBarInput*>*。在这种情况下,一个不可自定义的 Touch Bar 将仅使用这些可用的输入进行初始化。

NSTouchBarItemIdentifiers

Touch Bar 的架构主要基于一个名为 NSTouchBarItemIdentifierNSString* 包装类。Touch Bar 中的每个输入都有一个唯一的 NSTouchBarItemIdentifier。它们的结构采用反向 URI 格式,如下所示

com.mozilla.firefox.touchbar.[TYPE].[KEY]

[TYPE] 是一个字符串,指示输入的类型,例如“button”。如果一个输入是另一个输入的子级,则父级的类型会附加到子级的类型之前,例如“scrubber.button”表示包含在滑块中的按钮。

[KEY] 是 JS 端为该输入定义的 title 属性。

如果需要生成标识符,请使用便捷方法 [TouchBarInput nativeIdentifierWithType:withKey:]

注意

不要创建与任何其他输入具有相同标识符的新输入。所有标识符必须唯一。

警告

NSTouchBarItemIdentifier 在另一个地方使用:设置 customizationIdentifier。切勿更改此字符串。如果更改了它,用户在 Firefox 中对 Touch Bar 布局所做的任何自定义都将被擦除。

每个标识符都与一个 TouchBarInput 相关联。 TouchBarInput 是一个类,它保存为 gBuiltInInputs 中每个输入指定的属性。 nsTouchBar 使用它们来创建 NSTouchBarItem 的实例,这些实例是 Apple 的 Touch Bar API 实际使用的对象,并在 Touch Bar 中显示。了解 TouchBarInputNSTouchBarItem 之间的区别非常重要!

TouchBarInput 创建流程

创建 Touch Bar 及其 TouchBarInputs 的流程如下

  1. [nsTouchBar init][NSWindow makeTouchBar] 调用。

  2. init 将填充两个 NSArrays:customizationAllowedItemIdentifiersdefaultItemIdentifiers。它还为这两个数组的并集中的每个元素初始化一个 TouchBarInput 对象,并将它们存储在 NSMutableDictionary<NSTouchBarItemIdentifier, TouchBarInput*>* mappedLayoutItems 中。

  3. touchBar:makeItemForIdentifier: 会针对这两个标识符数组的并集中的每个元素调用。此方法检索给定标识符的 TouchBarInput,并使用它来初始化 NSTouchBarItemtouchBar:makeItemForIdentifier:TouchBarInput 中读取 type 属性以确定应初始化哪个 NSTouchBarItem 子类。我们的 Touch Bar 代码目前支持 NSCustomTouchBarItem(按钮,主按钮); NSPopoverTouchBarItem(弹出窗口); NSTextField(标签);和 NSScrollView(滚动视图)。

  4. 初始化 NSTouchBarItem 后,其属性将填充各种“更新”方法。这些包括 updateButtonupdateMainButtonupdateLabelupdatePopoverupdateScrollView

  5. 由于 TouchBarInput 标题的本地化在 JavaScript 代码中异步发生,因此 l10n 回调会执行 [nsTouchBarUpdater updateTouchBarInputs:]。此方法读取需要更新的输入的标识符,并调用其各自的“更新”方法。此方法最常用于在 l10n 完成后更新 title。它还可以用于更新 TouchBarInput 的任何属性;例如,当浏览器中发生特定事件时,可能希望更改 color