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,7 +655,13 @@ - (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
626667- (void )fixupLayoutWithAnimation : (BOOL )shouldAnimate delayResize : (BOOL )delayResize
@@ -656,6 +697,7 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
656697 frame.size .width = i == _tabs.count - 1 ? t.width + t.remainder : t.width ;
657698 frame.origin .x = i != 0 ? i * (t.width - TabOverlap) : 0 ;
658699 }
700+ frame = [self flipRectRTL: frame];
659701 if (shouldAnimate) {
660702 [NSAnimationContext runAnimationGroup: ^(NSAnimationContext * _Nonnull context) {
661703 context.allowsImplicitAnimation = YES ;
@@ -673,8 +715,20 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
673715 NSRect frame = _tabsContainer.frame ;
674716 frame.size .width = t.width * _tabs.count - TabOverlap * (_tabs.count - 1 );
675717 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;
718+ const BOOL sizeDecreasing = NSWidth (frame) < NSWidth (_tabsContainer.frame );
719+ if ([self useRightToLeft ]) {
720+ // In RTL mode we flip the X coords and grow from 0 to negative.
721+ // See updateTabsContainerBoundsForRTL which auto-updates the
722+ // bounds to match the frame.
723+ frame.origin .x = -NSWidth (frame);
724+ }
725+ if (shouldAnimate && sizeDecreasing) {
726+ // Need to animate to make sure we don't immediately get clamped by
727+ // the new size if we are already scrolled all the way to the back.
728+ _tabsContainer.animator .frame = frame;
729+ } else {
730+ _tabsContainer.frame = frame;
731+ }
678732 [self updateTabScrollButtonsEnabledState ];
679733 }
680734}
@@ -684,6 +738,41 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate
684738 [self fixupLayoutWithAnimation: shouldAnimate delayResize: NO ];
685739}
686740
741+ #pragma mark - Right-to-left (RTL) support
742+
743+ - (BOOL )useRightToLeft
744+ {
745+ // MMTabs support RTL locales. In such locales user interface items are
746+ // laid out from right to left. The layout of hover buttons and views are
747+ // automatically flipped by AppKit, but we need to handle this manually in
748+ // the tab placement logic since that is custom logic.
749+ return self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft;
750+ }
751+
752+ - (void )updateTabsContainerBoundsForRTL : (NSNotification *)notification
753+ {
754+ // In RTL mode, we grow the tabs container to the left. We want to preserve
755+ // stability of the scroll view's bounds, and also have the tabs animate
756+ // correctly. To do this, we have to make sure the container bounds matches
757+ // the frame at all times. This "cancels out" the negative X offsets with
758+ // each other and ease calculations.
759+ // E.g. an MMTab with origin (-100,0) inside the _tabsContainer coordinate
760+ // space will actually be (-100,0) in the scroll view as well.
761+ // In LTR mode we don't need this, since _tabsContainer's origin is always
762+ // at (0,0).
763+ _tabsContainer.bounds = _tabsContainer.frame ;
764+ }
765+
766+ - (NSRect )flipRectRTL : (NSRect )frame
767+ {
768+ if (![self useRightToLeft ])
769+ return frame;
770+ // In right-to-left mode, we flip the X coordinates for all the tabs so
771+ // they start at 0 and grow in the negative direction.
772+ frame.origin .x = -NSMaxX (frame);
773+ return frame;
774+ }
775+
687776#pragma mark - Mouse
688777
689778- (void )updateTrackingAreas
@@ -796,9 +885,15 @@ - (void)mouseDragged:(NSEvent *)event
796885 [self fixupTabZOrder ];
797886 [_draggedTab setFrameOrigin: NSMakePoint (mouse.x - _xOffsetForDrag, 0 )];
798887 MMTab *selectedTab = _selectedTabIndex == -1 ? nil : _tabs[_selectedTabIndex];
888+ const BOOL rightToLeft = [self useRightToLeft ];
799889 [_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;
890+ if (rightToLeft) {
891+ if (NSMaxX (t1.frame ) >= NSMaxX (t2.frame )) return NSOrderedAscending;
892+ if (NSMaxX (t1.frame ) < NSMaxX (t2.frame )) return NSOrderedDescending;
893+ } else {
894+ if (NSMinX (t1.frame ) <= NSMinX (t2.frame )) return NSOrderedAscending;
895+ if (NSMinX (t1.frame ) > NSMinX (t2.frame )) return NSOrderedDescending;
896+ }
802897 return NSOrderedSame;
803898 }];
804899 _selectedTabIndex = _selectedTabIndex == -1 ? -1 : [_tabs indexOfObject: selectedTab];
@@ -820,11 +915,18 @@ - (void)updateTabScrollButtonsEnabledState
820915 // on either side of _scrollView.
821916 NSRect clipBounds = _scrollView.contentView .bounds ;
822917 if (NSWidth (_tabsContainer.frame ) <= NSWidth (clipBounds)) {
823- _leftScrollButton .enabled = NO ;
824- _rightScrollButton .enabled = NO ;
918+ _backwardScrollButton .enabled = NO ;
919+ _forwardScrollButton .enabled = NO ;
825920 } else {
826- _leftScrollButton.enabled = clipBounds.origin .x > 0 ;
827- _rightScrollButton.enabled = clipBounds.origin .x + NSWidth (clipBounds) < NSMaxX (_tabsContainer.frame );
921+ BOOL scrollLeftEnabled = NSMinX (clipBounds) > NSMinX (_tabsContainer.frame );
922+ BOOL scrollRightEnabled = NSMaxX (clipBounds) < NSMaxX (_tabsContainer.frame );
923+ if ([self useRightToLeft ]) {
924+ _backwardScrollButton.enabled = scrollRightEnabled;
925+ _forwardScrollButton.enabled = scrollLeftEnabled;
926+ } else {
927+ _backwardScrollButton.enabled = scrollLeftEnabled;
928+ _forwardScrollButton.enabled = scrollRightEnabled;
929+ }
828930 }
829931}
830932
@@ -874,29 +976,37 @@ - (void)scrollTabToVisibleAtIndex:(NSInteger)index
874976 }
875977}
876978
877- - (void )scrollLeftOneTab
979+ - (void )scrollBackwardOneTab
878980{
879981 NSRect clipBounds = _scrollView.contentView .animator .bounds ;
880982 for (NSInteger i = _tabs.count - 1 ; i >= 0 ; i--) {
881983 NSRect tabFrame = _tabs[i].frame ;
882984 if (!NSContainsRect (clipBounds, tabFrame)) {
883- CGFloat allowance = i == 0 ? 0 : NSWidth (tabFrame) * ScrollOneTabAllowance;
884- if (NSMinX (tabFrame) + allowance < NSMinX (clipBounds)) {
985+ const CGFloat allowance = (i == 0 ) ?
986+ 0 : NSWidth (tabFrame) * ScrollOneTabAllowance;
987+ const BOOL outOfBounds = [self useRightToLeft ] ?
988+ NSMaxX (tabFrame) - allowance > NSMaxX (clipBounds) :
989+ NSMinX (tabFrame) + allowance < NSMinX (clipBounds);
990+ if (outOfBounds) {
885991 [self scrollTabToVisibleAtIndex: i];
886992 break ;
887993 }
888994 }
889995 }
890996}
891997
892- - (void )scrollRightOneTab
998+ - (void )scrollForwardOneTab
893999{
8941000 NSRect clipBounds = _scrollView.contentView .animator .bounds ;
8951001 for (NSInteger i = 0 ; i < _tabs.count ; i++) {
8961002 NSRect tabFrame = _tabs[i].frame ;
8971003 if (!NSContainsRect (clipBounds, tabFrame)) {
898- CGFloat allowance = i == _tabs.count - 1 ? 0 : NSWidth (tabFrame) * ScrollOneTabAllowance;
899- if (NSMaxX (tabFrame) - allowance > NSMaxX (clipBounds)) {
1004+ const CGFloat allowance = (i == _tabs.count - 1 ) ?
1005+ 0 : NSWidth (tabFrame) * ScrollOneTabAllowance;
1006+ const BOOL outOfBounds = [self useRightToLeft ] ?
1007+ NSMinX (tabFrame) + allowance < NSMinX (clipBounds) :
1008+ NSMaxX (tabFrame) - allowance > NSMaxX (clipBounds);
1009+ if (outOfBounds) {
9001010 [self scrollTabToVisibleAtIndex: i];
9011011 break ;
9021012 }
0 commit comments