8
8
CGFloat remainder;
9
9
} TabWidth;
10
10
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.
15
15
16
16
static MMHoverButton* MakeHoverButton (MMTabline *tabline, MMHoverButtonImage imageType, NSString *tooltip, SEL action, BOOL continuous) {
17
17
MMHoverButton *button = [MMHoverButton new ];
@@ -44,8 +44,8 @@ @implementation MMTabline
44
44
CGFloat _xOffsetForDrag;
45
45
NSInteger _initialDraggedTabIndex;
46
46
NSInteger _finalDraggedTabIndex;
47
- MMHoverButton *_leftScrollButton ;
48
- MMHoverButton *_rightScrollButton ;
47
+ MMHoverButton *_backwardScrollButton ;
48
+ MMHoverButton *_forwardScrollButton ;
49
49
id _scrollWheelEventMonitor;
50
50
}
51
51
@@ -82,23 +82,40 @@ - (instancetype)initWithFrame:(NSRect)frameRect
82
82
_scrollView.documentView = _tabsContainer;
83
83
[self addSubview: _scrollView];
84
84
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)]];
90
105
[self addConstraints: [NSLayoutConstraint constraintsWithVisualFormat: @" V:|[_scrollView]|" options: 0 metrics: nil views: @{@" _scrollView" :_scrollView}]];
91
106
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 ];
93
108
[self addConstraint: _tabScrollButtonsLeadingConstraint];
94
109
95
110
_addTabButtonTrailingConstraint = [NSLayoutConstraint constraintWithItem: self attribute: NSLayoutAttributeTrailing relatedBy: NSLayoutRelationEqual toItem: _addTabButton attribute: NSLayoutAttributeTrailing multiplier: 1 constant: 5 ];
96
111
[self addConstraint: _addTabButtonTrailingConstraint];
97
112
98
113
[[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
+ }
99
117
100
118
[self addScrollWheelMonitor ];
101
-
102
119
}
103
120
return self;
104
121
}
@@ -194,7 +211,7 @@ - (void)setShowsTabScrollButtons:(BOOL)showsTabScrollButtons
194
211
// (see -drawRect: in MMTab.m).
195
212
if (_showsTabScrollButtons != showsTabScrollButtons) {
196
213
_showsTabScrollButtons = showsTabScrollButtons;
197
- _tabScrollButtonsLeadingConstraint.constant = showsTabScrollButtons ? 5 : -((NSWidth (_leftScrollButton .frame ) * 2 ) + 5 + MMTabShadowBlurRadius);
214
+ _tabScrollButtonsLeadingConstraint.constant = showsTabScrollButtons ? 5 : -((NSWidth (_backwardScrollButton .frame ) * 2 ) + 5 + MMTabShadowBlurRadius);
198
215
}
199
216
}
200
217
@@ -244,8 +261,8 @@ - (void)setTablineSelFgColor:(NSColor *)color
244
261
{
245
262
_tablineSelFgColor = color;
246
263
_addTabButton.fgColor = color;
247
- _leftScrollButton .fgColor = color;
248
- _rightScrollButton .fgColor = color;
264
+ _backwardScrollButton .fgColor = color;
265
+ _forwardScrollButton .fgColor = color;
249
266
for (MMTab *tab in _tabs) tab.state = tab.state ;
250
267
}
251
268
@@ -280,6 +297,7 @@ - (NSInteger)addTabAtIndex:(NSInteger)index
280
297
NSRect frame = _tabsContainer.bounds ;
281
298
frame.size .width = index == _tabs.count ? t.width + t.remainder : t.width ;
282
299
frame.origin .x = index * (t.width - TabOverlap);
300
+ frame = [self flipRectRTL: frame];
283
301
MMTab *newTab = [[MMTab alloc ] initWithFrame: frame tabline: self ];
284
302
285
303
[_tabs insertObject: newTab atIndex: index];
@@ -383,6 +401,7 @@ - (void)updateTabsByTags:(NSInteger *)tags len:(NSUInteger)len delayTabResize:(B
383
401
NSRect frame = _tabsContainer.bounds ;
384
402
frame.size .width = i == (len - 1 ) ? t.width + t.remainder : t.width ;
385
403
frame.origin .x = i * (t.width - TabOverlap);
404
+ frame = [self flipRectRTL: frame];
386
405
MMTab *newTab = [[MMTab alloc ] initWithFrame: frame tabline: self ];
387
406
newTab.tag = tag;
388
407
[newTabs addObject: newTab];
@@ -533,19 +552,35 @@ - (void)setTablineSelBackground:(NSColor *)back foreground:(NSColor *)fore
533
552
534
553
#pragma mark - Helpers
535
554
536
- NSComparisonResult SortTabsForZOrder (MMTab *tab1, MMTab *tab2, void *draggedTab)
555
+ NSComparisonResult SortTabsForZOrder (MMTab *tab1, MMTab *tab2, void *draggedTab, BOOL rtl )
537
556
{ // Z-order, highest to lowest: dragged, selected, hovered, rightmost
538
557
if (tab1 == (__bridge MMTab *)draggedTab) return NSOrderedDescending;
539
558
if (tab2 == (__bridge MMTab *)draggedTab) return NSOrderedAscending;
540
559
if (tab1.state == MMTabStateSelected) return NSOrderedDescending;
541
560
if (tab2.state == MMTabStateSelected) return NSOrderedAscending;
542
561
if (tab1.state == MMTabStateUnselectedHover) return NSOrderedDescending;
543
562
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
+ }
546
570
return NSOrderedSame;
547
571
}
548
572
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
+
549
584
- (TabWidth)tabWidthForTabs : (NSInteger )numTabs
550
585
{
551
586
// Each tab (except the first) overlaps the previous tab by TabOverlap
@@ -620,7 +655,13 @@ - (void)fixupCloseButtons
620
655
621
656
- (void )fixupTabZOrder
622
657
{
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
+ }
624
665
}
625
666
626
667
- (void )fixupLayoutWithAnimation : (BOOL )shouldAnimate delayResize : (BOOL )delayResize
@@ -656,6 +697,7 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
656
697
frame.size .width = i == _tabs.count - 1 ? t.width + t.remainder : t.width ;
657
698
frame.origin .x = i != 0 ? i * (t.width - TabOverlap) : 0 ;
658
699
}
700
+ frame = [self flipRectRTL: frame];
659
701
if (shouldAnimate) {
660
702
[NSAnimationContext runAnimationGroup: ^(NSAnimationContext * _Nonnull context) {
661
703
context.allowsImplicitAnimation = YES ;
@@ -673,8 +715,20 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi
673
715
NSRect frame = _tabsContainer.frame ;
674
716
frame.size .width = t.width * _tabs.count - TabOverlap * (_tabs.count - 1 );
675
717
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
+ }
678
732
[self updateTabScrollButtonsEnabledState ];
679
733
}
680
734
}
@@ -684,6 +738,41 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate
684
738
[self fixupLayoutWithAnimation: shouldAnimate delayResize: NO ];
685
739
}
686
740
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
+
687
776
#pragma mark - Mouse
688
777
689
778
- (void )updateTrackingAreas
@@ -796,9 +885,15 @@ - (void)mouseDragged:(NSEvent *)event
796
885
[self fixupTabZOrder ];
797
886
[_draggedTab setFrameOrigin: NSMakePoint (mouse.x - _xOffsetForDrag, 0 )];
798
887
MMTab *selectedTab = _selectedTabIndex == -1 ? nil : _tabs[_selectedTabIndex];
888
+ const BOOL rightToLeft = [self useRightToLeft ];
799
889
[_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
+ }
802
897
return NSOrderedSame;
803
898
}];
804
899
_selectedTabIndex = _selectedTabIndex == -1 ? -1 : [_tabs indexOfObject: selectedTab];
@@ -820,11 +915,18 @@ - (void)updateTabScrollButtonsEnabledState
820
915
// on either side of _scrollView.
821
916
NSRect clipBounds = _scrollView.contentView .bounds ;
822
917
if (NSWidth (_tabsContainer.frame ) <= NSWidth (clipBounds)) {
823
- _leftScrollButton .enabled = NO ;
824
- _rightScrollButton .enabled = NO ;
918
+ _backwardScrollButton .enabled = NO ;
919
+ _forwardScrollButton .enabled = NO ;
825
920
} 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
+ }
828
930
}
829
931
}
830
932
@@ -874,29 +976,37 @@ - (void)scrollTabToVisibleAtIndex:(NSInteger)index
874
976
}
875
977
}
876
978
877
- - (void )scrollLeftOneTab
979
+ - (void )scrollBackwardOneTab
878
980
{
879
981
NSRect clipBounds = _scrollView.contentView .animator .bounds ;
880
982
for (NSInteger i = _tabs.count - 1 ; i >= 0 ; i--) {
881
983
NSRect tabFrame = _tabs[i].frame ;
882
984
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) {
885
991
[self scrollTabToVisibleAtIndex: i];
886
992
break ;
887
993
}
888
994
}
889
995
}
890
996
}
891
997
892
- - (void )scrollRightOneTab
998
+ - (void )scrollForwardOneTab
893
999
{
894
1000
NSRect clipBounds = _scrollView.contentView .animator .bounds ;
895
1001
for (NSInteger i = 0 ; i < _tabs.count ; i++) {
896
1002
NSRect tabFrame = _tabs[i].frame ;
897
1003
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) {
900
1010
[self scrollTabToVisibleAtIndex: i];
901
1011
break ;
902
1012
}
0 commit comments