88 CGFloat remainder;
99} TabWidth;
1010
11- const CGFloat OptimumTabWidth = 220 ;
12- const CGFloat MinimumTabWidth = 100 ;
13- const CGFloat TabOverlap = 6 ;
14- const CGFloat ScrollOneTabAllowance = 0.25 ; // If we are showing 75+% of the tab, consider it to be fully shown when deciding whether to scroll to next tab.
11+ static const CGFloat OptimumTabWidth = 200 ;
12+ static const CGFloat MinimumTabWidth = 100 ;
13+ static const CGFloat TabOverlap = 6 ;
14+ static const CGFloat ScrollOneTabAllowance = 0.25 ; // If we are showing 75+% of the tab, consider it to be fully shown when deciding whether to scroll to next tab.
1515
1616static MMHoverButton* MakeHoverButton (MMTabline *tabline, MMHoverButtonImage imageType, NSString *tooltip, SEL action, BOOL continuous) {
1717 MMHoverButton *button = [MMHoverButton new ];
@@ -44,8 +44,8 @@ @implementation MMTabline
4444 CGFloat _xOffsetForDrag;
4545 NSInteger _initialDraggedTabIndex;
4646 NSInteger _finalDraggedTabIndex;
47- MMHoverButton *_leftScrollButton ;
48- MMHoverButton *_rightScrollButton ;
47+ MMHoverButton *_backwardScrollButton ;
48+ MMHoverButton *_forwardScrollButton ;
4949 id _scrollWheelEventMonitor;
5050}
5151
@@ -82,23 +82,40 @@ - (instancetype)initWithFrame:(NSRect)frameRect
8282 _scrollView.documentView = _tabsContainer;
8383 [self addSubview: _scrollView];
8484
85- _addTabButton = MakeHoverButton (self, MMHoverButtonImageAddTab, NSLocalizedString(@" create-new-tab-button" , @" Create a new tab button" ), @selector (addTabAtEnd ), NO );
86- _leftScrollButton = MakeHoverButton (self, MMHoverButtonImageScrollLeft, NSLocalizedString(@" scroll-tabs-backward" , @" Scroll backward button in tabs line" ), @selector (scrollLeftOneTab ), YES );
87- _rightScrollButton = MakeHoverButton (self, MMHoverButtonImageScrollRight, NSLocalizedString(@" scroll-tabs-forward" , @" Scroll forward button in tabs line" ), @selector (scrollRightOneTab ), YES );
88-
89- [self addConstraints: [NSLayoutConstraint constraintsWithVisualFormat: @" H:[_leftScrollButton][_rightScrollButton]-5-[_scrollView]-5-[_addTabButton]" options: NSLayoutFormatAlignAllCenterY metrics: nil views: NSDictionaryOfVariableBindings(_scrollView, _leftScrollButton, _rightScrollButton, _addTabButton)]];
85+ _addTabButton = MakeHoverButton (
86+ self,
87+ MMHoverButtonImageAddTab,
88+ NSLocalizedString (@" create-new-tab-button" , @" Create a new tab button" ),
89+ @selector(addTabAtEnd),
90+ NO);
91+ _backwardScrollButton = MakeHoverButton (
92+ self,
93+ [self useRightToLeft ] ? MMHoverButtonImageScrollRight : MMHoverButtonImageScrollLeft,
94+ NSLocalizedString (@" scroll-tabs-backward" , @" Scroll backward button in tabs line" ),
95+ @selector(scrollBackwardOneTab),
96+ YES);
97+ _forwardScrollButton = MakeHoverButton (
98+ self,
99+ [self useRightToLeft ] ? MMHoverButtonImageScrollLeft : MMHoverButtonImageScrollRight,
100+ NSLocalizedString (@" scroll-tabs-forward" , @" Scroll forward button in tabs line" ),
101+ @selector(scrollForwardOneTab),
102+ YES);
103+
104+ [self addConstraints: [NSLayoutConstraint constraintsWithVisualFormat: @" H:[_backwardScrollButton][_forwardScrollButton]-5-[_scrollView]-5-[_addTabButton]" options: NSLayoutFormatAlignAllCenterY metrics: nil views: NSDictionaryOfVariableBindings(_scrollView, _backwardScrollButton, _forwardScrollButton, _addTabButton)]];
90105 [self addConstraints: [NSLayoutConstraint constraintsWithVisualFormat: @" V:|[_scrollView]|" options: 0 metrics: nil views: @{@" _scrollView" :_scrollView}]];
91106
92- _tabScrollButtonsLeadingConstraint = [NSLayoutConstraint constraintWithItem: _leftScrollButton attribute: NSLayoutAttributeLeading relatedBy: NSLayoutRelationEqual toItem: self attribute: NSLayoutAttributeLeading multiplier: 1 constant: 5 ];
107+ _tabScrollButtonsLeadingConstraint = [NSLayoutConstraint constraintWithItem: _backwardScrollButton attribute: NSLayoutAttributeLeading relatedBy: NSLayoutRelationEqual toItem: self attribute: NSLayoutAttributeLeading multiplier: 1 constant: 5 ];
93108 [self addConstraint: _tabScrollButtonsLeadingConstraint];
94109
95110 _addTabButtonTrailingConstraint = [NSLayoutConstraint constraintWithItem: self attribute: NSLayoutAttributeTrailing relatedBy: NSLayoutRelationEqual toItem: _addTabButton attribute: NSLayoutAttributeTrailing multiplier: 1 constant: 5 ];
96111 [self addConstraint: _addTabButtonTrailingConstraint];
97112
98113 [[NSNotificationCenter defaultCenter ] addObserver: self selector: @selector (didScroll: ) name: NSViewBoundsDidChangeNotification object: _scrollView.contentView];
114+ if ([self useRightToLeft ]) {
115+ [[NSNotificationCenter defaultCenter ] addObserver: self selector: @selector (updateTabsContainerBoundsForRTL: ) name: NSViewFrameDidChangeNotification object: _tabsContainer];
116+ }
99117
100118 [self addScrollWheelMonitor ];
101-
102119 }
103120 return self;
104121}
@@ -194,7 +211,7 @@ - (void)setShowsTabScrollButtons:(BOOL)showsTabScrollButtons
194211 // (see -drawRect: in MMTab.m).
195212 if (_showsTabScrollButtons != showsTabScrollButtons) {
196213 _showsTabScrollButtons = showsTabScrollButtons;
197- _tabScrollButtonsLeadingConstraint.constant = showsTabScrollButtons ? 5 : -((NSWidth (_leftScrollButton .frame ) * 2 ) + 5 + MMTabShadowBlurRadius);
214+ _tabScrollButtonsLeadingConstraint.constant = showsTabScrollButtons ? 5 : -((NSWidth (_backwardScrollButton .frame ) * 2 ) + 5 + MMTabShadowBlurRadius);
198215 }
199216}
200217
@@ -244,8 +261,8 @@ - (void)setTablineSelFgColor:(NSColor *)color
244261{
245262 _tablineSelFgColor = color;
246263 _addTabButton.fgColor = color;
247- _leftScrollButton .fgColor = color;
248- _rightScrollButton .fgColor = color;
264+ _backwardScrollButton .fgColor = color;
265+ _forwardScrollButton .fgColor = color;
249266 for (MMTab *tab in _tabs) tab.state = tab.state ;
250267}
251268
@@ -280,6 +297,7 @@ - (NSInteger)addTabAtIndex:(NSInteger)index
280297 NSRect frame = _tabsContainer.bounds ;
281298 frame.size .width = index == _tabs.count ? t.width + t.remainder : t.width ;
282299 frame.origin .x = index * (t.width - TabOverlap);
300+ frame = [self flipRectRTL: frame];
283301 MMTab *newTab = [[MMTab alloc ] initWithFrame: frame tabline: self ];
284302
285303 [_tabs insertObject: newTab atIndex: index];
@@ -383,6 +401,7 @@ - (void)updateTabsByTags:(NSInteger *)tags len:(NSUInteger)len delayTabResize:(B
383401 NSRect frame = _tabsContainer.bounds ;
384402 frame.size .width = i == (len - 1 ) ? t.width + t.remainder : t.width ;
385403 frame.origin .x = i * (t.width - TabOverlap);
404+ frame = [self flipRectRTL: frame];
386405 MMTab *newTab = [[MMTab alloc ] initWithFrame: frame tabline: self ];
387406 newTab.tag = tag;
388407 [newTabs addObject: newTab];
@@ -533,19 +552,35 @@ - (void)setTablineSelBackground:(NSColor *)back foreground:(NSColor *)fore
533552
534553#pragma mark - Helpers
535554
536- NSComparisonResult SortTabsForZOrder (MMTab *tab1, MMTab *tab2, void *draggedTab)
555+ NSComparisonResult SortTabsForZOrder (MMTab *tab1, MMTab *tab2, void *draggedTab, BOOL rtl )
537556{ // Z-order, highest to lowest: dragged, selected, hovered, rightmost
538557 if (tab1 == (__bridge MMTab *)draggedTab) return NSOrderedDescending;
539558 if (tab2 == (__bridge MMTab *)draggedTab) return NSOrderedAscending;
540559 if (tab1.state == MMTabStateSelected) return NSOrderedDescending;
541560 if (tab2.state == MMTabStateSelected) return NSOrderedAscending;
542561 if (tab1.state == MMTabStateUnselectedHover) return NSOrderedDescending;
543562 if (tab2.state == MMTabStateUnselectedHover) return NSOrderedAscending;
544- if (NSMinX (tab1.frame ) < NSMinX (tab2.frame )) return NSOrderedAscending;
545- if (NSMinX (tab1.frame ) > NSMinX (tab2.frame )) return NSOrderedDescending;
563+ if (rtl) {
564+ if (NSMinX (tab1.frame ) > NSMinX (tab2.frame )) return NSOrderedAscending;
565+ if (NSMinX (tab1.frame ) < NSMinX (tab2.frame )) return NSOrderedDescending;
566+ } else {
567+ if (NSMinX (tab1.frame ) < NSMinX (tab2.frame )) return NSOrderedAscending;
568+ if (NSMinX (tab1.frame ) > NSMinX (tab2.frame )) return NSOrderedDescending;
569+ }
546570 return NSOrderedSame;
547571}
548572
573+ NSComparisonResult SortTabsForZOrderLTR (MMTab *tab1, MMTab *tab2, void *draggedTab)
574+ {
575+ return SortTabsForZOrder (tab1, tab2, draggedTab, NO );
576+ }
577+
578+
579+ NSComparisonResult SortTabsForZOrderRTL (MMTab *tab1, MMTab *tab2, void *draggedTab)
580+ {
581+ return SortTabsForZOrder (tab1, tab2, draggedTab, YES );
582+ }
583+
549584- (TabWidth)tabWidthForTabs : (NSInteger )numTabs
550585{
551586 // Each tab (except the first) overlaps the previous tab by TabOverlap
@@ -620,9 +655,17 @@ - (void)fixupCloseButtons
620655
621656- (void )fixupTabZOrder
622657{
623- [_tabsContainer sortSubviewsUsingFunction: SortTabsForZOrder context: (__bridge void *)(_draggedTab)];
658+ if ([self useRightToLeft ]) {
659+ [_tabsContainer sortSubviewsUsingFunction: SortTabsForZOrderRTL
660+ context: (__bridge void *)(_draggedTab)];
661+ } else {
662+ [_tabsContainer sortSubviewsUsingFunction: SortTabsForZOrderLTR
663+ context: (__bridge void *)(_draggedTab)];
664+ }
624665}
625666
667+ // / The main layout function that calculates the tab positions and animate them
668+ // / accordingly. Call this every time tabs have been added/removed/moved.
626669- (void )fixupLayoutWithAnimation : (BOOL )shouldAnimate delayResize : (BOOL )delayResize
627670{
628671 if (!self.useAnimation )
@@ -656,6 +699,7 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
656699 frame.size .width = i == _tabs.count - 1 ? t.width + t.remainder : t.width ;
657700 frame.origin .x = i != 0 ? i * (t.width - TabOverlap) : 0 ;
658701 }
702+ frame = [self flipRectRTL: frame];
659703 if (shouldAnimate) {
660704 [NSAnimationContext runAnimationGroup: ^(NSAnimationContext * _Nonnull context) {
661705 context.allowsImplicitAnimation = YES ;
@@ -673,8 +717,20 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
673717 NSRect frame = _tabsContainer.frame ;
674718 frame.size .width = t.width * _tabs.count - TabOverlap * (_tabs.count - 1 );
675719 frame.size .width = NSWidth (frame) < NSWidth (_scrollView.frame ) ? NSWidth (_scrollView.frame ) : NSWidth (frame);
676- if (shouldAnimate) _tabsContainer.animator .frame = frame;
677- else _tabsContainer.frame = frame;
720+ const BOOL sizeDecreasing = NSWidth (frame) < NSWidth (_tabsContainer.frame );
721+ if ([self useRightToLeft ]) {
722+ // In RTL mode we flip the X coords and grow from 0 to negative.
723+ // See updateTabsContainerBoundsForRTL which auto-updates the
724+ // bounds to match the frame.
725+ frame.origin .x = -NSWidth (frame);
726+ }
727+ if (shouldAnimate && sizeDecreasing) {
728+ // Need to animate to make sure we don't immediately get clamped by
729+ // the new size if we are already scrolled all the way to the back.
730+ _tabsContainer.animator .frame = frame;
731+ } else {
732+ _tabsContainer.frame = frame;
733+ }
678734 [self updateTabScrollButtonsEnabledState ];
679735 }
680736}
@@ -684,6 +740,41 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate
684740 [self fixupLayoutWithAnimation: shouldAnimate delayResize: NO ];
685741}
686742
743+ #pragma mark - Right-to-left (RTL) support
744+
745+ - (BOOL )useRightToLeft
746+ {
747+ // MMTabs support RTL locales. In such locales user interface items are
748+ // laid out from right to left. The layout of hover buttons and views are
749+ // automatically flipped by AppKit, but we need to handle this manually in
750+ // the tab placement logic since that is custom logic.
751+ return self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft;
752+ }
753+
754+ - (void )updateTabsContainerBoundsForRTL : (NSNotification *)notification
755+ {
756+ // In RTL mode, we grow the tabs container to the left. We want to preserve
757+ // stability of the scroll view's bounds, and also have the tabs animate
758+ // correctly. To do this, we have to make sure the container bounds matches
759+ // the frame at all times. This "cancels out" the negative X offsets with
760+ // each other and ease calculations.
761+ // E.g. an MMTab with origin (-100,0) inside the _tabsContainer coordinate
762+ // space will actually be (-100,0) in the scroll view as well.
763+ // In LTR mode we don't need this, since _tabsContainer's origin is always
764+ // at (0,0).
765+ _tabsContainer.bounds = _tabsContainer.frame ;
766+ }
767+
768+ - (NSRect )flipRectRTL : (NSRect )frame
769+ {
770+ if ([self useRightToLeft ]) {
771+ // In right-to-left mode, we flip the X coordinates for all the tabs so
772+ // they start at 0 and grow in the negative direction.
773+ frame.origin .x = -NSMaxX (frame);
774+ }
775+ return frame;
776+ }
777+
687778#pragma mark - Mouse
688779
689780- (void )updateTrackingAreas
@@ -796,9 +887,15 @@ - (void)mouseDragged:(NSEvent *)event
796887 [self fixupTabZOrder ];
797888 [_draggedTab setFrameOrigin: NSMakePoint (mouse.x - _xOffsetForDrag, 0 )];
798889 MMTab *selectedTab = _selectedTabIndex == -1 ? nil : _tabs[_selectedTabIndex];
890+ const BOOL rightToLeft = [self useRightToLeft ];
799891 [_tabs sortWithOptions: NSSortStable usingComparator: ^NSComparisonResult (MMTab *t1, MMTab *t2) {
800- if (NSMinX (t1.frame ) <= NSMinX (t2.frame )) return NSOrderedAscending;
801- if (NSMinX (t1.frame ) > NSMinX (t2.frame )) return NSOrderedDescending;
892+ if (rightToLeft) {
893+ if (NSMaxX (t1.frame ) >= NSMaxX (t2.frame )) return NSOrderedAscending;
894+ if (NSMaxX (t1.frame ) < NSMaxX (t2.frame )) return NSOrderedDescending;
895+ } else {
896+ if (NSMinX (t1.frame ) <= NSMinX (t2.frame )) return NSOrderedAscending;
897+ if (NSMinX (t1.frame ) > NSMinX (t2.frame )) return NSOrderedDescending;
898+ }
802899 return NSOrderedSame;
803900 }];
804901 _selectedTabIndex = _selectedTabIndex == -1 ? -1 : [_tabs indexOfObject: selectedTab];
@@ -820,11 +917,18 @@ - (void)updateTabScrollButtonsEnabledState
820917 // on either side of _scrollView.
821918 NSRect clipBounds = _scrollView.contentView .bounds ;
822919 if (NSWidth (_tabsContainer.frame ) <= NSWidth (clipBounds)) {
823- _leftScrollButton .enabled = NO ;
824- _rightScrollButton .enabled = NO ;
920+ _backwardScrollButton .enabled = NO ;
921+ _forwardScrollButton .enabled = NO ;
825922 } else {
826- _leftScrollButton.enabled = clipBounds.origin .x > 0 ;
827- _rightScrollButton.enabled = clipBounds.origin .x + NSWidth (clipBounds) < NSMaxX (_tabsContainer.frame );
923+ BOOL scrollLeftEnabled = NSMinX (clipBounds) > NSMinX (_tabsContainer.frame );
924+ BOOL scrollRightEnabled = NSMaxX (clipBounds) < NSMaxX (_tabsContainer.frame );
925+ if ([self useRightToLeft ]) {
926+ _backwardScrollButton.enabled = scrollRightEnabled;
927+ _forwardScrollButton.enabled = scrollLeftEnabled;
928+ } else {
929+ _backwardScrollButton.enabled = scrollLeftEnabled;
930+ _forwardScrollButton.enabled = scrollRightEnabled;
931+ }
828932 }
829933}
830934
@@ -874,29 +978,37 @@ - (void)scrollTabToVisibleAtIndex:(NSInteger)index
874978 }
875979}
876980
877- - (void )scrollLeftOneTab
981+ - (void )scrollBackwardOneTab
878982{
879983 NSRect clipBounds = _scrollView.contentView .animator .bounds ;
880984 for (NSInteger i = _tabs.count - 1 ; i >= 0 ; i--) {
881985 NSRect tabFrame = _tabs[i].frame ;
882986 if (!NSContainsRect (clipBounds, tabFrame)) {
883- CGFloat allowance = i == 0 ? 0 : NSWidth (tabFrame) * ScrollOneTabAllowance;
884- if (NSMinX (tabFrame) + allowance < NSMinX (clipBounds)) {
987+ const CGFloat allowance = (i == 0 ) ?
988+ 0 : NSWidth (tabFrame) * ScrollOneTabAllowance;
989+ const BOOL outOfBounds = [self useRightToLeft ] ?
990+ NSMaxX (tabFrame) - allowance > NSMaxX (clipBounds) :
991+ NSMinX (tabFrame) + allowance < NSMinX (clipBounds);
992+ if (outOfBounds) {
885993 [self scrollTabToVisibleAtIndex: i];
886994 break ;
887995 }
888996 }
889997 }
890998}
891999
892- - (void )scrollRightOneTab
1000+ - (void )scrollForwardOneTab
8931001{
8941002 NSRect clipBounds = _scrollView.contentView .animator .bounds ;
8951003 for (NSInteger i = 0 ; i < _tabs.count ; i++) {
8961004 NSRect tabFrame = _tabs[i].frame ;
8971005 if (!NSContainsRect (clipBounds, tabFrame)) {
898- CGFloat allowance = i == _tabs.count - 1 ? 0 : NSWidth (tabFrame) * ScrollOneTabAllowance;
899- if (NSMaxX (tabFrame) - allowance > NSMaxX (clipBounds)) {
1006+ const CGFloat allowance = (i == _tabs.count - 1 ) ?
1007+ 0 : NSWidth (tabFrame) * ScrollOneTabAllowance;
1008+ const BOOL outOfBounds = [self useRightToLeft ] ?
1009+ NSMinX (tabFrame) + allowance < NSMinX (clipBounds) :
1010+ NSMaxX (tabFrame) - allowance > NSMaxX (clipBounds);
1011+ if (outOfBounds) {
9001012 [self scrollTabToVisibleAtIndex: i];
9011013 break ;
9021014 }
0 commit comments