Skip to content

Commit 558c2e9

Browse files
committed
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).
1 parent 8406d28 commit 558c2e9

File tree

4 files changed

+135
-49
lines changed

4 files changed

+135
-49
lines changed

src/MacVim/MMTabline/MMHoverButton.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66

77
@property (nonatomic, retain) NSColor *fgColor;
88

9-
+ (NSImage *)imageNamed:(NSString *)name;
9+
typedef enum : NSUInteger {
10+
MMHoverButtonImageAddTab = 0,
11+
MMHoverButtonImageCloseTab,
12+
MMHoverButtonImageScrollLeft,
13+
MMHoverButtonImageScrollRight,
14+
MMHoverButtonImageCount
15+
} MMHoverButtonImage;
16+
17+
+ (NSImage *)imageFromType:(MMHoverButtonImage)imageType;
1018

1119
@end

src/MacVim/MMTabline/MMHoverButton.m

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,64 @@ @implementation MMHoverButton
66
NSBox *_circle;
77
}
88

9-
+ (NSImage *)imageNamed:(NSString *)name
9+
+ (NSImage *)imageFromType:(MMHoverButtonImage)imageType
1010
{
11-
CGFloat size = [name isEqualToString:@"CloseTabButton"] ? 15 : 17;
12-
return [NSImage imageWithSize:NSMakeSize(size, size) flipped:NO drawingHandler:^BOOL(NSRect dstRect) {
13-
NSBezierPath *p = [NSBezierPath new];
14-
if ([name isEqualToString:@"AddTabButton"]) {
11+
if (imageType >= MMHoverButtonImageCount)
12+
return nil;
13+
14+
CGFloat size = imageType == MMHoverButtonImageCloseTab ? 15 : 17;
15+
16+
static __weak NSImage *imageCache[MMHoverButtonImageCount] = { nil };
17+
if (imageCache[imageType] != nil)
18+
return imageCache[imageType];
19+
20+
BOOL (^drawFuncs[MMHoverButtonImageCount])(NSRect) = {
21+
// AddTab
22+
^BOOL(NSRect dstRect) {
23+
NSBezierPath *p = [NSBezierPath new];
1524
[p moveToPoint:NSMakePoint( 8.5, 4.5)];
1625
[p lineToPoint:NSMakePoint( 8.5, 12.5)];
1726
[p moveToPoint:NSMakePoint( 4.5, 8.5)];
1827
[p lineToPoint:NSMakePoint(12.5, 8.5)];
1928
[p setLineWidth:1.2];
2029
[p stroke];
21-
}
22-
else if ([name isEqualToString:@"CloseTabButton"]) {
30+
return YES;
31+
},
32+
// CloseTab
33+
^BOOL(NSRect dstRect) {
34+
NSBezierPath *p = [NSBezierPath new];
2335
[p moveToPoint:NSMakePoint( 4.5, 4.5)];
2436
[p lineToPoint:NSMakePoint(10.5, 10.5)];
2537
[p moveToPoint:NSMakePoint( 4.5, 10.5)];
2638
[p lineToPoint:NSMakePoint(10.5, 4.5)];
2739
[p setLineWidth:1.2];
2840
[p stroke];
29-
}
30-
else if ([name isEqualToString:@"ScrollLeftButton"]) {
41+
return YES;
42+
},
43+
// ScrollLeft
44+
^BOOL(NSRect dstRect) {
45+
NSBezierPath *p = [NSBezierPath new];
3146
[p moveToPoint:NSMakePoint( 5.0, 8.5)];
3247
[p lineToPoint:NSMakePoint(10.0, 4.5)];
3348
[p lineToPoint:NSMakePoint(10.0, 12.5)];
3449
[p fill];
35-
}
36-
else if ([name isEqualToString:@"ScrollRightButton"]) {
50+
return YES;
51+
},
52+
// ScrollRight
53+
^BOOL(NSRect dstRect) {
54+
NSBezierPath *p = [NSBezierPath new];
3755
[p moveToPoint:NSMakePoint(12.0, 8.5)];
3856
[p lineToPoint:NSMakePoint( 7.0, 4.5)];
3957
[p lineToPoint:NSMakePoint( 7.0, 12.5)];
4058
[p fill];
59+
return YES;
4160
}
42-
return YES;
43-
}];
61+
};
62+
NSImage *img = [NSImage imageWithSize:NSMakeSize(size, size)
63+
flipped:NO
64+
drawingHandler:drawFuncs[imageType]];
65+
imageCache[imageType] = img;
66+
return img;
4467
}
4568

4669
- (instancetype)initWithFrame:(NSRect)frameRect
@@ -70,22 +93,28 @@ - (void)setFgColor:(NSColor *)color
7093
self.image = super.image;
7194
}
7295

