touch-bar.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. import { EventEmitter } from 'events';
  2. let nextItemID = 1;
  3. const hiddenProperties = Symbol('hidden touch bar props');
  4. const extendConstructHook = (target: any, hook: Function) => {
  5. const existingHook = target._hook;
  6. target._hook = function () {
  7. if (existingHook) existingHook.call(this);
  8. hook.call(this);
  9. };
  10. };
  11. const ImmutableProperty = <T extends TouchBarItem<any>>(def: (config: T extends TouchBarItem<infer C> ? C : never, setInternalProp: <K extends keyof T>(k: K, v: T[K]) => void) => any) => (target: T, propertyKey: keyof T) => {
  12. extendConstructHook(target, function (this: T) {
  13. (this as any)[hiddenProperties][propertyKey] = def((this as any)._config, (k, v) => {
  14. (this as any)[hiddenProperties][k] = v;
  15. });
  16. });
  17. Object.defineProperty(target, propertyKey, {
  18. get: function () {
  19. return this[hiddenProperties][propertyKey];
  20. },
  21. set: function () {
  22. throw new Error(`Cannot override property ${name}`);
  23. },
  24. enumerable: true,
  25. configurable: false
  26. });
  27. };
  28. const LiveProperty = <T extends TouchBarItem<any>>(def: (config: T extends TouchBarItem<infer C> ? C : never) => any, onMutate?: (self: T, newValue: any) => void) => (target: T, propertyKey: keyof T) => {
  29. extendConstructHook(target, function (this: T) {
  30. (this as any)[hiddenProperties][propertyKey] = def((this as any)._config);
  31. if (onMutate) onMutate((this as any), (this as any)[hiddenProperties][propertyKey]);
  32. });
  33. Object.defineProperty(target, propertyKey, {
  34. get: function () {
  35. return this[hiddenProperties][propertyKey];
  36. },
  37. set: function (value) {
  38. if (onMutate) onMutate((this as any), value);
  39. this[hiddenProperties][propertyKey] = value;
  40. this.emit('change', this);
  41. },
  42. enumerable: true
  43. });
  44. };
  45. abstract class TouchBarItem<ConfigType> extends EventEmitter {
  46. @ImmutableProperty(() => `${nextItemID++}`) id!: string;
  47. abstract type: string;
  48. abstract onInteraction: Function | null;
  49. child?: TouchBar;
  50. private _parents: { id: string; type: string }[] = [];
  51. private _config!: ConfigType;
  52. constructor (config: ConfigType) {
  53. super();
  54. this._config = this._config || config || {} as ConfigType;
  55. (this as any)[hiddenProperties] = {};
  56. const hook = (this as any)._hook;
  57. if (hook) hook.call(this);
  58. delete (this as any)._hook;
  59. }
  60. public _addParent (item: TouchBarItem<any>) {
  61. const existing = this._parents.some(test => test.id === item.id);
  62. if (!existing) {
  63. this._parents.push({
  64. id: item.id,
  65. type: item.type
  66. });
  67. }
  68. }
  69. public _removeParent (item: TouchBarItem<any>) {
  70. this._parents = this._parents.filter(test => test.id !== item.id);
  71. }
  72. }
  73. class TouchBarButton extends TouchBarItem<Electron.TouchBarButtonConstructorOptions> implements Electron.TouchBarButton {
  74. @ImmutableProperty(() => 'button')
  75. type!: string;
  76. @LiveProperty<TouchBarButton>(config => config.label)
  77. label!: string;
  78. @LiveProperty<TouchBarButton>(config => config.accessibilityLabel)
  79. accessibilityLabel!: string;
  80. @LiveProperty<TouchBarButton>(config => config.backgroundColor)
  81. backgroundColor!: string;
  82. @LiveProperty<TouchBarButton>(config => config.icon)
  83. icon!: Electron.NativeImage;
  84. @LiveProperty<TouchBarButton>(config => config.iconPosition)
  85. iconPosition!: Electron.TouchBarButton['iconPosition'];
  86. @LiveProperty<TouchBarButton>(config => typeof config.enabled !== 'boolean' ? true : config.enabled)
  87. enabled!: boolean;
  88. @ImmutableProperty<TouchBarButton>(({ click: onClick }) => typeof onClick === 'function' ? () => onClick() : null)
  89. onInteraction!: Function | null;
  90. }
  91. class TouchBarColorPicker extends TouchBarItem<Electron.TouchBarColorPickerConstructorOptions> implements Electron.TouchBarColorPicker {
  92. @ImmutableProperty(() => 'colorpicker')
  93. type!: string;
  94. @LiveProperty<TouchBarColorPicker>(config => config.availableColors)
  95. availableColors!: string[];
  96. @LiveProperty<TouchBarColorPicker>(config => config.selectedColor)
  97. selectedColor!: string;
  98. @ImmutableProperty<TouchBarColorPicker>(({ change: onChange }, setInternalProp) => typeof onChange === 'function'
  99. ? (details: { color: string }) => {
  100. setInternalProp('selectedColor', details.color);
  101. onChange(details.color);
  102. }
  103. : null)
  104. onInteraction!: Function | null;
  105. }
  106. class TouchBarGroup extends TouchBarItem<Electron.TouchBarGroupConstructorOptions> implements Electron.TouchBarGroup {
  107. @ImmutableProperty(() => 'group')
  108. type!: string;
  109. @LiveProperty<TouchBarGroup>(config => config.items instanceof TouchBar ? config.items : new TouchBar(config.items), (self, newChild: TouchBar) => {
  110. if (self.child) {
  111. for (const item of self.child.orderedItems) {
  112. item._removeParent(self);
  113. }
  114. }
  115. for (const item of newChild.orderedItems) {
  116. item._addParent(self);
  117. }
  118. })
  119. child!: TouchBar;
  120. onInteraction = null;
  121. }
  122. class TouchBarLabel extends TouchBarItem<Electron.TouchBarLabelConstructorOptions> implements Electron.TouchBarLabel {
  123. @ImmutableProperty(() => 'label')
  124. type!: string;
  125. @LiveProperty<TouchBarLabel>(config => config.label)
  126. label!: string;
  127. @LiveProperty<TouchBarLabel>(config => config.accessibilityLabel)
  128. accessibilityLabel!: string;
  129. @LiveProperty<TouchBarLabel>(config => config.textColor)
  130. textColor!: string;
  131. onInteraction = null;
  132. }
  133. class TouchBarPopover extends TouchBarItem<Electron.TouchBarPopoverConstructorOptions> implements Electron.TouchBarPopover {
  134. @ImmutableProperty(() => 'popover')
  135. type!: string;
  136. @LiveProperty<TouchBarPopover>(config => config.label)
  137. label!: string;
  138. @LiveProperty<TouchBarPopover>(config => config.icon)
  139. icon!: Electron.NativeImage;
  140. @LiveProperty<TouchBarPopover>(config => config.showCloseButton)
  141. showCloseButton!: boolean;
  142. @LiveProperty<TouchBarPopover>(config => config.items instanceof TouchBar ? config.items : new TouchBar(config.items), (self, newChild: TouchBar) => {
  143. if (self.child) {
  144. for (const item of self.child.orderedItems) {
  145. item._removeParent(self);
  146. }
  147. }
  148. for (const item of newChild.orderedItems) {
  149. item._addParent(self);
  150. }
  151. })
  152. child!: TouchBar;
  153. onInteraction = null;
  154. }
  155. class TouchBarSlider extends TouchBarItem<Electron.TouchBarSliderConstructorOptions> implements Electron.TouchBarSlider {
  156. @ImmutableProperty(() => 'slider')
  157. type!: string;
  158. @LiveProperty<TouchBarSlider>(config => config.label)
  159. label!: string;
  160. @LiveProperty<TouchBarSlider>(config => config.minValue)
  161. minValue!: number;
  162. @LiveProperty<TouchBarSlider>(config => config.maxValue)
  163. maxValue!: number;
  164. @LiveProperty<TouchBarSlider>(config => config.value)
  165. value!: number;
  166. @ImmutableProperty<TouchBarSlider>(({ change: onChange }, setInternalProp) => typeof onChange === 'function'
  167. ? (details: { value: number }) => {
  168. setInternalProp('value', details.value);
  169. onChange(details.value);
  170. }
  171. : null)
  172. onInteraction!: Function | null;
  173. }
  174. class TouchBarSpacer extends TouchBarItem<Electron.TouchBarSpacerConstructorOptions> implements Electron.TouchBarSpacer {
  175. @ImmutableProperty(() => 'spacer')
  176. type!: string;
  177. @ImmutableProperty<TouchBarSpacer>(config => config.size)
  178. size!: Electron.TouchBarSpacer['size'];
  179. onInteraction = null;
  180. }
  181. class TouchBarSegmentedControl extends TouchBarItem<Electron.TouchBarSegmentedControlConstructorOptions> implements Electron.TouchBarSegmentedControl {
  182. @ImmutableProperty(() => 'segmented_control')
  183. type!: string;
  184. @LiveProperty<TouchBarSegmentedControl>(config => config.segmentStyle)
  185. segmentStyle!: Electron.TouchBarSegmentedControl['segmentStyle'];
  186. @LiveProperty<TouchBarSegmentedControl>(config => config.segments || [])
  187. segments!: Electron.SegmentedControlSegment[];
  188. @LiveProperty<TouchBarSegmentedControl>(config => config.selectedIndex)
  189. selectedIndex!: number;
  190. @LiveProperty<TouchBarSegmentedControl>(config => config.mode)
  191. mode!: Electron.TouchBarSegmentedControl['mode'];
  192. @ImmutableProperty<TouchBarSegmentedControl>(({ change: onChange }, setInternalProp) => typeof onChange === 'function'
  193. ? (details: { selectedIndex: number, isSelected: boolean }) => {
  194. setInternalProp('selectedIndex', details.selectedIndex);
  195. onChange(details.selectedIndex, details.isSelected);
  196. }
  197. : null)
  198. onInteraction!: Function | null;
  199. }
  200. class TouchBarScrubber extends TouchBarItem<Electron.TouchBarScrubberConstructorOptions> implements Electron.TouchBarScrubber {
  201. @ImmutableProperty(() => 'scrubber')
  202. type!: string;
  203. @LiveProperty<TouchBarScrubber>(config => config.items)
  204. items!: Electron.ScrubberItem[];
  205. @LiveProperty<TouchBarScrubber>(config => config.selectedStyle || null)
  206. selectedStyle!: Electron.TouchBarScrubber['selectedStyle'];
  207. @LiveProperty<TouchBarScrubber>(config => config.overlayStyle || null)
  208. overlayStyle!: Electron.TouchBarScrubber['overlayStyle'];
  209. @LiveProperty<TouchBarScrubber>(config => config.showArrowButtons || false)
  210. showArrowButtons!: boolean;
  211. @LiveProperty<TouchBarScrubber>(config => config.mode || 'free')
  212. mode!: Electron.TouchBarScrubber['mode'];
  213. @LiveProperty<TouchBarScrubber>(config => typeof config.continuous === 'undefined' ? true : config.continuous)
  214. continuous!: boolean;
  215. @ImmutableProperty<TouchBarScrubber>(({ select: onSelect, highlight: onHighlight }) => typeof onSelect === 'function' || typeof onHighlight === 'function'
  216. ? (details: { type: 'select'; selectedIndex: number } | { type: 'highlight'; highlightedIndex: number }) => {
  217. if (details.type === 'select') {
  218. if (onSelect) onSelect(details.selectedIndex);
  219. } else {
  220. if (onHighlight) onHighlight(details.highlightedIndex);
  221. }
  222. }
  223. : null)
  224. onInteraction!: Function | null;
  225. }
  226. class TouchBarOtherItemsProxy extends TouchBarItem<null> implements Electron.TouchBarOtherItemsProxy {
  227. @ImmutableProperty(() => 'other_items_proxy') type!: string;
  228. onInteraction = null;
  229. }
  230. const escapeItemSymbol = Symbol('escape item');
  231. class TouchBar extends EventEmitter implements Electron.TouchBar {
  232. // Bind a touch bar to a window
  233. static _setOnWindow (touchBar: TouchBar | Electron.TouchBarConstructorOptions['items'], window: Electron.BaseWindow) {
  234. if (window._touchBar != null) {
  235. window._touchBar._removeFromWindow(window);
  236. }
  237. if (!touchBar) {
  238. window._setTouchBarItems([]);
  239. return;
  240. }
  241. if (Array.isArray(touchBar)) {
  242. touchBar = new TouchBar({ items: touchBar });
  243. }
  244. touchBar._addToWindow(window);
  245. }
  246. private windowListeners = new Map<number, Function>();
  247. private items = new Map<string, TouchBarItem<any>>();
  248. orderedItems: TouchBarItem<any>[] = [];
  249. constructor (options: Electron.TouchBarConstructorOptions) {
  250. super();
  251. if (options == null) {
  252. throw new Error('Must specify options object as first argument');
  253. }
  254. let { items, escapeItem } = options;
  255. if (!Array.isArray(items)) {
  256. items = [];
  257. }
  258. this.escapeItem = (escapeItem as any) || null;
  259. const registerItem = (item: TouchBarItem<any>) => {
  260. this.items.set(item.id, item);
  261. item.on('change', this.changeListener);
  262. if (item.child instanceof TouchBar) {
  263. for (const child of item.child.orderedItems) {
  264. registerItem(child);
  265. }
  266. }
  267. };
  268. let hasOtherItemsProxy = false;
  269. const idSet = new Set();
  270. for (const item of items) {
  271. if (!(item instanceof TouchBarItem)) {
  272. throw new TypeError('Each item must be an instance of TouchBarItem');
  273. }
  274. if (item.type === 'other_items_proxy') {
  275. if (!hasOtherItemsProxy) {
  276. hasOtherItemsProxy = true;
  277. } else {
  278. throw new Error('Must only have one OtherItemsProxy per TouchBar');
  279. }
  280. }
  281. if (!idSet.has(item.id)) {
  282. idSet.add(item.id);
  283. } else {
  284. throw new Error('Cannot add a single instance of TouchBarItem multiple times in a TouchBar');
  285. }
  286. }
  287. // register in separate loop after all items are validated
  288. for (const item of (items as TouchBarItem<any>[])) {
  289. this.orderedItems.push(item);
  290. registerItem(item);
  291. }
  292. }
  293. private changeListener = (item: TouchBarItem<any>) => {
  294. this.emit('change', item.id, item.type);
  295. };
  296. private [escapeItemSymbol]: TouchBarItem<unknown> | null = null;
  297. set escapeItem (item: TouchBarItem<unknown> | null) {
  298. if (item != null && !(item instanceof TouchBarItem)) {
  299. throw new Error('Escape item must be an instance of TouchBarItem');
  300. }
  301. const escapeItem = this.escapeItem;
  302. if (escapeItem) {
  303. escapeItem.removeListener('change', this.changeListener);
  304. }
  305. this[escapeItemSymbol] = item;
  306. if (this.escapeItem != null) {
  307. this.escapeItem.on('change', this.changeListener);
  308. }
  309. this.emit('escape-item-change', item);
  310. }
  311. get escapeItem (): TouchBarItem<unknown> | null {
  312. return this[escapeItemSymbol];
  313. }
  314. _addToWindow (window: Electron.BaseWindow) {
  315. const { id } = window;
  316. // Already added to window
  317. if (this.windowListeners.has(id)) return;
  318. window._touchBar = this;
  319. const changeListener = (itemID: string) => {
  320. window._refreshTouchBarItem(itemID);
  321. };
  322. this.on('change', changeListener);
  323. const escapeItemListener = (item: Electron.TouchBarItemType | null) => {
  324. window._setEscapeTouchBarItem(item ?? {});
  325. };
  326. this.on('escape-item-change', escapeItemListener);
  327. const interactionListener = (_: any, itemID: string, details: any) => {
  328. let item = this.items.get(itemID);
  329. if (item == null && this.escapeItem != null && this.escapeItem.id === itemID) {
  330. item = this.escapeItem;
  331. }
  332. if (item != null && item.onInteraction != null) {
  333. item.onInteraction(details);
  334. }
  335. };
  336. window.on('-touch-bar-interaction', interactionListener);
  337. const removeListeners = () => {
  338. this.removeListener('change', changeListener);
  339. this.removeListener('escape-item-change', escapeItemListener);
  340. window.removeListener('-touch-bar-interaction', interactionListener);
  341. window.removeListener('closed', removeListeners);
  342. window._touchBar = null;
  343. this.windowListeners.delete(id);
  344. const unregisterItems = (items: TouchBarItem<any>[]) => {
  345. for (const item of items) {
  346. item.removeListener('change', this.changeListener);
  347. if (item.child instanceof TouchBar) {
  348. unregisterItems(item.child.orderedItems);
  349. }
  350. }
  351. };
  352. unregisterItems(this.orderedItems);
  353. if (this.escapeItem) {
  354. this.escapeItem.removeListener('change', this.changeListener);
  355. }
  356. };
  357. window.once('closed', removeListeners);
  358. this.windowListeners.set(id, removeListeners);
  359. window._setTouchBarItems(this.orderedItems);
  360. escapeItemListener(this.escapeItem);
  361. }
  362. _removeFromWindow (window: Electron.BaseWindow) {
  363. const removeListeners = this.windowListeners.get(window.id);
  364. if (removeListeners != null) removeListeners();
  365. }
  366. static TouchBarButton = TouchBarButton;
  367. static TouchBarColorPicker = TouchBarColorPicker;
  368. static TouchBarGroup = TouchBarGroup;
  369. static TouchBarLabel = TouchBarLabel;
  370. static TouchBarPopover = TouchBarPopover;
  371. static TouchBarSlider = TouchBarSlider;
  372. static TouchBarSpacer = TouchBarSpacer;
  373. static TouchBarSegmentedControl = TouchBarSegmentedControl;
  374. static TouchBarScrubber = TouchBarScrubber;
  375. static TouchBarOtherItemsProxy = TouchBarOtherItemsProxy;
  376. }
  377. export default TouchBar;