Skip to content

Commit a7db694

Browse files
authored
Merge pull request #1538 from ychin/mmtabs-rtl
MMTabline: Add right-to-left (RTL) locale support
2 parents 62f5e1a + 3f8e405 commit a7db694

File tree

4 files changed

+152
-40
lines changed

4 files changed

+152
-40
lines changed

src/MacVim/MMAppController.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ + (void)registerDefaults
174174

175175
NSDictionary *macvimDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
176176
[NSNumber numberWithBool:NO], MMNoWindowKey,
177-
[NSNumber numberWithInt:120], MMTabMinWidthKey,
178-
[NSNumber numberWithInt:200], MMTabOptimumWidthKey,
177+
[NSNumber numberWithInt:130], MMTabMinWidthKey,
178+
[NSNumber numberWithInt:210], MMTabOptimumWidthKey,
179179
[NSNumber numberWithBool:YES], MMShowAddTabButtonKey,
180180
[NSNumber numberWithBool:NO], MMShowTabScrollButtonsKey,
181181
[NSNumber numberWithInt:2], MMTextInsetLeftKey,

src/MacVim/MMTabline/MMTabline.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@
5555
- (void)selectTabAtIndex:(NSInteger)index;
5656
- (MMTab *)tabAtIndex:(NSInteger)index;
5757
- (void)scrollTabToVisibleAtIndex:(NSInteger)index;
58-
- (void)scrollLeftOneTab;
59-
- (void)scrollRightOneTab;
58+
- (void)scrollBackwardOneTab;
59+
- (void)scrollForwardOneTab;
6060
- (void)setTablineSelBackground:(NSColor *)back foreground:(NSColor *)fore;
6161

6262
@end

src/MacVim/MMTabline/MMTabline.m

Lines changed: 146 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
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

1616
static 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
}

src/MacVim/MMVimView.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,12 @@ - (IBAction)scrollToCurrentTab:(id)sender
264264

265265
- (IBAction)scrollBackwardOneTab:(id)sender
266266
{
267-
[tabline scrollLeftOneTab];
267+
[tabline scrollBackwardOneTab];
268268
}
269269

270270
- (IBAction)scrollForwardOneTab:(id)sender
271271
{
272-
[tabline scrollRightOneTab];
272+
[tabline scrollForwardOneTab];
273273
}
274274

275275
- (void)showTabline:(BOOL)on

0 commit comments

Comments
 (0)