73-
- (void)setImage:(NSImage *)image
96+
- (void)setImage:(NSImage *)imageTemplate
7497
{
75-
_circle.cornerRadius = image.size.width / 2.0;
98+
_circle.cornerRadius = imageTemplate.size.width / 2.0;
7699
NSColor *fillColor = self.fgColor ?: NSColor.controlTextColor;
77-
super.image = [NSImage imageWithSize:image.size flipped:NO drawingHandler:^BOOL(NSRect dstRect) {
78-
[image drawInRect:dstRect];
100+
NSImage *image = [NSImage imageWithSize:imageTemplate.size
101+
flipped:NO
102+
drawingHandler:^BOOL(NSRect dstRect) {
103+
[imageTemplate drawInRect:dstRect];
79104
[fillColor set];
80105
NSRectFillUsingOperation(dstRect, NSCompositingOperationSourceAtop);
81106
return YES;
82107
}];
83-
self.alternateImage = [NSImage imageWithSize:image.size flipped:NO drawingHandler:^BOOL(NSRect dstRect) {
108+
NSImage *alternateImage = [NSImage imageWithSize:imageTemplate.size
109+
flipped:NO
110+
drawingHandler:^BOOL(NSRect dstRect) {
84111
[[fillColor colorWithAlphaComponent:0.2] set];
85112
[[NSBezierPath bezierPathWithOvalInRect:dstRect] fill];
86-
[super.image drawInRect:dstRect];
113+
[image drawInRect:dstRect];
87114
return YES;
88115
}];
116+
super.image = image;
117+
self.alternateImage = alternateImage;
89118
}
90119

91120
- (void)setEnabled:(BOOL)enabled

src/MacVim/MMTabline/MMTab.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect tabline:(MMTabline *)tabline
3939
_tabline = tabline;
4040

4141
_closeButton = [MMHoverButton new];
42-
_closeButton.image = [MMHoverButton imageNamed:@"CloseTabButton"];
42+
_closeButton.image = [MMHoverButton imageFromType:MMHoverButtonImageCloseTab];
4343
_closeButton.target = self;
4444
_closeButton.action = @selector(closeTab:);
4545
_closeButton.translatesAutoresizingMaskIntoConstraints = NO;

src/MacVim/MMTabline/MMTabline.m

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
const CGFloat TabOverlap = 6;
1414
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

16-
static MMHoverButton* MakeHoverButton(MMTabline *tabline, NSString *imageName, NSString *tooltip, SEL action, BOOL continuous) {
16+
static MMHoverButton* MakeHoverButton(MMTabline *tabline, MMHoverButtonImage imageType, NSString *tooltip, SEL action, BOOL continuous) {
1717
MMHoverButton *button = [MMHoverButton new];
18-
button.image = [MMHoverButton imageNamed:imageName];
18+
button.image = [MMHoverButton imageFromType:imageType];
1919
button.translatesAutoresizingMaskIntoConstraints = NO;
2020
button.target = tabline;
2121
button.action = action;
@@ -81,9 +81,9 @@ - (instancetype)initWithFrame:(NSRect)frameRect
8181
_scrollView.documentView = _tabsContainer;
8282
[self addSubview:_scrollView];
8383

84-
_addTabButton = MakeHoverButton(self, @"AddTabButton", @"New Tab (⌘T)", @selector(addTabAtEnd), NO);
85-
_leftScrollButton = MakeHoverButton(self, @"ScrollLeftButton", @"Scroll Tabs", @selector(scrollLeftOneTab), YES);
86-
_rightScrollButton = MakeHoverButton(self, @"ScrollRightButton", @"Scroll Tabs", @selector(scrollRightOneTab), YES);
84+
_addTabButton = MakeHoverButton(self, MMHoverButtonImageAddTab, @"New Tab (⌘T)", @selector(addTabAtEnd), NO);
85+
_leftScrollButton = MakeHoverButton(self, MMHoverButtonImageScrollLeft, @"Scroll Tabs", @selector(scrollLeftOneTab), YES);
86+
_rightScrollButton = MakeHoverButton(self, MMHoverButtonImageScrollRight, @"Scroll Tabs", @selector(scrollRightOneTab), YES);
8787

8888
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[_leftScrollButton][_rightScrollButton]-5-[_scrollView]-5-[_addTabButton]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(_scrollView, _leftScrollButton, _rightScrollButton, _addTabButton)]];
8989
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_scrollView]|" options:0 metrics:nil views:@{@"_scrollView":_scrollView}]];
@@ -96,29 +96,8 @@ - (instancetype)initWithFrame:(NSRect)frameRect
9696

9797
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didScroll:) name:NSViewBoundsDidChangeNotification object:_scrollView.contentView];
9898

99-
// Monitor for scroll wheel events so we can scroll the tabline
100-
// horizontally without the user having to hold down SHIFT.
101-
_scrollWheelEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
102-
NSPoint location = [_scrollView convertPoint:event.locationInWindow fromView:nil];
103-
// We want events:
104-
// where the mouse is over the _scrollView
105-
// and where the user is not modifying it with the SHIFT key
106-
// and initiated by the scroll wheel and not the trackpad
107-
if ([_scrollView mouse:location inRect:_scrollView.bounds]
108-
&& !event.modifierFlags
109-
&& !event.hasPreciseScrollingDeltas)
110-
{
111-
// Create a new scroll wheel event based on the original,
112-
// but set the new deltaX to the original's deltaY.
113-
// stackoverflow.com/a/38991946/111418
114-
CGEventRef cgEvent = CGEventCreateCopy(event.CGEvent);
115-
CGEventSetIntegerValueField(cgEvent, kCGScrollWheelEventDeltaAxis2, event.scrollingDeltaY);
116-
NSEvent *newEvent = [NSEvent eventWithCGEvent:cgEvent];
117-
CFRelease(cgEvent);
118-
return newEvent;
119-
}
120-
return event;
121-
}];
99+
[self addScrollWheelMonitor];
100+
122101
}
123102
return self;
124103
}
@@ -141,6 +120,32 @@ - (void)viewDidChangeEffectiveAppearance
141120
for (MMTab *tab in _tabs) tab.state = tab.state;
142121
}
143122

