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/. */
5 #include "nsTouchBar.h"
7 #include <objc/runtime.h>
9 #include "mozilla/MacStringHelpers.h"
10 #include "mozilla/dom/Document.h"
11 #include "nsArrayUtils.h"
12 #include "nsCocoaUtils.h"
13 #include "nsDirectoryServiceDefs.h"
15 #include "nsTouchBarInputIcon.h"
16 #include "nsWidgetsCID.h"
18 @implementation nsTouchBar
20 // Used to tie action strings to buttons.
21 static char sIdentifierAssociationKey;
23 // The default space between inputs, used where layout is not automatic.
24 static const uint32_t kInputSpacing = 8;
25 // The width of buttons in Apple's Share ScrollView. We use this in our
26 // ScrollViews to give them a native appearance.
27 static const uint32_t kScrollViewButtonWidth = 144;
28 static const uint32_t kInputIconSize = 16;
30 // The system default width for Touch Bar inputs is 128px. This is double.
31 #define MAIN_BUTTON_WIDTH 256
33 #pragma mark - NSTouchBarDelegate
35 - (instancetype)init {
36 return [self initWithInputs:nil];
39 - (instancetype)initWithInputs:(NSMutableArray<TouchBarInput*>*)aInputs {
40 if ((self = [super init])) {
41 mTouchBarHelper = do_GetService(NS_TOUCHBARHELPER_CID);
42 if (!mTouchBarHelper) {
43 NS_ERROR("Unable to create Touch Bar Helper.");
48 self.mappedLayoutItems = [NSMutableDictionary dictionary];
49 self.customizationAllowedItemIdentifiers = @[];
52 // This customization identifier is how users' custom layouts are saved by
53 // macOS. If this changes, all users' layouts would be reset to the
55 self.customizationIdentifier = [kTouchBarBaseIdentifier
56 stringByAppendingPathExtension:@"defaultbar"];
57 nsCOMPtr<nsIArray> allItems;
59 nsresult rv = mTouchBarHelper->GetAllItems(getter_AddRefs(allItems));
60 if (NS_FAILED(rv) || !allItems) {
64 uint32_t itemCount = 0;
65 allItems->GetLength(&itemCount);
66 // This is copied to self.customizationAllowedItemIdentifiers.
67 // Required since [self.mappedItems allKeys] does not preserve order.
68 // One slot is added for the spacer item.
69 NSMutableArray* orderedIdentifiers =
70 [NSMutableArray arrayWithCapacity:itemCount + 1];
71 for (uint32_t i = 0; i < itemCount; ++i) {
72 nsCOMPtr<nsITouchBarInput> input = do_QueryElementAt(allItems, i);
77 TouchBarInput* convertedInput;
78 NSTouchBarItemIdentifier newInputIdentifier =
79 [TouchBarInput nativeIdentifierWithXPCOM:input];
80 if (!newInputIdentifier) {
84 // If there is already an input in mappedLayoutItems with this
85 // identifier, that means updateItem fired before this initialization.
86 // The input cached by updateItem is more current, so we should use that
88 if (self.mappedLayoutItems[newInputIdentifier]) {
89 convertedInput = self.mappedLayoutItems[newInputIdentifier];
91 convertedInput = [[TouchBarInput alloc] initWithXPCOM:input];
92 // Add new input to dictionary for lookup of properties in delegate.
93 self.mappedLayoutItems[[convertedInput nativeIdentifier]] =
97 orderedIdentifiers[i] = [convertedInput nativeIdentifier];
99 [orderedIdentifiers addObject:@"NSTouchBarItemIdentifierFlexibleSpace"];
100 self.customizationAllowedItemIdentifiers = [orderedIdentifiers copy];
102 NSArray* defaultItemIdentifiers = @[
103 [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"back"],
104 [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"forward"],
105 [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"reload"],
106 [TouchBarInput nativeIdentifierWithType:@"mainButton"
107 withKey:@"open-location"],
108 [TouchBarInput nativeIdentifierWithType:@"button" withKey:@"new-tab"],
109 [TouchBarInput shareScrubberIdentifier],
110 [TouchBarInput searchPopoverIdentifier]
112 self.defaultItemIdentifiers = [defaultItemIdentifiers copy];
114 NSMutableArray* defaultItemIdentifiers =
115 [NSMutableArray arrayWithCapacity:[aInputs count]];
116 for (TouchBarInput* input in aInputs) {
117 self.mappedLayoutItems[[input nativeIdentifier]] = input;
118 [defaultItemIdentifiers addObject:[input nativeIdentifier]];
120 self.defaultItemIdentifiers = [defaultItemIdentifiers copy];
128 for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
129 NSTouchBarItem* item = [self itemForIdentifier:identifier];
133 if ([item isKindOfClass:[NSPopoverTouchBarItem class]]) {
134 [(NSPopoverTouchBarItem*)item setCollapsedRepresentationImage:nil];
135 [(nsTouchBar*)[(NSPopoverTouchBarItem*)item popoverTouchBar] release];
136 } else if ([[item view] isKindOfClass:[NSScrollView class]]) {
137 [[(NSScrollView*)[item view] documentView] release];
138 [(NSScrollView*)[item view] release];
144 [self.defaultItemIdentifiers release];
145 [self.customizationAllowedItemIdentifiers release];
146 [self.scrollViewButtons removeAllObjects];
147 [self.scrollViewButtons release];
148 [self.mappedLayoutItems removeAllObjects];
149 [self.mappedLayoutItems release];
153 - (NSTouchBarItem*)touchBar:(NSTouchBar*)aTouchBar
154 makeItemForIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
155 if (!mTouchBarHelper) {
159 TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
164 if ([input baseType] == TouchBarInputBaseType::kScrubber) {
165 // We check the identifier rather than the baseType here as a special case.
167 isEqualToString:[TouchBarInput shareScrubberIdentifier]]) {
168 // We're only supporting the Share scrubber for now.
171 return [self makeShareScrubberForIdentifier:aIdentifier];
174 if ([input baseType] == TouchBarInputBaseType::kPopover) {
175 NSPopoverTouchBarItem* newPopoverItem =
176 [[NSPopoverTouchBarItem alloc] initWithIdentifier:aIdentifier];
177 [newPopoverItem setCustomizationLabel:[input title]];
178 // We initialize popoverTouchBar here because we only allow setting this
179 // property on popover creation. Updating popoverTouchBar for every update
180 // of the popover item would be very expensive.
181 newPopoverItem.popoverTouchBar =
182 [[nsTouchBar alloc] initWithInputs:[input children]];
183 [self updatePopover:newPopoverItem withIdentifier:[input nativeIdentifier]];
184 return newPopoverItem;
187 // Our new item, which will be initialized depending on aIdentifier.
188 NSCustomTouchBarItem* newItem =
189 [[NSCustomTouchBarItem alloc] initWithIdentifier:aIdentifier];
190 [newItem setCustomizationLabel:[input title]];
192 if ([input baseType] == TouchBarInputBaseType::kScrollView) {
193 [self updateScrollView:newItem withIdentifier:[input nativeIdentifier]];
195 } else if ([input baseType] == TouchBarInputBaseType::kLabel) {
196 NSTextField* label = [NSTextField labelWithString:@""];
197 [self updateLabel:label withIdentifier:[input nativeIdentifier]];
198 newItem.view = label;
202 // The cases of a button or main button require the same setup.
203 NSButton* button = [NSButton buttonWithTitle:@""
205 action:@selector(touchBarAction:)];
206 newItem.view = button;
208 if ([input baseType] == TouchBarInputBaseType::kButton &&
209 ![[input type] hasPrefix:@"scrollView"]) {
210 [self updateButton:newItem withIdentifier:[input nativeIdentifier]];
211 } else if ([input baseType] == TouchBarInputBaseType::kMainButton) {
212 [self updateMainButton:newItem withIdentifier:[input nativeIdentifier]];
217 - (bool)updateItem:(TouchBarInput*)aInput {
218 if (!mTouchBarHelper) {
222 NSTouchBarItem* item = [self itemForIdentifier:[aInput nativeIdentifier]];
224 // If we can't immediately find item, there are three possibilities:
225 // * It is a button in a ScrollView, or
226 // * It is contained within a popover, or
227 // * It simply does not exist.
228 // We check for each possibility here.
229 if (!self.mappedLayoutItems[[aInput nativeIdentifier]]) {
230 if ([self maybeUpdateScrollViewChild:aInput]) {
233 if ([self maybeUpdatePopoverChild:aInput]) {
239 // Update our canonical copy of the input.
240 [self replaceMappedLayoutItem:aInput];
242 if ([aInput baseType] == TouchBarInputBaseType::kButton) {
243 [(NSCustomTouchBarItem*)item setCustomizationLabel:[aInput title]];
244 [self updateButton:(NSCustomTouchBarItem*)item
245 withIdentifier:[aInput nativeIdentifier]];
246 } else if ([aInput baseType] == TouchBarInputBaseType::kMainButton) {
247 [(NSCustomTouchBarItem*)item setCustomizationLabel:[aInput title]];
248 [self updateMainButton:(NSCustomTouchBarItem*)item
249 withIdentifier:[aInput nativeIdentifier]];
250 } else if ([aInput baseType] == TouchBarInputBaseType::kScrollView) {
251 [(NSCustomTouchBarItem*)item setCustomizationLabel:[aInput title]];
252 [self updateScrollView:(NSCustomTouchBarItem*)item
253 withIdentifier:[aInput nativeIdentifier]];
254 } else if ([aInput baseType] == TouchBarInputBaseType::kPopover) {
255 [(NSPopoverTouchBarItem*)item setCustomizationLabel:[aInput title]];
256 [self updatePopover:(NSPopoverTouchBarItem*)item
257 withIdentifier:[aInput nativeIdentifier]];
258 for (TouchBarInput* child in [aInput children]) {
259 [(nsTouchBar*)[(NSPopoverTouchBarItem*)item popoverTouchBar]
262 } else if ([aInput baseType] == TouchBarInputBaseType::kLabel) {
263 [self updateLabel:(NSTextField*)item.view
264 withIdentifier:[aInput nativeIdentifier]];
270 - (bool)maybeUpdatePopoverChild:(TouchBarInput*)aInput {
271 for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
272 TouchBarInput* potentialPopover = self.mappedLayoutItems[identifier];
273 if ([potentialPopover baseType] != TouchBarInputBaseType::kPopover) {
276 NSTouchBarItem* popover =
277 [self itemForIdentifier:[potentialPopover nativeIdentifier]];
279 if ([(nsTouchBar*)[(NSPopoverTouchBarItem*)popover popoverTouchBar]
280 updateItem:aInput]) {
288 - (bool)maybeUpdateScrollViewChild:(TouchBarInput*)aInput {
289 NSCustomTouchBarItem* scrollViewButton =
290 self.scrollViewButtons[[aInput nativeIdentifier]];
291 if (scrollViewButton) {
292 // ScrollView buttons are similar to mainButtons except for their width.
293 [self updateMainButton:scrollViewButton
294 withIdentifier:[aInput nativeIdentifier]];
295 NSButton* button = (NSButton*)scrollViewButton.view;
296 uint32_t buttonSize =
297 MAX(button.attributedTitle.size.width + kInputIconSize + kInputSpacing,
298 kScrollViewButtonWidth);
299 [[button widthAnchor] constraintGreaterThanOrEqualToConstant:buttonSize]
302 // Updating the TouchBarInput* in the ScrollView's mChildren array.
303 for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
304 TouchBarInput* potentialScrollView = self.mappedLayoutItems[identifier];
305 if ([potentialScrollView baseType] != TouchBarInputBaseType::kScrollView) {
308 for (uint32_t i = 0; i < [[potentialScrollView children] count]; ++i) {
309 TouchBarInput* child = [potentialScrollView children][i];
310 if (![[child nativeIdentifier]
311 isEqualToString:[aInput nativeIdentifier]]) {
314 [[potentialScrollView children] replaceObjectAtIndex:i withObject:aInput];
322 - (void)replaceMappedLayoutItem:(TouchBarInput*)aItem {
323 [self.mappedLayoutItems[[aItem nativeIdentifier]] release];
324 self.mappedLayoutItems[[aItem nativeIdentifier]] = aItem;
327 - (void)updateButton:(NSCustomTouchBarItem*)aButton
328 withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
329 if (!aButton || !aIdentifier) {
333 TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
338 NSButton* button = (NSButton*)[aButton view];
339 button.title = [input title];
340 if ([input imageURI]) {
341 [button setImagePosition:NSImageOnly];
342 [self loadIconForInput:input forItem:aButton];
343 // Because we are hiding the title, NSAccessibility also does not get it.
344 // Therefore, set an accessibility label as alternative text for image-only
346 [button setAccessibilityLabel:[input title]];
349 [button setEnabled:![input isDisabled]];
351 button.bezelColor = [input color];
354 objc_setAssociatedObject(button, &sIdentifierAssociationKey, aIdentifier,
355 OBJC_ASSOCIATION_RETAIN);
358 - (void)updateMainButton:(NSCustomTouchBarItem*)aMainButton
359 withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
360 if (!aMainButton || !aIdentifier) {
364 TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
369 [self updateButton:aMainButton withIdentifier:aIdentifier];
370 NSButton* button = (NSButton*)[aMainButton view];
372 // If empty, string is still being localized. Display a blank input instead.
373 if ([[input title] isEqualToString:@""]) {
374 [button setImagePosition:NSNoImage];
376 [button setImagePosition:NSImageLeft];
378 button.imageHugsTitle = YES;
379 [button.widthAnchor constraintGreaterThanOrEqualToConstant:MAIN_BUTTON_WIDTH]
381 [button setContentHuggingPriority:1.0
382 forOrientation:NSLayoutConstraintOrientationHorizontal];
385 - (void)updatePopover:(NSPopoverTouchBarItem*)aPopoverItem
386 withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
387 if (!aPopoverItem || !aIdentifier) {
391 TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
396 aPopoverItem.showsCloseButton = YES;
397 if ([input imageURI]) {
398 [self loadIconForInput:input forItem:aPopoverItem];
399 } else if ([input title]) {
400 aPopoverItem.collapsedRepresentationLabel = [input title];
403 // Special handling to show/hide the search popover if the Urlbar is focused.
404 if ([[input nativeIdentifier]
405 isEqualToString:[TouchBarInput searchPopoverIdentifier]]) {
406 // We can reach this code during window shutdown. We only want to toggle
407 // showPopover if we are in a normal running state.
408 if (!mTouchBarHelper) {
411 bool urlbarIsFocused = false;
412 mTouchBarHelper->GetIsUrlbarFocused(&urlbarIsFocused);
413 if (urlbarIsFocused) {
414 [aPopoverItem showPopover:self];
419 - (void)updateScrollView:(NSCustomTouchBarItem*)aScrollViewItem
420 withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
421 if (!aScrollViewItem || !aIdentifier) {
425 TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
426 if (!input || ![input children]) {
430 NSMutableDictionary* constraintViews = [NSMutableDictionary dictionary];
431 NSView* documentView = [[NSView alloc] initWithFrame:NSZeroRect];
432 NSString* layoutFormat = @"H:|-8-";
433 NSSize size = NSMakeSize(kInputSpacing, 30);
434 // Layout strings allow only alphanumeric characters. We will use this
435 // NSCharacterSet to strip illegal characters.
436 NSCharacterSet* charactersToRemove =
437 [[NSCharacterSet alphanumericCharacterSet] invertedSet];
439 for (TouchBarInput* childInput in [input children]) {
440 if ([childInput baseType] != TouchBarInputBaseType::kButton) {
443 [self replaceMappedLayoutItem:childInput];
444 NSCustomTouchBarItem* newItem = [[NSCustomTouchBarItem alloc]
445 initWithIdentifier:[childInput nativeIdentifier]];
446 NSButton* button = [NSButton buttonWithTitle:[childInput title]
448 action:@selector(touchBarAction:)];
449 newItem.view = button;
450 // ScrollView buttons are similar to mainButtons except for their width.
451 [self updateMainButton:newItem
452 withIdentifier:[childInput nativeIdentifier]];
453 uint32_t buttonSize =
454 MAX(button.attributedTitle.size.width + kInputIconSize + kInputSpacing,
455 kScrollViewButtonWidth);
456 [[button widthAnchor] constraintGreaterThanOrEqualToConstant:buttonSize]
459 NSCustomTouchBarItem* tempItem =
460 self.scrollViewButtons[[childInput nativeIdentifier]];
461 self.scrollViewButtons[[childInput nativeIdentifier]] = newItem;
464 button.translatesAutoresizingMaskIntoConstraints = NO;
465 [documentView addSubview:button];
466 NSString* layoutKey = [[[childInput nativeIdentifier]
467 componentsSeparatedByCharactersInSet:charactersToRemove]
468 componentsJoinedByString:@""];
470 // Iteratively create our layout string.
471 layoutFormat = [layoutFormat
472 stringByAppendingString:[NSString
473 stringWithFormat:@"[%@]-8-", layoutKey]];
474 [constraintViews setObject:button forKey:layoutKey];
475 size.width += kInputSpacing + buttonSize;
478 [layoutFormat stringByAppendingString:[NSString stringWithFormat:@"|"]];
479 NSArray* hConstraints = [NSLayoutConstraint
480 constraintsWithVisualFormat:layoutFormat
481 options:NSLayoutFormatAlignAllCenterY
483 views:constraintViews];
484 NSScrollView* scrollView = [[NSScrollView alloc]
485 initWithFrame:CGRectMake(0, 0, size.width, size.height)];
486 [documentView setFrame:NSMakeRect(0, 0, size.width, size.height)];
487 [NSLayoutConstraint activateConstraints:hConstraints];
488 scrollView.documentView = documentView;
490 aScrollViewItem.view = scrollView;
493 - (void)updateLabel:(NSTextField*)aLabel
494 withIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
495 if (!aLabel || !aIdentifier) {
499 TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
500 if (!input || ![input title]) {
503 [aLabel setStringValue:[input title]];
506 - (NSTouchBarItem*)makeShareScrubberForIdentifier:
507 (NSTouchBarItemIdentifier)aIdentifier {
508 TouchBarInput* input = self.mappedLayoutItems[aIdentifier];
509 // System-default share menu
510 NSSharingServicePickerTouchBarItem* servicesItem =
511 [[NSSharingServicePickerTouchBarItem alloc]
512 initWithIdentifier:aIdentifier];
514 // buttonImage needs to be set to nil while we wait for our icon to load.
515 // Otherwise, the default Apple share icon is automatically loaded.
516 servicesItem.buttonImage = nil;
518 [self loadIconForInput:input forItem:servicesItem];
520 servicesItem.delegate = self;
524 - (void)showPopover:(TouchBarInput*)aPopover showing:(bool)aShowing {
528 NSPopoverTouchBarItem* popoverItem = (NSPopoverTouchBarItem*)[self
529 itemForIdentifier:[aPopover nativeIdentifier]];
534 [popoverItem showPopover:self];
536 [popoverItem dismissPopover:self];
540 - (void)touchBarAction:(id)aSender {
541 NSTouchBarItemIdentifier identifier =
542 objc_getAssociatedObject(aSender, &sIdentifierAssociationKey);
543 if (!identifier || [identifier isEqualToString:@""]) {
547 TouchBarInput* input = self.mappedLayoutItems[identifier];
552 nsCOMPtr<nsITouchBarInputCallback> callback = [input callback];
554 NSLog(@"Touch Bar action attempted with no valid callback! Identifier: %@",
555 [input nativeIdentifier]);
558 callback->OnCommand();
561 - (void)loadIconForInput:(TouchBarInput*)aInput forItem:(NSTouchBarItem*)aItem {
562 if (!aInput || ![aInput imageURI] || !aItem || !mTouchBarHelper) {
566 RefPtr<nsTouchBarInputIcon> icon = [aInput icon];
569 RefPtr<Document> document;
570 nsresult rv = mTouchBarHelper->GetDocument(getter_AddRefs(document));
571 if (NS_FAILED(rv) || !document) {
574 icon = new nsTouchBarInputIcon(document, aInput, aItem);
575 [aInput setIcon:icon];
577 icon->SetupIcon([aInput imageURI]);
580 - (void)releaseJSObjects {
581 mTouchBarHelper = nil;
583 for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
584 TouchBarInput* input = self.mappedLayoutItems[identifier];
589 // Childless popovers contain the default Touch Bar as its popoverTouchBar.
590 // We check for [input children] since the default Touch Bar contains a
591 // popover (search-popover), so this would infinitely loop if there was no
593 if ([input baseType] == TouchBarInputBaseType::kPopover &&
595 NSTouchBarItem* item = [self itemForIdentifier:identifier];
596 [(nsTouchBar*)[(NSPopoverTouchBarItem*)item popoverTouchBar]
600 [input releaseJSObjects];
604 #pragma mark - NSSharingServicePickerTouchBarItemDelegate
606 - (NSArray*)itemsForSharingServicePickerTouchBarItem:
607 (NSSharingServicePickerTouchBarItem*)aPickerTouchBarItem {
608 NSURL* urlToShare = nil;
609 NSString* titleToShare = @"";
612 if (mTouchBarHelper) {
613 nsresult rv = mTouchBarHelper->GetActiveUrl(url);
614 if (!NS_FAILED(rv)) {
615 urlToShare = [NSURL URLWithString:nsCocoaUtils::ToNSString(url)];
616 // NSURL URLWithString returns nil if the URL is invalid. At this point,
617 // it is too late to simply shut down the share menu, so we default to
618 // about:blank if the share button is clicked when the URL is invalid.
619 if (urlToShare == nil) {
620 urlToShare = [NSURL URLWithString:@"about:blank"];
624 rv = mTouchBarHelper->GetActiveTitle(title);
625 if (!NS_FAILED(rv)) {
626 titleToShare = nsCocoaUtils::ToNSString(title);
630 return @[ urlToShare, titleToShare ];
633 - (NSArray<NSSharingService*>*)
634 sharingServicePicker:(NSSharingServicePicker*)aSharingServicePicker
635 sharingServicesForItems:(NSArray*)aItems
636 proposedSharingServices:(NSArray<NSSharingService*>*)aProposedServices {
637 // redundant services
638 NSArray* excludedServices = @[
639 @"com.apple.share.System.add-to-safari-reading-list",
642 NSArray* sharingServices = [aProposedServices
643 filteredArrayUsingPredicate:[NSPredicate
644 predicateWithFormat:@"NOT (name IN %@)",
647 return sharingServices;