From 8406d28431f6c1b4a155f69a104315a52ea610e7 Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Tue, 28 Jan 2025 21:36:58 -0800 Subject: [PATCH 1/4] MMTabline: expose scroll button settings and fix misc scroll issues Allows the user to set whether to show the tabs scroll buttons or not in the preference pane as this is likely going to be a relatively popular configuration. Also, add new `macaction`'s for scrolling to selected tab and scrolling backward/forward so the user doesn't have to use the mouse to scroll. Fix misc issues with scrolling to a tab. The existing implementation was animating both the bounds size and position which would cause issues if the user resizes the window while still scrolling. Make sure the animation only touches the position. Also, the scrolling logic were using the physical locations of the scroll bounds to determine scroll condition, which doesn't account for the fact that it could be in the middle of animation. This led to rapid clicking of the scroll buttons having some clicks being ignored. Instead, fix it to always use the animated destination. --- runtime/doc/gui_mac.txt | 7 +++-- src/MacVim/Actions.plist | 6 +++++ src/MacVim/Base.lproj/Preferences.xib | 37 +++++++++++++++++++++++---- src/MacVim/MMAppController.h | 1 + src/MacVim/MMAppController.m | 23 ++++++++--------- src/MacVim/MMPreferenceController.h | 1 + src/MacVim/MMPreferenceController.m | 5 ++++ src/MacVim/MMTabline/MMTabline.h | 2 ++ src/MacVim/MMTabline/MMTabline.m | 21 ++++++++------- src/MacVim/MMVimView.h | 5 ++++ src/MacVim/MMVimView.m | 31 ++++++++++++++++++++++ 11 files changed, 111 insertions(+), 28 deletions(-) diff --git a/runtime/doc/gui_mac.txt b/runtime/doc/gui_mac.txt index 68963f4bf1..fde60648e0 100644 --- a/runtime/doc/gui_mac.txt +++ b/runtime/doc/gui_mac.txt @@ -573,8 +573,11 @@ _cycleWindows: Select next window (similar to ) _cycleWindowsBackwards: Select previous window (similar to ) _removeWindowFromStageManagerSet Remove window from a Stage Manager Set. Same as the "Remove Window from Set" menu item. -joinAllStageManagerSets Window will float among all Stage Manager sets -unjoinAllStageManagerSets Window will only show up in its own set +joinAllStageManagerSets: Window will float among all Stage Manager sets +unjoinAllStageManagerSets: Window will only show up in its own set +scrollToCurrentTab: Scroll to the selected tab in the GUI tab bar +scrollBackwardOneTab: Scroll backward by one tab in the tab bar +scrollForwardOneTab: Scroll forward by one tab in the tab bar ============================================================================== 7. Toolbar *macvim-toolbar* diff --git a/src/MacVim/Actions.plist b/src/MacVim/Actions.plist index 99d32b8def..6e468c5ac9 100644 --- a/src/MacVim/Actions.plist +++ b/src/MacVim/Actions.plist @@ -8,6 +8,12 @@ addNewTab: + scrollToCurrentTab: + + scrollBackwardOneTab: + + scrollForwardOneTab: + arrangeInFront: clearRecentDocuments: diff --git a/src/MacVim/Base.lproj/Preferences.xib b/src/MacVim/Base.lproj/Preferences.xib index 0dbab227b2..1aa2e8057d 100644 --- a/src/MacVim/Base.lproj/Preferences.xib +++ b/src/MacVim/Base.lproj/Preferences.xib @@ -280,11 +280,11 @@ - + - + @@ -345,7 +345,7 @@ - + @@ -407,6 +407,33 @@ + + + + + + + + + + + + + + + + @@ -508,7 +535,7 @@ - + @@ -595,7 +622,7 @@ - + diff --git a/src/MacVim/MMAppController.h b/src/MacVim/MMAppController.h index e9f9631556..f97c028c76 100644 --- a/src/MacVim/MMAppController.h +++ b/src/MacVim/MMAppController.h @@ -84,6 +84,7 @@ - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args; - (void)refreshAllAppearances; +- (void)refreshAllTabProperties; - (void)refreshAllFonts; - (void)refreshAllResizeConstraints; - (void)refreshAllTextViews; diff --git a/src/MacVim/MMAppController.m b/src/MacVim/MMAppController.m index e1c1f4ffee..78df273f4d 100644 --- a/src/MacVim/MMAppController.m +++ b/src/MacVim/MMAppController.m @@ -1229,19 +1229,22 @@ - (BOOL)openFiles:(NSArray *)filenames withArguments:(NSDictionary *)args - (void)refreshAllAppearances { - const NSUInteger count = [vimControllers count]; - for (unsigned i = 0; i < count; ++i) { - MMVimController *vc = [vimControllers objectAtIndex:i]; + for (MMVimController *vc in vimControllers) { [vc.windowController refreshApperanceMode]; } } +- (void)refreshAllTabProperties +{ + for (MMVimController *vc in vimControllers) { + [vc.windowController.vimView refreshTabProperties]; + } +} + /// Refresh all Vim text views' fonts. - (void)refreshAllFonts { - const NSUInteger count = [vimControllers count]; - for (unsigned i = 0; i < count; ++i) { - MMVimController *vc = [vimControllers objectAtIndex:i]; + for (MMVimController *vc in vimControllers) { [vc.windowController refreshFonts]; } } @@ -1250,9 +1253,7 @@ - (void)refreshAllFonts /// and resize the windows to match the constraints. - (void)refreshAllResizeConstraints { - const NSUInteger count = [vimControllers count]; - for (unsigned i = 0; i < count; ++i) { - MMVimController *vc = [vimControllers objectAtIndex:i]; + for (MMVimController *vc in vimControllers) { [vc.windowController updateResizeConstraints:YES]; } } @@ -1261,9 +1262,7 @@ - (void)refreshAllResizeConstraints /// cmdline alignment properties to make sure they are pinned properly. - (void)refreshAllTextViews { - const NSUInteger count = [vimControllers count]; - for (unsigned i = 0; i < count; ++i) { - MMVimController *vc = [vimControllers objectAtIndex:i]; + for (MMVimController *vc in vimControllers) { [vc.windowController.vimView.textView updateCmdlineRow]; vc.windowController.vimView.textView.needsDisplay = YES; } diff --git a/src/MacVim/MMPreferenceController.h b/src/MacVim/MMPreferenceController.h index 3021e1be13..e705f4e3a2 100644 --- a/src/MacVim/MMPreferenceController.h +++ b/src/MacVim/MMPreferenceController.h @@ -39,5 +39,6 @@ // Appearance pane - (IBAction)fontPropertiesChanged:(id)sender; +- (IBAction)tabsPropertiesChanged:(id)sender; @end diff --git a/src/MacVim/MMPreferenceController.m b/src/MacVim/MMPreferenceController.m index a556626496..4a615da45b 100644 --- a/src/MacVim/MMPreferenceController.m +++ b/src/MacVim/MMPreferenceController.m @@ -167,6 +167,11 @@ - (IBAction)fontPropertiesChanged:(id)sender [[MMAppController sharedInstance] refreshAllFonts]; } +- (IBAction)tabsPropertiesChanged:(id)sender +{ + [[MMAppController sharedInstance] refreshAllTabProperties]; +} + - (IBAction)smoothResizeChanged:(id)sender { [[MMAppController sharedInstance] refreshAllResizeConstraints]; diff --git a/src/MacVim/MMTabline/MMTabline.h b/src/MacVim/MMTabline/MMTabline.h index 462619d2f0..02d813fdf6 100644 --- a/src/MacVim/MMTabline/MMTabline.h +++ b/src/MacVim/MMTabline/MMTabline.h @@ -53,6 +53,8 @@ - (void)selectTabAtIndex:(NSInteger)index; - (MMTab *)tabAtIndex:(NSInteger)index; - (void)scrollTabToVisibleAtIndex:(NSInteger)index; +- (void)scrollLeftOneTab; +- (void)scrollRightOneTab; - (void)setTablineSelBackground:(NSColor *)back foreground:(NSColor *)fore; @end diff --git a/src/MacVim/MMTabline/MMTabline.m b/src/MacVim/MMTabline/MMTabline.m index 6f58ccbbfd..5ef0745ca1 100644 --- a/src/MacVim/MMTabline/MMTabline.m +++ b/src/MacVim/MMTabline/MMTabline.m @@ -8,9 +8,10 @@ CGFloat remainder; } TabWidth; -const CGFloat OptimumTabWidth = 200; +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 MMHoverButton* MakeHoverButton(MMTabline *tabline, NSString *imageName, NSString *tooltip, SEL action, BOOL continuous) { MMHoverButton *button = [MMHoverButton new]; @@ -759,7 +760,7 @@ - (void)updateTabScrollButtonsEnabledState - (void)scrollTabToVisibleAtIndex:(NSInteger)index { if (_tabs.count == 0) return; - if (index < 0 || index >= _tabs.count) return; + index = index < 0 ? 0 : (index >= _tabs.count ? _tabs.count - 1 : index); // Get the amount of time elapsed between the previous invocation // of this method and now. Use this elapsed time to set the animation @@ -780,8 +781,8 @@ - (void)scrollTabToVisibleAtIndex:(NSInteger)index NSTimeInterval elapsedTime = 0.1; #endif - NSRect tabFrame = _tabs[index].frame; - NSRect clipBounds = _scrollView.contentView.bounds; + NSRect tabFrame = _tabs[index].animator.frame; + NSRect clipBounds =_scrollView.contentView.animator.bounds; // One side or the other of the selected tab is clipped. if (!NSContainsRect(clipBounds, tabFrame)) { if (NSMinX(tabFrame) > NSMinX(clipBounds)) { @@ -793,18 +794,19 @@ - (void)scrollTabToVisibleAtIndex:(NSInteger)index } [NSAnimationContext beginGrouping]; [NSAnimationContext.currentContext setDuration:elapsedTime < 0.2 ? 0.05 : 0.2]; - _scrollView.contentView.animator.bounds = clipBounds; + [_scrollView.contentView.animator setBoundsOrigin:clipBounds.origin]; [NSAnimationContext endGrouping]; } } - (void)scrollLeftOneTab { - NSRect clipBounds = _scrollView.contentView.bounds; + NSRect clipBounds = _scrollView.contentView.animator.bounds; for (NSInteger i = _tabs.count - 1; i >= 0; i--) { NSRect tabFrame = _tabs[i].frame; if (!NSContainsRect(clipBounds, tabFrame)) { - if (NSMinX(tabFrame) < NSMinX(clipBounds)) { + CGFloat allowance = i == 0 ? 0 : NSWidth(tabFrame) * ScrollOneTabAllowance; + if (NSMinX(tabFrame) + allowance < NSMinX(clipBounds)) { [self scrollTabToVisibleAtIndex:i]; break; } @@ -814,11 +816,12 @@ - (void)scrollLeftOneTab - (void)scrollRightOneTab { - NSRect clipBounds = _scrollView.contentView.bounds; + NSRect clipBounds = _scrollView.contentView.animator.bounds; for (NSInteger i = 0; i < _tabs.count; i++) { NSRect tabFrame = _tabs[i].frame; if (!NSContainsRect(clipBounds, tabFrame)) { - if (NSMaxX(tabFrame) > NSMaxX(clipBounds)) { + CGFloat allowance = i == _tabs.count - 1 ? 0 : NSWidth(tabFrame) * ScrollOneTabAllowance; + if (NSMaxX(tabFrame) - allowance > NSMaxX(clipBounds)) { [self scrollTabToVisibleAtIndex:i]; break; } diff --git a/src/MacVim/MMVimView.h b/src/MacVim/MMVimView.h index 8f7da31a2a..d29b75c63c 100644 --- a/src/MacVim/MMVimView.h +++ b/src/MacVim/MMVimView.h @@ -44,7 +44,12 @@ - (MMTabline *)tabline; - (IBAction)addNewTab:(id)sender; +- (IBAction)scrollToCurrentTab:(id)sender; +- (IBAction)scrollBackwardOneTab:(id)sender; +- (IBAction)scrollForwardOneTab:(id)sender; +- (void)showTabline:(BOOL)on; - (void)updateTabsWithData:(NSData *)data; +- (void)refreshTabProperties; - (void)createScrollbarWithIdentifier:(int32_t)ident type:(int)type; - (BOOL)destroyScrollbarWithIdentifier:(int32_t)ident; diff --git a/src/MacVim/MMVimView.m b/src/MacVim/MMVimView.m index 35a55cbb30..15d7b57cbb 100644 --- a/src/MacVim/MMVimView.m +++ b/src/MacVim/MMVimView.m @@ -254,6 +254,31 @@ - (IBAction)addNewTab:(id)sender [vimController sendMessage:AddNewTabMsgID data:nil]; } +- (IBAction)scrollToCurrentTab:(id)sender +{ + [tabline scrollTabToVisibleAtIndex:tabline.selectedTabIndex]; +} + +- (IBAction)scrollBackwardOneTab:(id)sender +{ + [tabline scrollLeftOneTab]; +} + +- (IBAction)scrollForwardOneTab:(id)sender +{ + [tabline scrollRightOneTab]; +} + +- (void)showTabline:(BOOL)on +{ + [tabline setHidden:!on]; + if (!on) { + // When the tab is not shown we don't get tab updates from Vim. We just + // close all of them as otherwise we will be holding onto stale states. + [tabline closeAllTabs]; + } +} + /// Callback from Vim to update the tabline with new tab data - (void)updateTabsWithData:(NSData *)data { @@ -330,6 +355,12 @@ - (void)updateTabsWithData:(NSData *)data } } +- (void)refreshTabProperties +{ + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + tabline.showsTabScrollButtons = [ud boolForKey:MMShowTabScrollButtonsKey]; +} + - (void)createScrollbarWithIdentifier:(int32_t)ident type:(int)type { MMScroller *scroller = [[MMScroller alloc] initWithIdentifier:ident From 558c2e936a4eaebb63ff49b339192b0626dbdcc1 Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Tue, 28 Jan 2025 03:29:43 -0800 Subject: [PATCH 2/4] MMTabline: Use cached images for buttons, fix memory leaks and misc issues MMHoverButton was a bit inefficient in its image management. It always made a new template image which then had to be converted into two other images (image and alternateImage) for every button. Cache at least the template images with weak references so we don't have to keep generating new ones. For now, allow setImage: to create new images but they could be changed to also just use cached images as well. Also, fix memory leaks in the tabs codebase due to improper closure usage in blocks. They were subtly capturing the self pointer which led to the tab line and hover buttons never getting destroyed. Fix to make sure we never accidentally capture self and try to capture as little as possible. Another leak happens in the usage of the local event monitor that we use to intercept scroll wheel events. The API contract mandates that we remove the monitor which the code never does. Make sure we do that, and fix up the logic of the event interceptor to be more resilient and works better with third-party software (which could inject horizontal scroll events without holding down the Shift key). --- src/MacVim/MMTabline/MMHoverButton.h | 10 ++- src/MacVim/MMTabline/MMHoverButton.m | 67 ++++++++++++----- src/MacVim/MMTabline/MMTab.m | 2 +- src/MacVim/MMTabline/MMTabline.m | 105 ++++++++++++++++++++------- 4 files changed, 135 insertions(+), 49 deletions(-) diff --git a/src/MacVim/MMTabline/MMHoverButton.h b/src/MacVim/MMTabline/MMHoverButton.h index df3322b8a9..43351a9b73 100644 --- a/src/MacVim/MMTabline/MMHoverButton.h +++ b/src/MacVim/MMTabline/MMHoverButton.h @@ -6,6 +6,14 @@ @property (nonatomic, retain) NSColor *fgColor; -+ (NSImage *)imageNamed:(NSString *)name; +typedef enum : NSUInteger { + MMHoverButtonImageAddTab = 0, + MMHoverButtonImageCloseTab, + MMHoverButtonImageScrollLeft, + MMHoverButtonImageScrollRight, + MMHoverButtonImageCount +} MMHoverButtonImage; + ++ (NSImage *)imageFromType:(MMHoverButtonImage)imageType; @end diff --git a/src/MacVim/MMTabline/MMHoverButton.m b/src/MacVim/MMTabline/MMHoverButton.m index e51c4d8c30..0b9b076f85 100644 --- a/src/MacVim/MMTabline/MMHoverButton.m +++ b/src/MacVim/MMTabline/MMHoverButton.m @@ -6,41 +6,64 @@ @implementation MMHoverButton NSBox *_circle; } -+ (NSImage *)imageNamed:(NSString *)name ++ (NSImage *)imageFromType:(MMHoverButtonImage)imageType { - CGFloat size = [name isEqualToString:@"CloseTabButton"] ? 15 : 17; - return [NSImage imageWithSize:NSMakeSize(size, size) flipped:NO drawingHandler:^BOOL(NSRect dstRect) { - NSBezierPath *p = [NSBezierPath new]; - if ([name isEqualToString:@"AddTabButton"]) { + if (imageType >= MMHoverButtonImageCount) + return nil; + + CGFloat size = imageType == MMHoverButtonImageCloseTab ? 15 : 17; + + static __weak NSImage *imageCache[MMHoverButtonImageCount] = { nil }; + if (imageCache[imageType] != nil) + return imageCache[imageType]; + + BOOL (^drawFuncs[MMHoverButtonImageCount])(NSRect) = { + // AddTab + ^BOOL(NSRect dstRect) { + NSBezierPath *p = [NSBezierPath new]; [p moveToPoint:NSMakePoint( 8.5, 4.5)]; [p lineToPoint:NSMakePoint( 8.5, 12.5)]; [p moveToPoint:NSMakePoint( 4.5, 8.5)]; [p lineToPoint:NSMakePoint(12.5, 8.5)]; [p setLineWidth:1.2]; [p stroke]; - } - else if ([name isEqualToString:@"CloseTabButton"]) { + return YES; + }, + // CloseTab + ^BOOL(NSRect dstRect) { + NSBezierPath *p = [NSBezierPath new]; [p moveToPoint:NSMakePoint( 4.5, 4.5)]; [p lineToPoint:NSMakePoint(10.5, 10.5)]; [p moveToPoint:NSMakePoint( 4.5, 10.5)]; [p lineToPoint:NSMakePoint(10.5, 4.5)]; [p setLineWidth:1.2]; [p stroke]; - } - else if ([name isEqualToString:@"ScrollLeftButton"]) { + return YES; + }, + // ScrollLeft + ^BOOL(NSRect dstRect) { + NSBezierPath *p = [NSBezierPath new]; [p moveToPoint:NSMakePoint( 5.0, 8.5)]; [p lineToPoint:NSMakePoint(10.0, 4.5)]; [p lineToPoint:NSMakePoint(10.0, 12.5)]; [p fill]; - } - else if ([name isEqualToString:@"ScrollRightButton"]) { + return YES; + }, + // ScrollRight + ^BOOL(NSRect dstRect) { + NSBezierPath *p = [NSBezierPath new]; [p moveToPoint:NSMakePoint(12.0, 8.5)]; [p lineToPoint:NSMakePoint( 7.0, 4.5)]; [p lineToPoint:NSMakePoint( 7.0, 12.5)]; [p fill]; + return YES; } - return YES; - }]; + }; + NSImage *img = [NSImage imageWithSize:NSMakeSize(size, size) + flipped:NO + drawingHandler:drawFuncs[imageType]]; + imageCache[imageType] = img; + return img; } - (instancetype)initWithFrame:(NSRect)frameRect @@ -70,22 +93,28 @@ - (void)setFgColor:(NSColor *)color self.image = super.image; } -- (void)setImage:(NSImage *)image +- (void)setImage:(NSImage *)imageTemplate { - _circle.cornerRadius = image.size.width / 2.0; + _circle.cornerRadius = imageTemplate.size.width / 2.0; NSColor *fillColor = self.fgColor ?: NSColor.controlTextColor; - super.image = [NSImage imageWithSize:image.size flipped:NO drawingHandler:^BOOL(NSRect dstRect) { - [image drawInRect:dstRect]; + NSImage *image = [NSImage imageWithSize:imageTemplate.size + flipped:NO + drawingHandler:^BOOL(NSRect dstRect) { + [imageTemplate drawInRect:dstRect]; [fillColor set]; NSRectFillUsingOperation(dstRect, NSCompositingOperationSourceAtop); return YES; }]; - self.alternateImage = [NSImage imageWithSize:image.size flipped:NO drawingHandler:^BOOL(NSRect dstRect) { + NSImage *alternateImage = [NSImage imageWithSize:imageTemplate.size + flipped:NO + drawingHandler:^BOOL(NSRect dstRect) { [[fillColor colorWithAlphaComponent:0.2] set]; [[NSBezierPath bezierPathWithOvalInRect:dstRect] fill]; - [super.image drawInRect:dstRect]; + [image drawInRect:dstRect]; return YES; }]; + super.image = image; + self.alternateImage = alternateImage; } - (void)setEnabled:(BOOL)enabled diff --git a/src/MacVim/MMTabline/MMTab.m b/src/MacVim/MMTabline/MMTab.m index 5b3fe16ce4..1b6de407a9 100644 --- a/src/MacVim/MMTabline/MMTab.m +++ b/src/MacVim/MMTabline/MMTab.m @@ -39,7 +39,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect tabline:(MMTabline *)tabline _tabline = tabline; _closeButton = [MMHoverButton new]; - _closeButton.image = [MMHoverButton imageNamed:@"CloseTabButton"]; + _closeButton.image = [MMHoverButton imageFromType:MMHoverButtonImageCloseTab]; _closeButton.target = self; _closeButton.action = @selector(closeTab:); _closeButton.translatesAutoresizingMaskIntoConstraints = NO; diff --git a/src/MacVim/MMTabline/MMTabline.m b/src/MacVim/MMTabline/MMTabline.m index 5ef0745ca1..c5dd783011 100644 --- a/src/MacVim/MMTabline/MMTabline.m +++ b/src/MacVim/MMTabline/MMTabline.m @@ -13,9 +13,9 @@ 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 MMHoverButton* MakeHoverButton(MMTabline *tabline, NSString *imageName, NSString *tooltip, SEL action, BOOL continuous) { +static MMHoverButton* MakeHoverButton(MMTabline *tabline, MMHoverButtonImage imageType, NSString *tooltip, SEL action, BOOL continuous) { MMHoverButton *button = [MMHoverButton new]; - button.image = [MMHoverButton imageNamed:imageName]; + button.image = [MMHoverButton imageFromType:imageType]; button.translatesAutoresizingMaskIntoConstraints = NO; button.target = tabline; button.action = action; @@ -81,9 +81,9 @@ - (instancetype)initWithFrame:(NSRect)frameRect _scrollView.documentView = _tabsContainer; [self addSubview:_scrollView]; - _addTabButton = MakeHoverButton(self, @"AddTabButton", @"New Tab (⌘T)", @selector(addTabAtEnd), NO); - _leftScrollButton = MakeHoverButton(self, @"ScrollLeftButton", @"Scroll Tabs", @selector(scrollLeftOneTab), YES); - _rightScrollButton = MakeHoverButton(self, @"ScrollRightButton", @"Scroll Tabs", @selector(scrollRightOneTab), YES); + _addTabButton = MakeHoverButton(self, MMHoverButtonImageAddTab, @"New Tab (⌘T)", @selector(addTabAtEnd), NO); + _leftScrollButton = MakeHoverButton(self, MMHoverButtonImageScrollLeft, @"Scroll Tabs", @selector(scrollLeftOneTab), YES); + _rightScrollButton = MakeHoverButton(self, MMHoverButtonImageScrollRight, @"Scroll Tabs", @selector(scrollRightOneTab), YES); [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_leftScrollButton][_rightScrollButton]-5-[_scrollView]-5-[_addTabButton]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(_scrollView, _leftScrollButton, _rightScrollButton, _addTabButton)]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_scrollView]|" options:0 metrics:nil views:@{@"_scrollView":_scrollView}]]; @@ -96,29 +96,8 @@ - (instancetype)initWithFrame:(NSRect)frameRect [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didScroll:) name:NSViewBoundsDidChangeNotification object:_scrollView.contentView]; - // Monitor for scroll wheel events so we can scroll the tabline - // horizontally without the user having to hold down SHIFT. - _scrollWheelEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) { - NSPoint location = [_scrollView convertPoint:event.locationInWindow fromView:nil]; - // We want events: - // where the mouse is over the _scrollView - // and where the user is not modifying it with the SHIFT key - // and initiated by the scroll wheel and not the trackpad - if ([_scrollView mouse:location inRect:_scrollView.bounds] - && !event.modifierFlags - && !event.hasPreciseScrollingDeltas) - { - // Create a new scroll wheel event based on the original, - // but set the new deltaX to the original's deltaY. - // stackoverflow.com/a/38991946/111418 - CGEventRef cgEvent = CGEventCreateCopy(event.CGEvent); - CGEventSetIntegerValueField(cgEvent, kCGScrollWheelEventDeltaAxis2, event.scrollingDeltaY); - NSEvent *newEvent = [NSEvent eventWithCGEvent:cgEvent]; - CFRelease(cgEvent); - return newEvent; - } - return event; - }]; + [self addScrollWheelMonitor]; + } return self; } @@ -141,6 +120,32 @@ - (void)viewDidChangeEffectiveAppearance for (MMTab *tab in _tabs) tab.state = tab.state; } +- (void)viewDidHide +{ + if (_scrollWheelEventMonitor != nil) { + [NSEvent removeMonitor:_scrollWheelEventMonitor]; + _scrollWheelEventMonitor = nil; + } + [super viewDidHide]; +} + +- (void)viewDidUnhide +{ + [self addScrollWheelMonitor]; + [super viewDidUnhide]; +} + +- (void)dealloc +{ + if (_scrollWheelEventMonitor != nil) { + [NSEvent removeMonitor:_scrollWheelEventMonitor]; + _scrollWheelEventMonitor = nil; + } + + // This is not necessary after macOS 10.11, but there's no harm in doing so + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + #pragma mark - Accessors - (NSInteger)numberOfTabs @@ -546,6 +551,50 @@ - (TabWidth)tabWidthForTabs:(NSInteger)numTabs return (TabWidth){tabWidth, availableWidthForTabs - tabWidth * numTabs}; } +/// Install a scroll wheel event monitor so that we can convert vertical scroll +/// wheel events to horizontal ones, so that the user doesn't have to hold down +/// SHIFT key while scrolling. +/// +/// Caller *has* to call `removeMonitor:` on `_scrollWheelEventMonitor` +/// afterwards. +- (void)addScrollWheelMonitor +{ + // We have to use a local event monitor because we are not allowed to + // override NSScrollView's scrollWheel: method. If we do so we will lose + // macOS responsive scrolling. See: + // https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKitOlderNotes/index.html#10_9Scrolling + if (_scrollWheelEventMonitor != nil) + return; + __weak NSScrollView *scrollView_weak = _scrollView; + __weak __typeof__(self) self_weak = self; + _scrollWheelEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) { + // We want an event: + // - that actually belongs to this window + // - initiated by the scroll wheel and not the trackpad + // - is a vertical scroll event (if this is a horizontal scroll event + // either via holding SHIFT or third-party software we just let it + // through) + // - where the mouse is over the scroll view + if (event.window == self_weak.window + && !event.hasPreciseScrollingDeltas + && (event.scrollingDeltaX == 0 && event.scrollingDeltaY != 0) + && [scrollView_weak mouse:[scrollView_weak convertPoint:event.locationInWindow fromView:nil] + inRect:scrollView_weak.bounds]) + { + // Create a new scroll wheel event based on the original, + // but set the new deltaX to the original's deltaY. + // stackoverflow.com/a/38991946/111418 + CGEventRef cgEvent = CGEventCreateCopy(event.CGEvent); + CGEventSetIntegerValueField(cgEvent, kCGScrollWheelEventDeltaAxis1, 0); + CGEventSetIntegerValueField(cgEvent, kCGScrollWheelEventDeltaAxis2, event.scrollingDeltaY); + NSEvent *newEvent = [NSEvent eventWithCGEvent:cgEvent]; + CFRelease(cgEvent); + return newEvent; + } + return event; + }]; +} + - (void)fixupCloseButtons { if (_tabs.count == 1) { From 2745806351a9e9ef90d20311779ef700e053da9d Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Tue, 28 Jan 2025 03:23:02 -0800 Subject: [PATCH 3/4] MMTabline: Clear all tabs while showing / hiding tab line When the GUI tab line is hidden (either due to `set showtabline` or `set go-=e`), Vim stops sending update messages to the GUI. This means the tabs were stale and would never get cleared, and the moment we show the tabs we see a confusing animation when the tabs quicly try to rearrange to match Vim's state. To fix this, just make sure to clean up and remove all of them when we hide the tab line. --- src/MacVim/MMTabline/MMTabline.h | 1 + src/MacVim/MMTabline/MMTabline.m | 20 +++++++++++++++++++- src/MacVim/MMVimView.m | 2 ++ src/MacVim/MMWindowController.m | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/MacVim/MMTabline/MMTabline.h b/src/MacVim/MMTabline/MMTabline.h index 02d813fdf6..347cc86c78 100644 --- a/src/MacVim/MMTabline/MMTabline.h +++ b/src/MacVim/MMTabline/MMTabline.h @@ -33,6 +33,7 @@ - (NSInteger)addTabAtIndex:(NSInteger)index; - (void)closeTab:(MMTab *)tab force:(BOOL)force layoutImmediately:(BOOL)layoutImmediately; +- (void)closeAllTabs; /// Batch update all the tabs using tab tags as unique IDs. Tab line will handle /// creating / removing tabs as necessary, and moving tabs to their new diff --git a/src/MacVim/MMTabline/MMTabline.m b/src/MacVim/MMTabline/MMTabline.m index c5dd783011..ef45c58384 100644 --- a/src/MacVim/MMTabline/MMTabline.m +++ b/src/MacVim/MMTabline/MMTabline.m @@ -319,6 +319,18 @@ - (void)closeTab:(MMTab *)tab force:(BOOL)force layoutImmediately:(BOOL)layoutIm } } +- (void)closeAllTabs +{ + _selectedTabIndex = -1; + _draggedTab = nil; + _initialDraggedTabIndex = _finalDraggedTabIndex = NSNotFound; + for (MMTab *tab in _tabs) { + [tab removeFromSuperview]; + } + [_tabs removeAllObjects]; + [self fixupLayoutWithAnimation:NO]; +} + - (void)updateTabsByTags:(NSInteger *)tags len:(NSUInteger)len delayTabResize:(BOOL)delayTabResize { BOOL needUpdate = NO; @@ -612,7 +624,13 @@ - (void)fixupTabZOrder - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResize { - if (_tabs.count == 0) return; + if (_tabs.count == 0) { + NSRect frame = _tabsContainer.frame; + frame.size.width = 0; + _tabsContainer.frame = frame; + [self updateTabScrollButtonsEnabledState]; + return; + } if (delayResize) { // The pending delayed resize is trigged by mouse exit, but if we are diff --git a/src/MacVim/MMVimView.m b/src/MacVim/MMVimView.m index 15d7b57cbb..dc1db6060a 100644 --- a/src/MacVim/MMVimView.m +++ b/src/MacVim/MMVimView.m @@ -251,6 +251,8 @@ - (void)setDesiredRows:(int)r columns:(int)c - (IBAction)addNewTab:(id)sender { + // Callback from the "Create a new tab button". We override this so we can + // send a message to Vim first and let it handle it before replying back. [vimController sendMessage:AddNewTabMsgID data:nil]; } diff --git a/src/MacVim/MMWindowController.m b/src/MacVim/MMWindowController.m index 5ae435bf34..e062796052 100644 --- a/src/MacVim/MMWindowController.m +++ b/src/MacVim/MMWindowController.m @@ -868,7 +868,7 @@ - (void)processInputQueueDidFinish - (void)showTabline:(BOOL)on { - [[vimView tabline] setHidden:!on]; + [vimView showTabline:on]; [self updateTablineSeparator]; shouldMaximizeWindow = YES; } From 18f00e57e4efbb85706b389030ea85903b09c98f Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Wed, 29 Jan 2025 01:21:35 -0800 Subject: [PATCH 4/4] MMTabline: Add ability to turn off animation This is not exposed in the user preference pane, as it should be a niche case, but it's useful to have a way to turn off animations for either accessibility or user preferences reasons. --- runtime/doc/gui_mac.txt | 1 + runtime/doc/tags | 1 + src/MacVim/MMAppController.m | 1 + src/MacVim/MMTabline/MMTabline.h | 1 + src/MacVim/MMTabline/MMTabline.m | 16 ++++++++++++---- src/MacVim/MMVimView.m | 1 + src/MacVim/Miscellaneous.h | 1 + src/MacVim/Miscellaneous.m | 1 + 8 files changed, 19 insertions(+), 4 deletions(-) diff --git a/runtime/doc/gui_mac.txt b/runtime/doc/gui_mac.txt index fde60648e0..df2644f4e2 100644 --- a/runtime/doc/gui_mac.txt +++ b/runtime/doc/gui_mac.txt @@ -290,6 +290,7 @@ KEY VALUE ~ *MMDialogsTrackPwd* open/save dialogs track the Vim pwd [bool] *MMDisableLaunchAnimation* disable launch animation when opening a new MacVim window [bool] +*MMDisableTablineAnimation* disable animation in GUI tabs [bool] *MMFontPreserveLineSpacing* use the line-spacing as specified by font [bool] *MMLoginShell* use login shell for launching Vim [bool] *MMLoginShellArgument* login shell parameter [string] diff --git a/runtime/doc/tags b/runtime/doc/tags index b1b23149db..d8b5f59472 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -5648,6 +5648,7 @@ MMCmdLineAlignBottom gui_mac.txt /*MMCmdLineAlignBottom* MMDefaultTablineColors gui_mac.txt /*MMDefaultTablineColors* MMDialogsTrackPwd gui_mac.txt /*MMDialogsTrackPwd* MMDisableLaunchAnimation gui_mac.txt /*MMDisableLaunchAnimation* +MMDisableTablineAnimation gui_mac.txt /*MMDisableTablineAnimation* MMFontPreserveLineSpacing gui_mac.txt /*MMFontPreserveLineSpacing* MMFullScreenFadeTime gui_mac.txt /*MMFullScreenFadeTime* MMLoginShell gui_mac.txt /*MMLoginShell* diff --git a/src/MacVim/MMAppController.m b/src/MacVim/MMAppController.m index 78df273f4d..116ee74c27 100644 --- a/src/MacVim/MMAppController.m +++ b/src/MacVim/MMAppController.m @@ -196,6 +196,7 @@ + (void)registerDefaults MMUntitledWindowKey, [NSNumber numberWithBool:NO], MMNoWindowShadowKey, [NSNumber numberWithBool:NO], MMDisableLaunchAnimationKey, + [NSNumber numberWithBool:NO], MMDisableTablineAnimationKey, [NSNumber numberWithInt:0], MMAppearanceModeSelectionKey, [NSNumber numberWithBool:NO], MMNoTitleBarWindowKey, [NSNumber numberWithBool:NO], MMTitlebarAppearsTransparentKey, diff --git a/src/MacVim/MMTabline/MMTabline.h b/src/MacVim/MMTabline/MMTabline.h index 347cc86c78..eb59034990 100644 --- a/src/MacVim/MMTabline/MMTabline.h +++ b/src/MacVim/MMTabline/MMTabline.h @@ -16,6 +16,7 @@ @property (nonatomic) NSInteger minimumTabWidth; @property (nonatomic) BOOL showsAddTabButton; @property (nonatomic) BOOL showsTabScrollButtons; +@property (nonatomic) BOOL useAnimation; @property (nonatomic, readonly) NSInteger numberOfTabs; @property (nonatomic, retain, readonly) MMHoverButton *addTabButton; @property (nonatomic, retain) NSColor *tablineBgColor; diff --git a/src/MacVim/MMTabline/MMTabline.m b/src/MacVim/MMTabline/MMTabline.m index ef45c58384..eb37b9f072 100644 --- a/src/MacVim/MMTabline/MMTabline.m +++ b/src/MacVim/MMTabline/MMTabline.m @@ -64,6 +64,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect _tabs = [NSMutableArray new]; _showsAddTabButton = YES; // get from NSUserDefaults _showsTabScrollButtons = YES; // get from NSUserDefaults + _useAnimation = YES; // get from NSUserDefaults _selectedTabIndex = -1; @@ -624,6 +625,9 @@ - (void)fixupTabZOrder - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResize { + if (!self.useAnimation) + shouldAnimate = NO; + if (_tabs.count == 0) { NSRect frame = _tabsContainer.frame; frame.size.width = 0; @@ -859,10 +863,14 @@ - (void)scrollTabToVisibleAtIndex:(NSInteger)index // Left side of the selected tab is clipped. clipBounds.origin.x = tabFrame.origin.x; } - [NSAnimationContext beginGrouping]; - [NSAnimationContext.currentContext setDuration:elapsedTime < 0.2 ? 0.05 : 0.2]; - [_scrollView.contentView.animator setBoundsOrigin:clipBounds.origin]; - [NSAnimationContext endGrouping]; + if (_useAnimation) { + [NSAnimationContext beginGrouping]; + [NSAnimationContext.currentContext setDuration:elapsedTime < 0.2 ? 0.05 : 0.2]; + [_scrollView.contentView.animator setBoundsOrigin:clipBounds.origin]; + [NSAnimationContext endGrouping]; + } else { + [_scrollView.contentView setBoundsOrigin:clipBounds.origin]; + } } } diff --git a/src/MacVim/MMVimView.m b/src/MacVim/MMVimView.m index dc1db6060a..b696ac32ee 100644 --- a/src/MacVim/MMVimView.m +++ b/src/MacVim/MMVimView.m @@ -115,6 +115,7 @@ - (MMVimView *)initWithFrame:(NSRect)frame tabline.hidden = YES; tabline.showsAddTabButton = [ud boolForKey:MMShowAddTabButtonKey]; tabline.showsTabScrollButtons = [ud boolForKey:MMShowTabScrollButtonsKey]; + tabline.useAnimation = ![ud boolForKey:MMDisableTablineAnimationKey]; tabline.optimumTabWidth = [ud integerForKey:MMTabOptimumWidthKey]; tabline.minimumTabWidth = [ud integerForKey:MMTabMinWidthKey]; tabline.addTabButton.target = self; diff --git a/src/MacVim/Miscellaneous.h b/src/MacVim/Miscellaneous.h index 3b864602a8..0d297dd094 100644 --- a/src/MacVim/Miscellaneous.h +++ b/src/MacVim/Miscellaneous.h @@ -41,6 +41,7 @@ extern NSString *MMTitlebarAppearsTransparentKey; extern NSString *MMTitlebarShowsDocumentIconKey; extern NSString *MMNoWindowShadowKey; extern NSString *MMDisableLaunchAnimationKey; +extern NSString *MMDisableTablineAnimationKey; extern NSString *MMLoginShellKey; extern NSString *MMUntitledWindowKey; extern NSString *MMZoomBothKey; diff --git a/src/MacVim/Miscellaneous.m b/src/MacVim/Miscellaneous.m index 84b1bbec44..0b1e0523e2 100644 --- a/src/MacVim/Miscellaneous.m +++ b/src/MacVim/Miscellaneous.m @@ -37,6 +37,7 @@ NSString *MMTitlebarShowsDocumentIconKey = @"MMTitlebarShowsDocumentIcon"; NSString *MMNoWindowShadowKey = @"MMNoWindowShadow"; NSString *MMDisableLaunchAnimationKey = @"MMDisableLaunchAnimation"; +NSString *MMDisableTablineAnimationKey = @"MMDisableTablineAnimation"; NSString *MMLoginShellKey = @"MMLoginShell"; NSString *MMUntitledWindowKey = @"MMUntitledWindow"; NSString *MMZoomBothKey = @"MMZoomBoth";