diff --git a/src/MacVim/MMAppController.m b/src/MacVim/MMAppController.m index 116ee74c27..1dd4ac5a4e 100644 --- a/src/MacVim/MMAppController.m +++ b/src/MacVim/MMAppController.m @@ -174,8 +174,8 @@ + (void)registerDefaults NSDictionary *macvimDefaults = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:NO], MMNoWindowKey, - [NSNumber numberWithInt:120], MMTabMinWidthKey, - [NSNumber numberWithInt:200], MMTabOptimumWidthKey, + [NSNumber numberWithInt:130], MMTabMinWidthKey, + [NSNumber numberWithInt:210], MMTabOptimumWidthKey, [NSNumber numberWithBool:YES], MMShowAddTabButtonKey, [NSNumber numberWithBool:NO], MMShowTabScrollButtonsKey, [NSNumber numberWithInt:2], MMTextInsetLeftKey, diff --git a/src/MacVim/MMTabline/MMTabline.h b/src/MacVim/MMTabline/MMTabline.h index eb59034990..a43be7828b 100644 --- a/src/MacVim/MMTabline/MMTabline.h +++ b/src/MacVim/MMTabline/MMTabline.h @@ -55,8 +55,8 @@ - (void)selectTabAtIndex:(NSInteger)index; - (MMTab *)tabAtIndex:(NSInteger)index; - (void)scrollTabToVisibleAtIndex:(NSInteger)index; -- (void)scrollLeftOneTab; -- (void)scrollRightOneTab; +- (void)scrollBackwardOneTab; +- (void)scrollForwardOneTab; - (void)setTablineSelBackground:(NSColor *)back foreground:(NSColor *)fore; @end diff --git a/src/MacVim/MMTabline/MMTabline.m b/src/MacVim/MMTabline/MMTabline.m index 9d24fa1604..ff90d46d01 100644 --- a/src/MacVim/MMTabline/MMTabline.m +++ b/src/MacVim/MMTabline/MMTabline.m @@ -8,10 +8,10 @@ CGFloat remainder; } TabWidth; -const CGFloat OptimumTabWidth = 220; -const CGFloat MinimumTabWidth = 100; -const CGFloat TabOverlap = 6; -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. +static const CGFloat OptimumTabWidth = 200; +static const CGFloat MinimumTabWidth = 100; +static const CGFloat TabOverlap = 6; +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. static MMHoverButton* MakeHoverButton(MMTabline *tabline, MMHoverButtonImage imageType, NSString *tooltip, SEL action, BOOL continuous) { MMHoverButton *button = [MMHoverButton new]; @@ -44,8 +44,8 @@ @implementation MMTabline CGFloat _xOffsetForDrag; NSInteger _initialDraggedTabIndex; NSInteger _finalDraggedTabIndex; - MMHoverButton *_leftScrollButton; - MMHoverButton *_rightScrollButton; + MMHoverButton *_backwardScrollButton; + MMHoverButton *_forwardScrollButton; id _scrollWheelEventMonitor; } @@ -82,23 +82,40 @@ - (instancetype)initWithFrame:(NSRect)frameRect _scrollView.documentView = _tabsContainer; [self addSubview:_scrollView]; - _addTabButton = MakeHoverButton(self, MMHoverButtonImageAddTab, NSLocalizedString(@"create-new-tab-button", @"Create a new tab button"), @selector(addTabAtEnd), NO); - _leftScrollButton = MakeHoverButton(self, MMHoverButtonImageScrollLeft, NSLocalizedString(@"scroll-tabs-backward", @"Scroll backward button in tabs line"), @selector(scrollLeftOneTab), YES); - _rightScrollButton = MakeHoverButton(self, MMHoverButtonImageScrollRight, NSLocalizedString(@"scroll-tabs-forward", @"Scroll forward button in tabs line"), @selector(scrollRightOneTab), YES); - - [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_leftScrollButton][_rightScrollButton]-5-[_scrollView]-5-[_addTabButton]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(_scrollView, _leftScrollButton, _rightScrollButton, _addTabButton)]]; + _addTabButton = MakeHoverButton( + self, + MMHoverButtonImageAddTab, + NSLocalizedString(@"create-new-tab-button", @"Create a new tab button"), + @selector(addTabAtEnd), + NO); + _backwardScrollButton = MakeHoverButton( + self, + [self useRightToLeft] ? MMHoverButtonImageScrollRight : MMHoverButtonImageScrollLeft, + NSLocalizedString(@"scroll-tabs-backward", @"Scroll backward button in tabs line"), + @selector(scrollBackwardOneTab), + YES); + _forwardScrollButton = MakeHoverButton( + self, + [self useRightToLeft] ? MMHoverButtonImageScrollLeft : MMHoverButtonImageScrollRight, + NSLocalizedString(@"scroll-tabs-forward", @"Scroll forward button in tabs line"), + @selector(scrollForwardOneTab), + YES); + + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_backwardScrollButton][_forwardScrollButton]-5-[_scrollView]-5-[_addTabButton]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(_scrollView, _backwardScrollButton, _forwardScrollButton, _addTabButton)]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_scrollView]|" options:0 metrics:nil views:@{@"_scrollView":_scrollView}]]; - _tabScrollButtonsLeadingConstraint = [NSLayoutConstraint constraintWithItem:_leftScrollButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1 constant:5]; + _tabScrollButtonsLeadingConstraint = [NSLayoutConstraint constraintWithItem:_backwardScrollButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1 constant:5]; [self addConstraint:_tabScrollButtonsLeadingConstraint]; _addTabButtonTrailingConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:_addTabButton attribute:NSLayoutAttributeTrailing multiplier:1 constant:5]; [self addConstraint:_addTabButtonTrailingConstraint]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didScroll:) name:NSViewBoundsDidChangeNotification object:_scrollView.contentView]; + if ([self useRightToLeft]) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateTabsContainerBoundsForRTL:) name:NSViewFrameDidChangeNotification object:_tabsContainer]; + } [self addScrollWheelMonitor]; - } return self; } @@ -194,7 +211,7 @@ - (void)setShowsTabScrollButtons:(BOOL)showsTabScrollButtons // (see -drawRect: in MMTab.m). if (_showsTabScrollButtons != showsTabScrollButtons) { _showsTabScrollButtons = showsTabScrollButtons; - _tabScrollButtonsLeadingConstraint.constant = showsTabScrollButtons ? 5 : -((NSWidth(_leftScrollButton.frame) * 2) + 5 + MMTabShadowBlurRadius); + _tabScrollButtonsLeadingConstraint.constant = showsTabScrollButtons ? 5 : -((NSWidth(_backwardScrollButton.frame) * 2) + 5 + MMTabShadowBlurRadius); } } @@ -244,8 +261,8 @@ - (void)setTablineSelFgColor:(NSColor *)color { _tablineSelFgColor = color; _addTabButton.fgColor = color; - _leftScrollButton.fgColor = color; - _rightScrollButton.fgColor = color; + _backwardScrollButton.fgColor = color; + _forwardScrollButton.fgColor = color; for (MMTab *tab in _tabs) tab.state = tab.state; } @@ -280,6 +297,7 @@ - (NSInteger)addTabAtIndex:(NSInteger)index NSRect frame = _tabsContainer.bounds; frame.size.width = index == _tabs.count ? t.width + t.remainder : t.width; frame.origin.x = index * (t.width - TabOverlap); + frame = [self flipRectRTL:frame]; MMTab *newTab = [[MMTab alloc] initWithFrame:frame tabline:self]; [_tabs insertObject:newTab atIndex:index]; @@ -383,6 +401,7 @@ - (void)updateTabsByTags:(NSInteger *)tags len:(NSUInteger)len delayTabResize:(B NSRect frame = _tabsContainer.bounds; frame.size.width = i == (len - 1) ? t.width + t.remainder : t.width; frame.origin.x = i * (t.width - TabOverlap); + frame = [self flipRectRTL:frame]; MMTab *newTab = [[MMTab alloc] initWithFrame:frame tabline:self]; newTab.tag = tag; [newTabs addObject:newTab]; @@ -533,7 +552,7 @@ - (void)setTablineSelBackground:(NSColor *)back foreground:(NSColor *)fore #pragma mark - Helpers -NSComparisonResult SortTabsForZOrder(MMTab *tab1, MMTab *tab2, void *draggedTab) +NSComparisonResult SortTabsForZOrder(MMTab *tab1, MMTab *tab2, void *draggedTab, BOOL rtl) { // Z-order, highest to lowest: dragged, selected, hovered, rightmost if (tab1 == (__bridge MMTab *)draggedTab) return NSOrderedDescending; if (tab2 == (__bridge MMTab *)draggedTab) return NSOrderedAscending; @@ -541,11 +560,27 @@ NSComparisonResult SortTabsForZOrder(MMTab *tab1, MMTab *tab2, void *draggedTab) if (tab2.state == MMTabStateSelected) return NSOrderedAscending; if (tab1.state == MMTabStateUnselectedHover) return NSOrderedDescending; if (tab2.state == MMTabStateUnselectedHover) return NSOrderedAscending; - if (NSMinX(tab1.frame) < NSMinX(tab2.frame)) return NSOrderedAscending; - if (NSMinX(tab1.frame) > NSMinX(tab2.frame)) return NSOrderedDescending; + if (rtl) { + if (NSMinX(tab1.frame) > NSMinX(tab2.frame)) return NSOrderedAscending; + if (NSMinX(tab1.frame) < NSMinX(tab2.frame)) return NSOrderedDescending; + } else { + if (NSMinX(tab1.frame) < NSMinX(tab2.frame)) return NSOrderedAscending; + if (NSMinX(tab1.frame) > NSMinX(tab2.frame)) return NSOrderedDescending; + } return NSOrderedSame; } +NSComparisonResult SortTabsForZOrderLTR(MMTab *tab1, MMTab *tab2, void *draggedTab) +{ + return SortTabsForZOrder(tab1, tab2, draggedTab, NO); +} + + +NSComparisonResult SortTabsForZOrderRTL(MMTab *tab1, MMTab *tab2, void *draggedTab) +{ + return SortTabsForZOrder(tab1, tab2, draggedTab, YES); +} + - (TabWidth)tabWidthForTabs:(NSInteger)numTabs { // Each tab (except the first) overlaps the previous tab by TabOverlap @@ -620,9 +655,17 @@ - (void)fixupCloseButtons - (void)fixupTabZOrder { - [_tabsContainer sortSubviewsUsingFunction:SortTabsForZOrder context:(__bridge void *)(_draggedTab)]; + if ([self useRightToLeft]) { + [_tabsContainer sortSubviewsUsingFunction:SortTabsForZOrderRTL + context:(__bridge void *)(_draggedTab)]; + } else { + [_tabsContainer sortSubviewsUsingFunction:SortTabsForZOrderLTR + context:(__bridge void *)(_draggedTab)]; + } } +/// The main layout function that calculates the tab positions and animate them +/// accordingly. Call this every time tabs have been added/removed/moved. - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResize { if (!self.useAnimation) @@ -656,6 +699,7 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi frame.size.width = i == _tabs.count - 1 ? t.width + t.remainder : t.width; frame.origin.x = i != 0 ? i * (t.width - TabOverlap) : 0; } + frame = [self flipRectRTL:frame]; if (shouldAnimate) { [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) { context.allowsImplicitAnimation = YES; @@ -673,8 +717,20 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResi NSRect frame = _tabsContainer.frame; frame.size.width = t.width * _tabs.count - TabOverlap * (_tabs.count - 1); frame.size.width = NSWidth(frame) < NSWidth(_scrollView.frame) ? NSWidth(_scrollView.frame) : NSWidth(frame); - if (shouldAnimate) _tabsContainer.animator.frame = frame; - else _tabsContainer.frame = frame; + const BOOL sizeDecreasing = NSWidth(frame) < NSWidth(_tabsContainer.frame); + if ([self useRightToLeft]) { + // In RTL mode we flip the X coords and grow from 0 to negative. + // See updateTabsContainerBoundsForRTL which auto-updates the + // bounds to match the frame. + frame.origin.x = -NSWidth(frame); + } + if (shouldAnimate && sizeDecreasing) { + // Need to animate to make sure we don't immediately get clamped by + // the new size if we are already scrolled all the way to the back. + _tabsContainer.animator.frame = frame; + } else { + _tabsContainer.frame = frame; + } [self updateTabScrollButtonsEnabledState]; } } @@ -684,6 +740,41 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate [self fixupLayoutWithAnimation:shouldAnimate delayResize:NO]; } +#pragma mark - Right-to-left (RTL) support + +- (BOOL)useRightToLeft +{ + // MMTabs support RTL locales. In such locales user interface items are + // laid out from right to left. The layout of hover buttons and views are + // automatically flipped by AppKit, but we need to handle this manually in + // the tab placement logic since that is custom logic. + return self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft; +} + +- (void)updateTabsContainerBoundsForRTL:(NSNotification *)notification +{ + // In RTL mode, we grow the tabs container to the left. We want to preserve + // stability of the scroll view's bounds, and also have the tabs animate + // correctly. To do this, we have to make sure the container bounds matches + // the frame at all times. This "cancels out" the negative X offsets with + // each other and ease calculations. + // E.g. an MMTab with origin (-100,0) inside the _tabsContainer coordinate + // space will actually be (-100,0) in the scroll view as well. + // In LTR mode we don't need this, since _tabsContainer's origin is always + // at (0,0). + _tabsContainer.bounds = _tabsContainer.frame; +} + +- (NSRect)flipRectRTL:(NSRect)frame +{ + if ([self useRightToLeft]) { + // In right-to-left mode, we flip the X coordinates for all the tabs so + // they start at 0 and grow in the negative direction. + frame.origin.x = -NSMaxX(frame); + } + return frame; +} + #pragma mark - Mouse - (void)updateTrackingAreas @@ -796,9 +887,15 @@ - (void)mouseDragged:(NSEvent *)event [self fixupTabZOrder]; [_draggedTab setFrameOrigin:NSMakePoint(mouse.x - _xOffsetForDrag, 0)]; MMTab *selectedTab = _selectedTabIndex == -1 ? nil : _tabs[_selectedTabIndex]; + const BOOL rightToLeft = [self useRightToLeft]; [_tabs sortWithOptions:NSSortStable usingComparator:^NSComparisonResult(MMTab *t1, MMTab *t2) { - if (NSMinX(t1.frame) <= NSMinX(t2.frame)) return NSOrderedAscending; - if (NSMinX(t1.frame) > NSMinX(t2.frame)) return NSOrderedDescending; + if (rightToLeft) { + if (NSMaxX(t1.frame) >= NSMaxX(t2.frame)) return NSOrderedAscending; + if (NSMaxX(t1.frame) < NSMaxX(t2.frame)) return NSOrderedDescending; + } else { + if (NSMinX(t1.frame) <= NSMinX(t2.frame)) return NSOrderedAscending; + if (NSMinX(t1.frame) > NSMinX(t2.frame)) return NSOrderedDescending; + } return NSOrderedSame; }]; _selectedTabIndex = _selectedTabIndex == -1 ? -1 : [_tabs indexOfObject:selectedTab]; @@ -820,11 +917,18 @@ - (void)updateTabScrollButtonsEnabledState // on either side of _scrollView. NSRect clipBounds = _scrollView.contentView.bounds; if (NSWidth(_tabsContainer.frame) <= NSWidth(clipBounds)) { - _leftScrollButton.enabled = NO; - _rightScrollButton.enabled = NO; + _backwardScrollButton.enabled = NO; + _forwardScrollButton.enabled = NO; } else { - _leftScrollButton.enabled = clipBounds.origin.x > 0; - _rightScrollButton.enabled = clipBounds.origin.x + NSWidth(clipBounds) < NSMaxX(_tabsContainer.frame); + BOOL scrollLeftEnabled = NSMinX(clipBounds) > NSMinX(_tabsContainer.frame); + BOOL scrollRightEnabled = NSMaxX(clipBounds) < NSMaxX(_tabsContainer.frame); + if ([self useRightToLeft]) { + _backwardScrollButton.enabled = scrollRightEnabled; + _forwardScrollButton.enabled = scrollLeftEnabled; + } else { + _backwardScrollButton.enabled = scrollLeftEnabled; + _forwardScrollButton.enabled = scrollRightEnabled; + } } } @@ -874,14 +978,18 @@ - (void)scrollTabToVisibleAtIndex:(NSInteger)index } } -- (void)scrollLeftOneTab +- (void)scrollBackwardOneTab { NSRect clipBounds = _scrollView.contentView.animator.bounds; for (NSInteger i = _tabs.count - 1; i >= 0; i--) { NSRect tabFrame = _tabs[i].frame; if (!NSContainsRect(clipBounds, tabFrame)) { - CGFloat allowance = i == 0 ? 0 : NSWidth(tabFrame) * ScrollOneTabAllowance; - if (NSMinX(tabFrame) + allowance < NSMinX(clipBounds)) { + const CGFloat allowance = (i == 0) ? + 0 : NSWidth(tabFrame) * ScrollOneTabAllowance; + const BOOL outOfBounds = [self useRightToLeft] ? + NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds) : + NSMinX(tabFrame) + allowance < NSMinX(clipBounds); + if (outOfBounds) { [self scrollTabToVisibleAtIndex:i]; break; } @@ -889,14 +997,18 @@ - (void)scrollLeftOneTab } } -- (void)scrollRightOneTab +- (void)scrollForwardOneTab { NSRect clipBounds = _scrollView.contentView.animator.bounds; for (NSInteger i = 0; i < _tabs.count; i++) { NSRect tabFrame = _tabs[i].frame; if (!NSContainsRect(clipBounds, tabFrame)) { - CGFloat allowance = i == _tabs.count - 1 ? 0 : NSWidth(tabFrame) * ScrollOneTabAllowance; - if (NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds)) { + const CGFloat allowance = (i == _tabs.count - 1) ? + 0 : NSWidth(tabFrame) * ScrollOneTabAllowance; + const BOOL outOfBounds = [self useRightToLeft] ? + NSMinX(tabFrame) + allowance < NSMinX(clipBounds) : + NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds); + if (outOfBounds) { [self scrollTabToVisibleAtIndex:i]; break; } diff --git a/src/MacVim/MMVimView.m b/src/MacVim/MMVimView.m index b696ac32ee..96c6273094 100644 --- a/src/MacVim/MMVimView.m +++ b/src/MacVim/MMVimView.m @@ -264,12 +264,12 @@ - (IBAction)scrollToCurrentTab:(id)sender - (IBAction)scrollBackwardOneTab:(id)sender { - [tabline scrollLeftOneTab]; + [tabline scrollBackwardOneTab]; } - (IBAction)scrollForwardOneTab:(id)sender { - [tabline scrollRightOneTab]; + [tabline scrollForwardOneTab]; } - (void)showTabline:(BOOL)on