diff --git a/src/MacVim/MMBackend.m b/src/MacVim/MMBackend.m index 569d4c7c98..9f38769967 100644 --- a/src/MacVim/MMBackend.m +++ b/src/MacVim/MMBackend.m @@ -764,32 +764,36 @@ - (void)selectTab:(int)index - (void)updateTabBar { + // Update the tab bar with the most up-to-date info, including number of + // tabs and titles/tooltips. MacVim would also like to know which specific + // tabs were moved/added/deleted in order for animation to work, but Vim + // does not have specific callbacks to listen to that. Instead, since the + // tabpage_T memory address is constant per tab, we use that as a permanent + // identifier for each GUI tab so MacVim can do the association. NSMutableData *data = [NSMutableData data]; + // 1. Current selected tab index int idx = tabpage_index(curtab) - 1; [data appendBytes:&idx length:sizeof(int)]; tabpage_T *tp; + // 2. Unique id for all the tabs + // Do these first so they appear as a consecutive memory block. for (tp = first_tabpage; tp != NULL; tp = tp->tp_next) { - // Count the number of windows in the tabpage. - //win_T *wp = tp->tp_firstwin; - //int wincount; - //for (wincount = 0; wp != NULL; wp = wp->w_next, ++wincount); - //[data appendBytes:&wincount length:sizeof(int)]; - - int tabProp = MMTabInfoCount; - [data appendBytes:&tabProp length:sizeof(int)]; - for (tabProp = MMTabLabel; tabProp < MMTabInfoCount; ++tabProp) { + [data appendBytes:&tp length:sizeof(void*)]; + } + // Null terminate the unique IDs. + tp = 0; + [data appendBytes:&tp length:sizeof(void*)]; + // 3. Labels and tooltips of each tab + for (tp = first_tabpage; tp != NULL; tp = tp->tp_next) { + for (int tabProp = MMTabLabel; tabProp < MMTabInfoCount; ++tabProp) { // This function puts the label of the tab in the global 'NameBuff'. get_tabline_label(tp, (tabProp == MMTabToolTip)); - NSString *s = [NSString stringWithVimString:NameBuff]; - int len = [s lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - if (len < 0) - len = 0; - - [data appendBytes:&len length:sizeof(int)]; + size_t len = STRLEN(NameBuff); + [data appendBytes:&len length:sizeof(size_t)]; if (len > 0) - [data appendBytes:[s UTF8String] length:len]; + [data appendBytes:NameBuff length:len]; } } diff --git a/src/MacVim/MMTabline/MMTab.h b/src/MacVim/MMTabline/MMTab.h index 2bd05037d2..8a6c198a83 100644 --- a/src/MacVim/MMTabline/MMTab.h +++ b/src/MacVim/MMTabline/MMTab.h @@ -14,6 +14,7 @@ typedef enum : NSInteger { @interface MMTab : NSView +@property (nonatomic, readwrite) NSInteger tag; ///< Unique identifier that caller can set for the tab @property (nonatomic, copy) NSString *title; @property (nonatomic, getter=isCloseButtonHidden) BOOL closeButtonHidden; @property (nonatomic) MMTabState state; diff --git a/src/MacVim/MMTabline/MMTab.m b/src/MacVim/MMTabline/MMTab.m index e69b1335f2..5b3fe16ce4 100644 --- a/src/MacVim/MMTabline/MMTab.m +++ b/src/MacVim/MMTabline/MMTab.m @@ -20,6 +20,8 @@ @implementation MMTab NSTextField *_titleLabel; } +@synthesize tag = _tag; + + (id)defaultAnimationForKey:(NSAnimatablePropertyKey)key { if ([key isEqualToString:@"fillColor"]) { diff --git a/src/MacVim/MMTabline/MMTabline.h b/src/MacVim/MMTabline/MMTabline.h index 18c7dca70e..462619d2f0 100644 --- a/src/MacVim/MMTabline/MMTabline.h +++ b/src/MacVim/MMTabline/MMTabline.h @@ -10,7 +10,8 @@ @interface MMTabline : NSView -@property (nonatomic) NSInteger selectedTabIndex; +/// The index of the selected tab. Can be -1 if nothing is selected. +@property (nonatomic, readonly) NSInteger selectedTabIndex; @property (nonatomic) NSInteger optimumTabWidth; @property (nonatomic) NSInteger minimumTabWidth; @property (nonatomic) BOOL showsAddTabButton; @@ -24,10 +25,31 @@ @property (nonatomic, retain) NSColor *tablineFillFgColor; @property (nonatomic, weak) id delegate; +/// Add a tab at the end. It's not selected automatically. - (NSInteger)addTabAtEnd; +/// Add a tab after the selected one. It's not selected automatically. - (NSInteger)addTabAfterSelectedTab; +/// Add a tab at specified index. It's not selected automatically. - (NSInteger)addTabAtIndex:(NSInteger)index; + - (void)closeTab:(MMTab *)tab force:(BOOL)force layoutImmediately:(BOOL)layoutImmediately; + +/// 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 +/// positions. +/// +/// The tags array has to have unique items only, and each existing MMTab also +/// has to have unique tags. +/// +/// @param tags The list of unique tags that are cross-referenced with each +/// MMTab's tag. Order within the array indicates the desired tab order. +/// @param len The length of the tags array. +/// @param delayTabResize If true, do not resize tab widths until the the tab +/// line loses focus. This helps preserve the relative tab positions and +/// lines up the close buttons to the previous tab. This will also +/// prevent scrolling to the new selected tab. +- (void)updateTabsByTags:(NSInteger *)tags len:(NSUInteger)len delayTabResize:(BOOL)delayTabResize; + - (void)selectTabAtIndex:(NSInteger)index; - (MMTab *)tabAtIndex:(NSInteger)index; - (void)scrollTabToVisibleAtIndex:(NSInteger)index; diff --git a/src/MacVim/MMTabline/MMTabline.m b/src/MacVim/MMTabline/MMTabline.m index 5d0d04c7ba..6f58ccbbfd 100644 --- a/src/MacVim/MMTabline/MMTabline.m +++ b/src/MacVim/MMTabline/MMTabline.m @@ -39,7 +39,6 @@ @implementation MMTabline NSLayoutConstraint *_tabScrollButtonsLeadingConstraint; NSLayoutConstraint *_addTabButtonTrailingConstraint; BOOL _pendingFixupLayout; - MMTab *_selectedTab; MMTab *_draggedTab; CGFloat _xOffsetForDrag; NSInteger _initialDraggedTabIndex; @@ -64,7 +63,11 @@ - (instancetype)initWithFrame:(NSRect)frameRect _tabs = [NSMutableArray new]; _showsAddTabButton = YES; // get from NSUserDefaults _showsTabScrollButtons = YES; // get from NSUserDefaults - + + _selectedTabIndex = -1; + + _initialDraggedTabIndex = _finalDraggedTabIndex = NSNotFound; + // This view holds the tab views. _tabsContainer = [NSView new]; _tabsContainer.frame = (NSRect){{0, 0}, frameRect.size}; @@ -269,7 +272,7 @@ - (NSInteger)addTabAtIndex:(NSInteger)index TabWidth t = [self tabWidthForTabs:_tabs.count + 1]; NSRect frame = _tabsContainer.bounds; frame.size.width = index == _tabs.count ? t.width + t.remainder : t.width; - frame.origin.x = index ? index * (t.width - TabOverlap) : 0; + frame.origin.x = index * (t.width - TabOverlap); MMTab *newTab = [[MMTab alloc] initWithFrame:frame tabline:self]; [_tabs insertObject:newTab atIndex:index]; @@ -292,14 +295,7 @@ - (void)closeTab:(MMTab *)tab force:(BOOL)force layoutImmediately:(BOOL)layoutIm if (![self.delegate tabline:self shouldCloseTabAtIndex:index]) return; } if (index != NSNotFound) { - CGFloat w = NSWidth(tab.frame) - TabOverlap; [tab removeFromSuperview]; - for (NSInteger i = index + 1; i < _tabs.count; i++) { - MMTab *tv = _tabs[i]; - NSRect frame = tv.frame; - frame.origin.x -= w; - tv.animator.frame = frame; - } [_tabs removeObject:tab]; if (index <= _selectedTabIndex) { if (index < _selectedTabIndex || index > _tabs.count - 1) { @@ -311,26 +307,161 @@ - (void)closeTab:(MMTab *)tab force:(BOOL)force layoutImmediately:(BOOL)layoutIm } [self fixupCloseButtons]; [self evaluateHoverStateForMouse:[self.window mouseLocationOutsideOfEventStream]]; - if (layoutImmediately) [self fixupLayoutWithAnimation:YES]; - else _pendingFixupLayout = YES; + [self fixupLayoutWithAnimation:YES delayResize:!layoutImmediately]; } else { NSLog(@"CANNOT FIND TAB TO REMOVE"); } } +- (void)updateTabsByTags:(NSInteger *)tags len:(NSUInteger)len delayTabResize:(BOOL)delayTabResize +{ + BOOL needUpdate = NO; + if (len != _tabs.count) { + needUpdate = YES; + } else { + for (NSUInteger i = 0; i < len; i++) { + MMTab *tab = _tabs[i]; + if (tab.tag != tags[i]) { + needUpdate = YES; + break; + } + } + } + if (!needUpdate) + return; + + // Perform a diff between the existing tabs (using MMTab's tags as unique + // identifiers) and the input specified tags + + // Create a mapping for tags->index. Could potentially cache this but it's + // simpler to recreate this every time to avoid tracking states. + NSMutableDictionary *tagToTabIdx = [NSMutableDictionary dictionaryWithCapacity:_tabs.count]; + for (NSUInteger i = 0; i < _tabs.count; i++) { + MMTab *tab = _tabs[i]; + if (tagToTabIdx[@(tab.tag)] != nil) { + NSLog(@"Duplicate tag found in tabs"); + // Duplicates are not supposed to exist. We need to remove the view + // here because the algorithm below will not handle this case and + // leaves stale views. + [tab removeFromSuperview]; + continue; + } + tagToTabIdx[@(tab.tag)] = @(i); + } + + const NSInteger oldSelectedTabTag = _selectedTabIndex < 0 ? 0 : _tabs[_selectedTabIndex].tag; + NSInteger newSelectedTabIndex = -1; + + // Allocate a new tabs list and store all the new and moved tabs there. This + // is simpler than an in-place algorithm. + NSMutableArray *newTabs = [NSMutableArray arrayWithCapacity:len]; + for (NSUInteger i = 0; i < len; i++) { + NSInteger tag = tags[i]; + NSNumber *newTabIdxObj = [tagToTabIdx objectForKey:@(tag)]; + if (newTabIdxObj == nil) { + // Create new tab + TabWidth t = [self tabWidthForTabs:len]; + NSRect frame = _tabsContainer.bounds; + frame.size.width = i == (len - 1) ? t.width + t.remainder : t.width; + frame.origin.x = i * (t.width - TabOverlap); + MMTab *newTab = [[MMTab alloc] initWithFrame:frame tabline:self]; + newTab.tag = tag; + [newTabs addObject:newTab]; + [_tabsContainer addSubview:newTab]; + } else { + // Move existing tab + NSUInteger newTabIdx = [newTabIdxObj unsignedIntegerValue]; + [newTabs addObject:_tabs[newTabIdx]]; + [tagToTabIdx removeObjectForKey:@(tag)]; + + // Remap indices if needed + if (newTabIdx == _selectedTabIndex) { + newSelectedTabIndex = newTabs.count - 1; + } + if (newTabIdx == _initialDraggedTabIndex) { + _initialDraggedTabIndex = newTabs.count - 1; + _finalDraggedTabIndex = _initialDraggedTabIndex; + } + } + } + + // Now go through the remaining tabs that did not make it to the new list + // and remove them. + NSInteger numDeletedTabsBeforeSelected = 0; + for (NSUInteger i = 0; i < _tabs.count; i++) { + MMTab *tab = _tabs[i]; + if ([tagToTabIdx objectForKey:@(tab.tag)] == nil) { + continue; + } + [tab removeFromSuperview]; + if (i < _selectedTabIndex) { + numDeletedTabsBeforeSelected++; + } + if (_draggedTab != nil && _draggedTab == tab) { + _draggedTab = nil; + _initialDraggedTabIndex = _finalDraggedTabIndex = NSNotFound; + } + } + const BOOL selectedTabMovedByDeleteOnly = newSelectedTabIndex != -1 && + (newSelectedTabIndex == _selectedTabIndex - numDeletedTabsBeforeSelected); + + _tabs = newTabs; + + if (newSelectedTabIndex == -1) { + // The old selected tab is removed. Select a new one nearby. + newSelectedTabIndex = _selectedTabIndex >= _tabs.count ? _tabs.count - 1 : _selectedTabIndex; + } + [self selectTabAtIndex:newSelectedTabIndex]; + + [self fixupLayoutWithAnimation:YES delayResize:delayTabResize]; + [self fixupCloseButtons]; + [self evaluateHoverStateForMouse:[self.window mouseLocationOutsideOfEventStream]]; + + // Heuristics for scrolling to the selected tab after update: + // 1. If 'delayTabResize' is set, we are trying to line up tab positions, do + // DON'T scroll, even if the old selected tab was removed. + // 2. Otherwise if we changed tab selection (happens when the selected tab + // was removed), just scroll to the new selected tab. + // 3. If the selected tab has moved in position, scroll to it, unless it + // only moved due to the earlier tabs being deleted (meaning that the tab + // ordering was preserved). This helps prevent unnecessary scrolling + // around when the user is trying to delete tabs in other areas. + // This chould potentially be exposed to the caller for more custimization. + const NSInteger newSelectedTabTag = _selectedTabIndex < 0 ? 0 : _tabs[_selectedTabIndex].tag; + BOOL scrollToSelected = NO; + if (!delayTabResize) { + if (oldSelectedTabTag != newSelectedTabTag) + scrollToSelected = YES; + else if (!selectedTabMovedByDeleteOnly) + scrollToSelected = YES; + } + if (scrollToSelected) + [self scrollTabToVisibleAtIndex:_selectedTabIndex]; +} + - (void)selectTabAtIndex:(NSInteger)index { - if (_selectedTabIndex <= _tabs.count - 1) { + if (_draggedTab != nil) { + // Selected a non-dragged tab, simply unset the dragging operation. This + // is somewhat Vim-specific, as it does not support re-ordering a + // non-active tab. Could be made configurable in the future. + if (index < 0 || index >= _tabs.count || _tabs[index] != _draggedTab) { + _draggedTab = nil; + _initialDraggedTabIndex = _finalDraggedTabIndex = NSNotFound; + [self fixupLayoutWithAnimation:YES]; + } + } + if (_selectedTabIndex >= 0 && _selectedTabIndex <= _tabs.count - 1) { _tabs[_selectedTabIndex].state = MMTabStateUnselected; } if (index <= _tabs.count - 1) { _selectedTabIndex = index; - _tabs[_selectedTabIndex].state = MMTabStateSelected; + if (index >= 0) + _tabs[_selectedTabIndex].state = MMTabStateSelected; } else { NSLog(@"TRIED TO SELECT OUT OF BOUNDS: %ld/%ld", index, _tabs.count - 1); } - _selectedTab = _tabs[_selectedTabIndex]; [self fixupTabZOrder]; } @@ -429,17 +560,30 @@ - (void)fixupTabZOrder [_tabsContainer sortSubviewsUsingFunction:SortTabsForZOrder context:(__bridge void *)(_draggedTab)]; } -- (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate +- (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate delayResize:(BOOL)delayResize { if (_tabs.count == 0) return; + if (delayResize) { + // The pending delayed resize is trigged by mouse exit, but if we are + // already outside, then there's nothing to delay. + NSPoint locationInWindow = [self.window mouseLocationOutsideOfEventStream]; + if (![self mouse:locationInWindow inRect:self.frame]) { + delayResize = NO; + } + } + TabWidth t = [self tabWidthForTabs:_tabs.count]; for (NSInteger i = 0; i < _tabs.count; i++) { MMTab *tab = _tabs[i]; if (_draggedTab == tab) continue; NSRect frame = tab.frame; - frame.size.width = i == _tabs.count - 1 ? t.width + t.remainder : t.width; - frame.origin.x = i ? i * (t.width - TabOverlap) : 0; + if (delayResize) { + frame.origin.x = i != 0 ? i * (NSWidth(_tabs[i-1].frame) - TabOverlap) : 0; + } else { + frame.size.width = i == _tabs.count - 1 ? t.width + t.remainder : t.width; + frame.origin.x = i != 0 ? i * (t.width - TabOverlap) : 0; + } if (shouldAnimate) { [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) { context.allowsImplicitAnimation = YES; @@ -450,13 +594,22 @@ - (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate tab.frame = frame; } } - // _tabsContainer expands to fit tabs, is at least as wide as _scrollView. - 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; - [self updateTabScrollButtonsEnabledState]; + if (delayResize) { + _pendingFixupLayout = YES; + } else { + // _tabsContainer expands to fit tabs, is at least as wide as _scrollView. + 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; + [self updateTabScrollButtonsEnabledState]; + } +} + +- (void)fixupLayoutWithAnimation:(BOOL)shouldAnimate +{ + [self fixupLayoutWithAnimation:shouldAnimate delayResize:NO]; } #pragma mark - Mouse @@ -556,6 +709,7 @@ - (void)mouseUp:(NSEvent *)event [self.delegate tabline:self didDragTab:_tabs[_finalDraggedTabIndex] toIndex:_finalDraggedTabIndex]; } } + _initialDraggedTabIndex = _finalDraggedTabIndex = NSNotFound; } - (void)mouseDragged:(NSEvent *)event @@ -569,12 +723,13 @@ - (void)mouseDragged:(NSEvent *)event [_tabsContainer autoscroll:event]; [self fixupTabZOrder]; [_draggedTab setFrameOrigin:NSMakePoint(mouse.x - _xOffsetForDrag, 0)]; + MMTab *selectedTab = _selectedTabIndex == -1 ? nil : _tabs[_selectedTabIndex]; [_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; return NSOrderedSame; }]; - _selectedTabIndex = [_tabs indexOfObject:_selectedTab]; + _selectedTabIndex = _selectedTabIndex == -1 ? -1 : [_tabs indexOfObject:selectedTab]; _finalDraggedTabIndex = [_tabs indexOfObject:_draggedTab]; [self fixupLayoutWithAnimation:YES]; } @@ -604,6 +759,7 @@ - (void)updateTabScrollButtonsEnabledState - (void)scrollTabToVisibleAtIndex:(NSInteger)index { if (_tabs.count == 0) return; + if (index < 0 || index >= _tabs.count) return; // Get the amount of time elapsed between the previous invocation // of this method and now. Use this elapsed time to set the animation diff --git a/src/MacVim/MMVimView.h b/src/MacVim/MMVimView.h index eac3d4da39..8f7da31a2a 100644 --- a/src/MacVim/MMVimView.h +++ b/src/MacVim/MMVimView.h @@ -20,8 +20,9 @@ @interface MMVimView : NSView { + /// The tab that has been requested to be closed and waiting on Vim to respond + NSInteger pendingCloseTabID; MMTabline *tabline; - MMTab *tabToClose; MMVimController *vimController; MMTextView *textView; NSMutableArray *scrollbars; @@ -44,8 +45,6 @@ - (MMTabline *)tabline; - (IBAction)addNewTab:(id)sender; - (void)updateTabsWithData:(NSData *)data; -- (void)selectTabWithIndex:(int)idx; -- (MMTab *)addNewTab; - (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 2a88fa9616..35a55cbb30 100644 --- a/src/MacVim/MMVimView.m +++ b/src/MacVim/MMVimView.m @@ -254,118 +254,82 @@ - (IBAction)addNewTab:(id)sender [vimController sendMessage:AddNewTabMsgID data:nil]; } +/// Callback from Vim to update the tabline with new tab data - (void)updateTabsWithData:(NSData *)data { const void *p = [data bytes]; - const void *end = p + [data length]; - int tabIdx = 0; - BOOL didCloseTab = NO; - - // Count how many tabs Vim has and compare to the number MacVim's tabline has. - const void *q = [data bytes]; - int vimNumberOfTabs = 0; - q += sizeof(int); // skip over current tab index - while (q < end) { - int infoCount = *((int*)q); q += sizeof(int); - for (unsigned i = 0; i < infoCount; ++i) { - int length = *((int*)q); q += sizeof(int); - if (length <= 0) continue; - q += length; - if (i == MMTabLabel) ++vimNumberOfTabs; - } - } - // Close the specific tab where the user clicked the close button. - if (tabToClose && vimNumberOfTabs == tabline.numberOfTabs - 1) { - [tabline closeTab:tabToClose force:YES layoutImmediately:NO]; - tabToClose = nil; - didCloseTab = YES; - } + const void * const end = p + [data length]; - // HACK! Current tab is first in the message. This way it is not - // necessary to guess which tab should be the selected one (this can be - // problematic for instance when new tabs are created). + // 1. Current tab is first in the message. int curtabIdx = *((int*)p); p += sizeof(int); + // 2. Read all the tab IDs (which uniquely identify each tab), and count + // the number of Vim tabs in the process of doing so. + int numTabs = 0; + BOOL pendingCloseTabClosed = (pendingCloseTabID != 0); + const intptr_t * const tabIDs = p; while (p < end) { - MMTab *tv; + intptr_t tabID = *((intptr_t*)p); p += sizeof(intptr_t); + if (tabID == 0) // null-terminated + break; + if (pendingCloseTabID != 0 && (NSInteger)tabID == pendingCloseTabID) { + // Vim hasn't gotten around to handling the tab close message yet, + // just wait until it has done so. + pendingCloseTabClosed = NO; + } + numTabs += 1; + } - //int wincount = *((int*)p); p += sizeof(int); - int infoCount = *((int*)p); p += sizeof(int); - unsigned i; - for (i = 0; i < infoCount; ++i) { - int length = *((int*)p); p += sizeof(int); + BOOL delayTabResize = NO; + if (pendingCloseTabClosed) { + // When the user has pressed a tab close button, only animate tab + // positions, not the widths. This allows the next tab's close button + // to line up with the last, allowing the user to close multiple tabs + // quickly. + delayTabResize = YES; + pendingCloseTabID = 0; + } + + // Ask the tabline to update all the tabs based on the tab IDs + static_assert(sizeof(NSInteger) == sizeof(intptr_t), + "Tab tag size mismatch between Vim and MacVim"); + [tabline updateTabsByTags:(NSInteger*)tabIDs + len:numTabs + delayTabResize:delayTabResize]; + + // 3. Read all the tab labels/tooltips and assign to each tab + NSInteger tabIdx = 0; + while (p < end && tabIdx < tabline.numberOfTabs) { + MMTab *tv = [tabline tabAtIndex:tabIdx]; + for (unsigned i = 0; i < MMTabInfoCount; ++i) { + size_t length = *((size_t*)p); p += sizeof(size_t); if (length <= 0) continue; - NSString *val = [[NSString alloc] initWithBytes:(void*)p length:length encoding:NSUTF8StringEncoding]; p += length; - - switch (i) { - case MMTabLabel: - // Set the label of the tab, adding a new tab when needed. - tv = tabline.numberOfTabs <= tabIdx - ? [self addNewTab] - : [tabline tabAtIndex:tabIdx]; - tv.title = val; - ++tabIdx; - break; - case MMTabToolTip: - if (tv) tv.toolTip = val; - break; - default: - ASLogWarn(@"Unknown tab info for index: %d", i); + if (i == MMTabLabel) { + tv.title = val; + } else if (i == MMTabToolTip) { + tv.toolTip = val; } - [val release]; } + tabIdx += 1; } - // Remove unused tabs from the tabline. - long i, count = tabline.numberOfTabs; - for (i = count-1; i >= tabIdx; --i) { - MMTab *tv = [tabline tabAtIndex:i]; - [tabline closeTab:tv force:YES layoutImmediately:YES]; - } - - [self selectTabWithIndex:curtabIdx]; - // It would be better if we could scroll to the selected tab only if it - // reflected user intent. Presumably, the user expects MacVim to scroll - // to the selected tab if they: added a tab, clicked a partially hidden - // tab, or navigated to a tab with a keyboard command. Since we don't - // have this kind of information, we always scroll to selected unless - // the window isn't key or we think the user is in the process of - // closing a tab by clicking its close button. Doing it this way instead - // of using a signal of explicit user intent is probably too aggressive. - if (self.window.isKeyWindow && !tabToClose && !didCloseTab) { - [tabline scrollTabToVisibleAtIndex:curtabIdx]; - } -} - -- (void)selectTabWithIndex:(int)idx -{ - if (idx < 0 || idx >= tabline.numberOfTabs) { - ASLogWarn(@"No tab with index %d exists.", idx); + // Finally, select the currently selected tab + if (curtabIdx < 0 || curtabIdx >= tabline.numberOfTabs) { + ASLogWarn(@"No tab with index %d exists.", curtabIdx); return; } - // Do not try to select a tab if already selected. - if (idx != tabline.selectedTabIndex) { - [tabline selectTabAtIndex:idx]; - // We might need to change the scrollbars that are visible. - self.pendingPlaceScrollbars = YES; + if (curtabIdx != tabline.selectedTabIndex) { + [tabline selectTabAtIndex:curtabIdx]; + [tabline scrollTabToVisibleAtIndex:curtabIdx]; } } -- (MMTab *)addNewTab -{ - // NOTE! A newly created tab is not by selected by default; Vim decides - // which tab should be selected at all times. However, the AppKit will - // automatically select the first tab added to a tab view. - NSUInteger index = [tabline addTabAtEnd]; - return [tabline tabAtIndex:index]; -} - - (void)createScrollbarWithIdentifier:(int32_t)ident type:(int)type { MMScroller *scroller = [[MMScroller alloc] initWithIdentifier:ident @@ -486,7 +450,7 @@ - (BOOL)tabline:(MMTabline *)tabline shouldSelectTabAtIndex:(NSUInteger)index { // Propagate the selection message to Vim. if (NSNotFound != index) { - int i = (int)index; // HACK! Never more than MAXINT tabs?! + int i = (int)index; NSData *data = [NSData dataWithBytes:&i length:sizeof(int)]; [vimController sendMessage:SelectTabMsgID data:data]; } @@ -497,14 +461,19 @@ - (BOOL)tabline:(MMTabline *)tabline shouldSelectTabAtIndex:(NSUInteger)index - (BOOL)tabline:(MMTabline *)tabline shouldCloseTabAtIndex:(NSUInteger)index { if (index >= 0 && index < tabline.numberOfTabs - 1) { - tabToClose = [tabline tabAtIndex:index]; + // If the user is closing any tab other than the last one, we remember + // the state so later on we don't resize the tabs in the layout + // animation to preserve the stability of tab positions to allow for + // quickly closing multiple tabs. This is similar to how macOS tabs + // work. + pendingCloseTabID = [tabline tabAtIndex:index].tag; } - // HACK! This method is only called when the user clicks the close button - // on the tab. Instead of letting the tab bar close the tab, we return NO - // and pass a message on to Vim to let it handle the closing. - int i = (int)index; // HACK! Never more than MAXINT tabs?! + // Propagate the close message to Vim + int i = (int)index; NSData *data = [NSData dataWithBytes:&i length:sizeof(int)]; [vimController sendMessage:CloseTabMsgID data:data]; + + // Let Vim decide whether to close the tab or not. return NO; } diff --git a/src/MacVim/MMWindowController.h b/src/MacVim/MMWindowController.h index 8d4605abe8..df932f0e98 100644 --- a/src/MacVim/MMWindowController.h +++ b/src/MacVim/MMWindowController.h @@ -65,7 +65,6 @@ - (BOOL)presentWindow:(id)unused; - (void)moveWindowAcrossScreens:(NSPoint)origin; - (void)updateTabsWithData:(NSData *)data; -- (void)selectTabWithIndex:(int)idx; - (void)setTextDimensionsWithRows:(int)rows columns:(int)cols isLive:(BOOL)live keepGUISize:(BOOL)keepGUISize keepOnScreen:(BOOL)onScreen; diff --git a/src/MacVim/MMWindowController.m b/src/MacVim/MMWindowController.m index e52896ee85..5ae435bf34 100644 --- a/src/MacVim/MMWindowController.m +++ b/src/MacVim/MMWindowController.m @@ -331,9 +331,6 @@ - (void)openWindow // TODO: Remove this method? Everything can probably be done in // presentWindow: but must carefully check dependencies on 'setupDone' // flag. - - [vimView addNewTab]; - setupDone = YES; } @@ -392,11 +389,6 @@ - (void)updateTabsWithData:(NSData *)data [vimView updateTabsWithData:data]; } -- (void)selectTabWithIndex:(int)idx -{ - [vimView selectTabWithIndex:idx]; -} - - (void)setTextDimensionsWithRows:(int)rows columns:(int)cols isLive:(BOOL)live keepGUISize:(BOOL)keepGUISize keepOnScreen:(BOOL)onScreen