123+
- (void)viewDidHide
124+
{
125+
if (_scrollWheelEventMonitor != nil) {
126+
[NSEvent removeMonitor:_scrollWheelEventMonitor];
127+
_scrollWheelEventMonitor = nil;
128+
}
129+
[super viewDidHide];
130+
}
131+
132+
- (void)viewDidUnhide
133+
{
134+
[self addScrollWheelMonitor];
135+
[super viewDidUnhide];
136+
}
137+
138+
- (void)dealloc
139+
{
140+
if (_scrollWheelEventMonitor != nil) {
141+
[NSEvent removeMonitor:_scrollWheelEventMonitor];
142+
_scrollWheelEventMonitor = nil;
143+
}
144+
145+
// This is not necessary after macOS 10.11, but there's no harm in doing so
146+
[[NSNotificationCenter defaultCenter] removeObserver:self];
147+
}
148+
144149
#pragma mark - Accessors
145150

146151
- (NSInteger)numberOfTabs
@@ -546,6 +551,50 @@ - (TabWidth)tabWidthForTabs:(NSInteger)numTabs
546551
return (TabWidth){tabWidth, availableWidthForTabs - tabWidth * numTabs};
547552
}
548553

554+
/// Install a scroll wheel event monitor so that we can convert vertical scroll
555+
/// wheel events to horizontal ones, so that the user doesn't have to hold down
556+
/// SHIFT key while scrolling.
557+
///
558+
/// Caller *has* to call `removeMonitor:` on `_scrollWheelEventMonitor`
559+
/// afterwards.
560+
- (void)addScrollWheelMonitor
561+
{
562+
// We have to use a local event monitor because we are not allowed to
563+
// override NSScrollView's scrollWheel: method. If we do so we will lose
564+
// macOS responsive scrolling. See:
565+
// https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKitOlderNotes/index.html#10_9Scrolling
566+
if (_scrollWheelEventMonitor != nil)
567+
return;
568+
__weak NSScrollView *scrollView_weak = _scrollView;
569+
__weak __typeof__(self) self_weak = self;
570+
_scrollWheelEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
571+
// We want an event:
572+
// - that actually belongs to this window
573+
// - initiated by the scroll wheel and not the trackpad
574+
// - is a vertical scroll event (if this is a horizontal scroll event
575+
// either via holding SHIFT or third-party software we just let it
576+
// through)
577+
// - where the mouse is over the scroll view
578+
if (event.window == self_weak.window
579+
&& !event.hasPreciseScrollingDeltas
580+
&& (event.scrollingDeltaX == 0 && event.scrollingDeltaY != 0)
581+
&& [scrollView_weak mouse:[scrollView_weak convertPoint:event.locationInWindow fromView:nil]
582+
inRect:scrollView_weak.bounds])
583+
{
584+
// Create a new scroll wheel event based on the original,
585+
// but set the new deltaX to the original's deltaY.
586+
// stackoverflow.com/a/38991946/111418
587+
CGEventRef cgEvent = CGEventCreateCopy(event.CGEvent);
588+
CGEventSetIntegerValueField(cgEvent, kCGScrollWheelEventDeltaAxis1, 0);
589+
CGEventSetIntegerValueField(cgEvent, kCGScrollWheelEventDeltaAxis2, event.scrollingDeltaY);
590+
NSEvent *newEvent = [NSEvent eventWithCGEvent:cgEvent];
591+
CFRelease(cgEvent);
592+
return newEvent;
593+
}
594+
return event;
595+
}];
596+
}
597+
549598
- (void)fixupCloseButtons
550599
{
551600
if (_tabs.count == 1) {

0 commit comments

Comments
 (0)