From a7279d8cb66b5d759adb3ac8f706702a9443a1cf Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Sun, 11 Sep 2022 15:19:20 -0700 Subject: [PATCH] Allow pinning the cmdline to be aligned to the bottom of window Add a setting that could pin the command-line portion of Vim to the bottom of the MacVim window. This is useful when smooth resizing is set, guioption+=k, or in full screen. In those situations, the MacVim window size is usually not direct multiples of the Vim text sizes. Previously the command-line would be drawn like other texts, and hence not aligned to the bottom and hence looking aesthetically a little off. When this setting is set, the command-line portion would be aligned to the bottom of the window. This essentially moves the gap (due to the extra height of the window) from the bottom to be between cmdline and the rest of Vim. When cmdheight is changed, or other situations (e.g. typing too much cmdline height to be increased), the gap will be adjusted as well. Implementation-wise, this was done by passing the `commandline_row` var from Vim to MacVim, which serves as a good estimate of where the command-line is. This works better than just using the `cmdheight` option as it is closer to the current state of the cmdline. One issue is that in hit-enter prompts, the row is set to the 2nd to last row to anticipate more messages, and we just add a big hack by incrementing the row by 1 in hit-enter state so only the "Press Enter..." part is aligned to bottom. We also have to do something similar to when it's showing "--more--" for similar reasons. - An alternative would have been to modify Vim to provide us the information we want (the number of rows below the status line) but it's pretty tricky to do as cmdline_row is used in lots of places. It's easier / simpler to do a simple hack like this to localize the damage. Close #833 --- runtime/doc/gui_mac.txt | 5 +- runtime/doc/tags | 1 + src/MacVim/Base.lproj/Preferences.xib | 42 +++++++-- src/MacVim/MMAppController.h | 1 + src/MacVim/MMAppController.m | 12 +++ src/MacVim/MMBackend.m | 20 +++++ src/MacVim/MMCoreTextView.h | 1 + src/MacVim/MMCoreTextView.m | 121 ++++++++++++++++++++++---- src/MacVim/MMPreferenceController.m | 5 ++ src/MacVim/MMTextView.h | 3 +- src/MacVim/MMTextView.m | 5 ++ src/MacVim/Miscellaneous.h | 1 + src/MacVim/Miscellaneous.m | 2 +- 13 files changed, 193 insertions(+), 26 deletions(-) diff --git a/runtime/doc/gui_mac.txt b/runtime/doc/gui_mac.txt index 5404cd3267..a9e733e341 100644 --- a/runtime/doc/gui_mac.txt +++ b/runtime/doc/gui_mac.txt @@ -256,12 +256,14 @@ Here is a list of relevant dictionary entries: KEY VALUE ~ *MMCellWidthMultiplier* width of a normal glyph in em units [float] +*MMCmdLineAlignBottom* Pin command-line to bottom of MacVim [bool] *MMDialogsTrackPwd* open/save dialogs track the Vim pwd [bool] *MMDisableLaunchAnimation* disable launch animation when opening a new MacVim window [bool] -*MMFullScreenFadeTime* fade delay for non-native fullscreen [float] +*MMFontPreserveLineSpacing* use the line-spacing as specified by font [bool] *MMLoginShellArgument* login shell parameter [string] *MMLoginShellCommand* which shell to use to launch Vim [string] +*MMFullScreenFadeTime* fade delay for non-native fullscreen [float] *MMNativeFullScreen* use native full screen mode [bool] *MMNonNativeFullScreenShowMenu* show menus when in non-native full screen [bool] *MMNonNativeFullScreenSafeAreaBehavior* @@ -269,7 +271,6 @@ KEY VALUE ~ the safe area (aka the "notch") [int] *MMNoFontSubstitution* disable automatic font substitution [bool] (Deprecated: Non-CoreText renderer only) -*MMFontPreserveLineSpacing* use the line-spacing as specified by font [bool] *MMNoTitleBarWindow* hide title bar [bool] *MMTitlebarAppearsTransparent* enable a transparent titlebar [bool] *MMAppearanceModeSelection* dark mode selection (|macvim-dark-mode|)[bool] diff --git a/runtime/doc/tags b/runtime/doc/tags index 1e44be0000..24a187a2f1 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -5405,6 +5405,7 @@ M motion.txt /*M* MDI starting.txt /*MDI* MMAppearanceModeSelection gui_mac.txt /*MMAppearanceModeSelection* MMCellWidthMultiplier gui_mac.txt /*MMCellWidthMultiplier* +MMCmdLineAlignBottom gui_mac.txt /*MMCmdLineAlignBottom* MMDialogsTrackPwd gui_mac.txt /*MMDialogsTrackPwd* MMDisableLaunchAnimation gui_mac.txt /*MMDisableLaunchAnimation* MMFontPreserveLineSpacing gui_mac.txt /*MMFontPreserveLineSpacing* diff --git a/src/MacVim/Base.lproj/Preferences.xib b/src/MacVim/Base.lproj/Preferences.xib index 3fa5badc61..6ae857cb31 100644 --- a/src/MacVim/Base.lproj/Preferences.xib +++ b/src/MacVim/Base.lproj/Preferences.xib @@ -257,11 +257,11 @@ - + - + @@ -322,7 +322,7 @@ - + + + + + + + + + + + + - + @@ -504,7 +532,7 @@ - + diff --git a/src/MacVim/MMAppController.h b/src/MacVim/MMAppController.h index b776bdc20d..9786457080 100644 --- a/src/MacVim/MMAppController.h +++ b/src/MacVim/MMAppController.h @@ -61,6 +61,7 @@ - (void)refreshAllAppearances; - (void)refreshAllFonts; - (void)refreshAllResizeConstraints; +- (void)refreshAllTextViews; - (IBAction)newWindow:(id)sender; - (IBAction)newWindowAndActivate:(id)sender; diff --git a/src/MacVim/MMAppController.m b/src/MacVim/MMAppController.m index 95d89867c0..be27b5fee8 100644 --- a/src/MacVim/MMAppController.m +++ b/src/MacVim/MMAppController.m @@ -40,6 +40,7 @@ #import "MMAppController.h" #import "MMPreferenceController.h" #import "MMVimController.h" +#import "MMVimView.h" #import "MMWindowController.h" #import "MMTextView.h" #import "Miscellaneous.h" @@ -253,6 +254,7 @@ + (void)initialize [NSNumber numberWithInt:0], MMNonNativeFullScreenSafeAreaBehaviorKey, [NSNumber numberWithBool:YES], MMShareFindPboardKey, [NSNumber numberWithBool:NO], MMSmoothResizeKey, + [NSNumber numberWithBool:NO], MMCmdLineAlignBottomKey, nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:dict]; @@ -1142,6 +1144,16 @@ - (void)refreshAllResizeConstraints } } +- (void)refreshAllTextViews +{ + unsigned count = [vimControllers count]; + for (unsigned i = 0; i < count; ++i) { + MMVimController *vc = [vimControllers objectAtIndex:i]; + [vc.windowController.vimView.textView updateCmdlineRow]; + vc.windowController.vimView.textView.needsDisplay = YES; + } +} + - (IBAction)newWindow:(id)sender { ASLogDebug(@"Open new window"); diff --git a/src/MacVim/MMBackend.m b/src/MacVim/MMBackend.m index 71735cf2ae..3c3dcaddd6 100644 --- a/src/MacVim/MMBackend.m +++ b/src/MacVim/MMBackend.m @@ -1816,6 +1816,25 @@ - (void)insertVimStateMessage if (numTabs < 0) numTabs = 0; + // Custom hacks to deal with cmdline_row not being perfect for our use cases. + int cmdline_row_adjusted = cmdline_row; + if (State == MODE_HITRETURN) { + // When we are in hit-return mode, Vim does a weird thing and sets + // cmdline_row to be the 2nd-to-last row, which would make pinning + // cmdline to bottom look weird. This is done in msg_start() and + // wait_return(). + // Instead of modifying Vim, we just hack around this by manually + // increasing the row by one. This would make the pin happen right at + // the "Hit Enter..." prompt. + cmdline_row_adjusted++; + } else if (State == MODE_ASKMORE) { + // In "more" mode, Vim sometimes set cmdline_row, sometimes it doesn't. + // Silver lining is that it always only takes one row and doesn't wrap + // like hit-enter, so we know we can always just pin it to the last row + // and be done with the hack. + cmdline_row_adjusted = Rows - 1; + } + NSDictionary *vimState = [NSDictionary dictionaryWithObjectsAndKeys: [[NSFileManager defaultManager] currentDirectoryPath], @"pwd", [NSNumber numberWithInt:p_mh], @"p_mh", @@ -1823,6 +1842,7 @@ - (void)insertVimStateMessage [NSNumber numberWithInt:numTabs], @"numTabs", [NSNumber numberWithInt:fuoptions_flags], @"fullScreenOptions", [NSNumber numberWithLong:p_mouset], @"p_mouset", + [NSNumber numberWithInt:cmdline_row_adjusted], @"cmdline_row", // Used for pinning cmdline to bottom of window nil]; // Put the state before all other messages. diff --git a/src/MacVim/MMCoreTextView.h b/src/MacVim/MMCoreTextView.h index 717050713b..ba31aac84f 100644 --- a/src/MacVim/MMCoreTextView.h +++ b/src/MacVim/MMCoreTextView.h @@ -86,6 +86,7 @@ - (BOOL)convertPoint:(NSPoint)point toRow:(int *)row column:(int *)column; - (NSRect)rectForRow:(int)row column:(int)column numRows:(int)nr numColumns:(int)nc; +- (void)updateCmdlineRow; // // NSTextView methods diff --git a/src/MacVim/MMCoreTextView.m b/src/MacVim/MMCoreTextView.m index 441044415c..47e3373b66 100644 --- a/src/MacVim/MMCoreTextView.m +++ b/src/MacVim/MMCoreTextView.m @@ -76,13 +76,12 @@ - (MMVimController *)vimController; - (NSFont *)fontVariantForTextFlags:(int)textFlags; - (CTLineRef)lineForCharacterString:(NSString *)string textFlags:(int)flags; +- (void)setCmdlineRow:(int)row; @end @interface MMCoreTextView (Drawing) - (NSPoint)pointForRow:(int)row column:(int)column; -- (NSRect)rectFromRow:(int)row1 column:(int)col1 - toRow:(int)row2 column:(int)col2; - (NSSize)textAreaSize; - (void)batchDrawData:(NSData *)data; - (void)setString:(NSString *)string @@ -215,6 +214,9 @@ static void grid_free(Grid *grid) { @implementation MMCoreTextView { Grid grid; + + BOOL alignCmdLineToBottom; ///< Whether to pin the Vim command-line to the bottom of the window + int cmdlineRow; ///< Row number (0-indexed) where the cmdline starts. Used for pinning it to the bottom if desired. } - (id)initWithFrame:(NSRect)frame @@ -242,6 +244,10 @@ - (id)initWithFrame:(NSRect)frame NSFilenamesPboardType, NSStringPboardType, nil]]; ligatures = NO; + + alignCmdLineToBottom = NO; // this would be updated to the user preferences later + cmdlineRow = -1; // this would be updated by Vim + return self; } @@ -328,6 +334,8 @@ - (NSRect)rectForRowsInRange:(NSRange)range // include the top inset as well. (This method is only used to place the // scrollbars inside MMVimView.) + // Note: This doesn't really take alignCmdLineToBottom into account right now. + NSRect rect = { {0, 0}, {0, 0} }; unsigned start = range.location > maxRows ? maxRows : range.location; unsigned length = range.length; @@ -545,6 +553,56 @@ - (void)setThinStrokes:(BOOL)state thinStrokes = state; } +- (void)updateCmdlineRow +{ + [self setCmdlineRow: [[[self vimController] objectForVimStateKey:@"cmdline_row"] intValue]]; +} + +/// Set Vim's cmdline row number. This will mark the relevant parts to be repainted +/// if the row number has changed as we are pinning the cmdline to the bottom, +/// because otherwise we will have a gap that doesn't get cleared and leaves artifacts. +/// +/// @param row The row (0-indexed) of the current cmdline in Vim. +- (void)setCmdlineRow:(int)row +{ + const BOOL newAlignCmdLineToBottom = [[NSUserDefaults standardUserDefaults] boolForKey:MMCmdLineAlignBottomKey]; + + if (newAlignCmdLineToBottom != alignCmdLineToBottom) { + // The user settings has changed (usually through the settings panel). Just update everything. + alignCmdLineToBottom = newAlignCmdLineToBottom; + cmdlineRow = row; + [self setNeedsDisplay:YES]; + return; + } + + if (row != cmdlineRow) { + // The cmdline row has changed. Need to redraw the necessary parts if we + // are configured to pin cmdline to the bottom. + if (alignCmdLineToBottom) { + // Since we are changing the cmdline row, we need to repaint the + // parts where the gap changed. Just for simplicity, we repaint + // both the old/new cmdline rows and the row above them. This way + // the gap in between the top and bottom aligned rows should be + // touched in the repainting and cleared to bg. + [self setNeedsDisplayFromRow:cmdlineRow-1 + column:grid.cols + toRow:cmdlineRow + column:grid.cols]; + + // Have to do this between the two calls as cmdlineRow would affect + // the calculation in them. + cmdlineRow = row; + + [self setNeedsDisplayFromRow:cmdlineRow-1 + column:grid.cols + toRow:cmdlineRow + column:grid.cols]; + } else { + cmdlineRow = row; + } + } +} + - (void)setImControl:(BOOL)enable { [helper setImControl:enable]; @@ -767,8 +825,8 @@ - (void)drawRect:(NSRect)rect CGContextFillRect(ctx, rect); for (size_t r = 0; r < grid.rows; r++) { - CGRect rowRect = [self rectForRow:r column:0 numRows:1 numColumns:grid.cols]; - CGRect rowClipRect = CGRectIntersection(rowRect, rect); + const CGRect rowRect = [self rectForRow:r column:0 numRows:1 numColumns:grid.cols]; + const CGRect rowClipRect = CGRectIntersection(rowRect, rect); if (CGRectIsNull(rowClipRect)) continue; CGContextSaveGState(ctx); @@ -1057,8 +1115,12 @@ - (IBAction)selectAll:(id)sender [[self windowController] vimMenuItemAction:sender]; } +/// Converts a point in this NSView to a specific Vim row/column. +/// +/// @param point The point in NSView. Note that it's y-up as that's Mac convention, whereas row starts from the top. - (BOOL)convertPoint:(NSPoint)point toRow:(int *)row column:(int *)column { + // Convert y-up to y-down. point.y = [self bounds].size.height - point.y; NSPoint origin = { insetSize.width, insetSize.height }; @@ -1066,6 +1128,22 @@ - (BOOL)convertPoint:(NSPoint)point toRow:(int *)row column:(int *)column if (!(cellSize.width > 0 && cellSize.height > 0)) return NO; + if (alignCmdLineToBottom) { + // Account for the gap we added to pin cmdline to the bottom of the window + const NSRect frame = [self bounds]; + const int insetBottom = [[NSUserDefaults standardUserDefaults] integerForKey:MMTextInsetBottomKey]; + const CGFloat gapHeight = frame.size.height - grid.rows*cellSize.height - insetSize.height - insetBottom; + const CGFloat cmdlineRowY = insetSize.height + cmdlineRow*cellSize.height + 1; + if (point.y > cmdlineRowY) { + point.y -= gapHeight; + if (point.y <= cmdlineRowY) { + // This was inside the gap between top and bottom lines. Round it down + // to the next line. + point.y = cmdlineRowY + 1; + } + } + } + if (row) *row = floor((point.y-origin.y-1) / cellSize.height); if (column) *column = floor((point.x-origin.x-1) / cellSize.width); @@ -1075,6 +1153,11 @@ - (BOOL)convertPoint:(NSPoint)point toRow:(int *)row column:(int *)column return YES; } +/// Calculates the rect for the row/column range, accounting for insets. This also +/// has additional for accounting for aligning cmdline to bottom, and filling last +/// column to the right. +/// +/// @return Rectangle containing the row/column range. - (NSRect)rectForRow:(int)row column:(int)col numRows:(int)nr numColumns:(int)nc { @@ -1103,6 +1186,22 @@ - (NSRect)rectForRow:(int)row column:(int)col numRows:(int)nr rect.size.width += extraWidth; } + // When configured to align cmdline to bottom, need to adjust the rect with an additional gap to pin + // the rect to the bottom. + if (alignCmdLineToBottom) { + const int insetBottom = [[NSUserDefaults standardUserDefaults] integerForKey:MMTextInsetBottomKey]; + const CGFloat gapHeight = frame.size.height - grid.rows*cellSize.height - insetSize.height - insetBottom; + if (row >= cmdlineRow) { + rect.origin.y -= gapHeight; + } else if (row + nr - 1 >= cmdlineRow) { + // This is an odd case where the gap between cmdline and the top-aligned content is inside + // the rect so we need to adjust the height as well. During rendering we draw line-by-line + // so this shouldn't cause any issues as we only encounter this situation when calculating + // the rect in setNeedsDisplayFromRow:. + rect.size.height += gapHeight; + rect.origin.y -= gapHeight; + } + } return rect; } @@ -1210,17 +1309,6 @@ - (NSPoint)pointForRow:(int)row column:(int)col frame.size.height - (row+1)*cellSize.height - insetSize.height); } -- (NSRect)rectFromRow:(int)row1 column:(int)col1 - toRow:(int)row2 column:(int)col2 -{ - NSRect frame = [self bounds]; - return NSMakeRect( - insetSize.width + col1*cellSize.width, - frame.size.height - insetSize.height - (row2+1)*cellSize.height, - (col2 + 1 - col1) * cellSize.width, - (row2 + 1 - row1) * cellSize.height); -} - - (NSSize)textAreaSize { // Calculate the (desired) size of the text area, i.e. the text view area @@ -1448,6 +1536,9 @@ - (void)batchDrawData:(NSData *)data #endif // TODO: Sanity check input + // Update the cmdline rows to decide if we need to update based on whether we are pinning cmdline to bottom or not. + [self updateCmdlineRow]; + while (bytes < end) { struct DrawCmd drawCmd; int type = ReadDrawCmd(&bytes, &drawCmd); diff --git a/src/MacVim/MMPreferenceController.m b/src/MacVim/MMPreferenceController.m index eb1ceb90a3..c28c2c5554 100644 --- a/src/MacVim/MMPreferenceController.m +++ b/src/MacVim/MMPreferenceController.m @@ -168,4 +168,9 @@ - (IBAction)smoothResizeChanged:(id)sender [[MMAppController sharedInstance] refreshAllResizeConstraints]; } +- (IBAction)cmdlineAlignBottomChanged:(id)sender +{ + [[MMAppController sharedInstance] refreshAllTextViews]; +} + @end diff --git a/src/MacVim/MMTextView.h b/src/MacVim/MMTextView.h index c1387e69ea..c6430d2542 100644 --- a/src/MacVim/MMTextView.h +++ b/src/MacVim/MMTextView.h @@ -39,6 +39,8 @@ - (void)setImControl:(BOOL)enable; - (void)activateIm:(BOOL)enable; - (void)checkImState; +- (void)refreshFonts; +- (void)updateCmdlineRow; // // MMTextStorage methods @@ -47,7 +49,6 @@ - (void)setFont:(NSFont *)newFont; - (NSFont *)fontWide; - (void)setWideFont:(NSFont *)newFont; -- (void)refreshFonts; - (NSSize)cellSize; - (void)setLinespace:(float)newLinespace; - (void)setColumnspace:(float)newColumnspace; diff --git a/src/MacVim/MMTextView.m b/src/MacVim/MMTextView.m index d41ceac9c6..e9933f5f47 100644 --- a/src/MacVim/MMTextView.m +++ b/src/MacVim/MMTextView.m @@ -357,6 +357,11 @@ - (void)refreshFonts // Doesn't do anything. CoreText renderer only. } +- (void)updateCmdlineRow +{ + // Doesn't do anything. CoreText renderer only. +} + - (NSSize)cellSize { return [(MMTextStorage*)[self textStorage] cellSize]; diff --git a/src/MacVim/Miscellaneous.h b/src/MacVim/Miscellaneous.h index 635e46bebd..0a76dd141f 100644 --- a/src/MacVim/Miscellaneous.h +++ b/src/MacVim/Miscellaneous.h @@ -59,6 +59,7 @@ extern NSString *MMFullScreenFadeTimeKey; extern NSString *MMNonNativeFullScreenShowMenuKey; extern NSString *MMNonNativeFullScreenSafeAreaBehaviorKey; extern NSString *MMSmoothResizeKey; +extern NSString *MMCmdLineAlignBottomKey; // Enum for MMUntitledWindowKey diff --git a/src/MacVim/Miscellaneous.m b/src/MacVim/Miscellaneous.m index b2f52edede..ecbd37bc21 100644 --- a/src/MacVim/Miscellaneous.m +++ b/src/MacVim/Miscellaneous.m @@ -55,7 +55,7 @@ NSString *MMNonNativeFullScreenShowMenuKey = @"MMNonNativeFullScreenShowMenu"; NSString *MMNonNativeFullScreenSafeAreaBehaviorKey = @"MMNonNativeFullScreenSafeAreaBehavior"; NSString *MMSmoothResizeKey = @"MMSmoothResize"; - +NSString *MMCmdLineAlignBottomKey = @"MMCmdLineAlignBottom"; @implementation NSIndexSet (MMExtras)