diff --git a/runtime/doc/gui_mac.txt b/runtime/doc/gui_mac.txt index e0b519d4b6..65fc6551ae 100644 --- a/runtime/doc/gui_mac.txt +++ b/runtime/doc/gui_mac.txt @@ -259,6 +259,8 @@ as general information regarding macOS user defaults. Here is a list of relevant dictionary entries: KEY VALUE ~ +*MMAllowForceClickLookUp* use Force click for data lookup instead of + [bool] *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] @@ -783,11 +785,17 @@ Each gesture generates one of the following Vim pseudo keys: ** ** Generated when swiping three fingers across the trackpad in a - vertical direction. (Not supported by the Apple Magic Mouse.) + vertical direction. (Not supported by the Apple Magic Mouse) ** Generated when doing a Force click by pressing hard on a trackpad. - (Only supported on trackpads that support Force Touch.) + (Only supported on trackpads that support Force Touch) + + If you have configured to use Force click for "Look up & data + detectors" in the system settings, by default MacVim will do a + dictionary lookup instead of triggering this mapping. You can turn + this off in MacVim's Preference pane, or directly set + |MMAllowForceClickLookUp|. You can map these keys like with any other key using the |:map| family of commands. For example, the following commands map left/right swipe to change diff --git a/runtime/doc/tags b/runtime/doc/tags index 0bbaac575f..57e18d71b6 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -5417,6 +5417,7 @@ LogiPat-flags pi_logipat.txt /*LogiPat-flags* Lua if_lua.txt /*Lua* M motion.txt /*M* MDI starting.txt /*MDI* +MMAllowForceClickLookUp gui_mac.txt /*MMAllowForceClickLookUp* MMAppearanceModeSelection gui_mac.txt /*MMAppearanceModeSelection* MMCellWidthMultiplier gui_mac.txt /*MMCellWidthMultiplier* MMCmdLineAlignBottom gui_mac.txt /*MMCmdLineAlignBottom* diff --git a/src/MacVim/Base.lproj/Preferences.xib b/src/MacVim/Base.lproj/Preferences.xib index 598673cbcc..f5e5c847fa 100644 --- a/src/MacVim/Base.lproj/Preferences.xib +++ b/src/MacVim/Base.lproj/Preferences.xib @@ -9,9 +9,11 @@ + + @@ -475,6 +477,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MacVim/MMAppController.m b/src/MacVim/MMAppController.m index eda69bf161..776e54c12a 100644 --- a/src/MacVim/MMAppController.m +++ b/src/MacVim/MMAppController.m @@ -256,6 +256,7 @@ + (void)initialize [NSNumber numberWithBool:YES], MMShareFindPboardKey, [NSNumber numberWithBool:NO], MMSmoothResizeKey, [NSNumber numberWithBool:NO], MMCmdLineAlignBottomKey, + [NSNumber numberWithBool:YES], MMAllowForceClickLookUpKey, nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:dict]; diff --git a/src/MacVim/MMCoreTextView.h b/src/MacVim/MMCoreTextView.h index 4959e04ce9..5f1aab2367 100644 --- a/src/MacVim/MMCoreTextView.h +++ b/src/MacVim/MMCoreTextView.h @@ -13,8 +13,31 @@ @class MMTextViewHelper; +/// The main text view that manages drawing Vim's content using Core Text, and +/// handles input. We are using this instead of NSTextView because of the +/// custom needs in order to draw Vim's texts, as we don't have access to the +/// full contents of Vim, and works more like a smart terminal to Vim. +/// +/// Currently the rendering is done in software via Core Text, but a future +/// extension will add support for Metal rendering which probably will require +/// splitting this class up. +/// +/// Since this class implements text rendering/input using a custom view, it +/// implements NSTextInputClient, mostly for the following needs: +/// 1. Text input. This is done via insertText / doCommandBySelector. +/// 2. Input methods (e.g. for CJK). This is done via the marked text and the +/// other APIs like selectedRange/firstRectForCharacterRange/etc. +/// 3. Support native dictionary lookup (quickLookWithEvent:) when the user +/// wants to. This mostly involves implementing the attributeSubstring / +/// firstRectForCharacterRange / characterIndexForPoint APIs. +/// There is an inherent difficulty to implementing NSTextInputClient +/// 'correctly', because it assumes we have an entire text storage with +/// indexable ranges. However, we don't have full access to Vim's internal +/// storage, and we are represening the screen view instead in row-major +/// indexing, but this becomes complicated when we want to implement marked +/// texts. We the relevant parts for comments on how we hack around this. @interface MMCoreTextView : NSView < - NSTextInput + NSTextInputClient #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14 , NSFontChanging , NSMenuItemValidation @@ -122,8 +145,21 @@ // NSTextView methods // - (void)keyDown:(NSEvent *)event; -- (void)insertText:(id)string; + +// +// NSTextInputClient methods +// +- (void)insertText:(id)string replacementRange:(NSRange)replacementRange; - (void)doCommandBySelector:(SEL)selector; +- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange; +- (void)unmarkText; +- (NSRange)selectedRange; +- (NSRange)markedRange; +- (BOOL)hasMarkedText; +- (nullable NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange; +- (nonnull NSArray *)validAttributesForMarkedText; +- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange; +- (NSUInteger)characterIndexForPoint:(NSPoint)point; // // NSTextContainer methods diff --git a/src/MacVim/MMCoreTextView.m b/src/MacVim/MMCoreTextView.m index 018cfc965b..21475608f2 100644 --- a/src/MacVim/MMCoreTextView.m +++ b/src/MacVim/MMCoreTextView.m @@ -596,8 +596,9 @@ - (void)keyDown:(NSEvent *)event [helper keyDown:event]; } -- (void)insertText:(id)string +- (void)insertText:(id)string replacementRange:(NSRange)replacementRange { + // We are not currently replacementRange right now. [helper insertText:string]; } @@ -606,36 +607,6 @@ - (void)doCommandBySelector:(SEL)selector [helper doCommandBySelector:selector]; } -- (BOOL)hasMarkedText -{ - return [helper hasMarkedText]; -} - -- (NSRange)markedRange -{ - return [helper markedRange]; -} - -- (NSDictionary *)markedTextAttributes -{ - return [helper markedTextAttributes]; -} - -- (void)setMarkedTextAttributes:(NSDictionary *)attr -{ - [helper setMarkedTextAttributes:attr]; -} - -- (void)setMarkedText:(id)text selectedRange:(NSRange)range -{ - [helper setMarkedText:text selectedRange:range]; -} - -- (void)unmarkText -{ - [helper unmarkText]; -} - - (void)scrollWheel:(NSEvent *)event { [helper scrollWheel:event]; @@ -1283,39 +1254,422 @@ - (NSRect)rectForRow:(int)row column:(int)col numRows:(int)nr return rect; } -- (NSArray *)validAttributesForMarkedText +#pragma mark Text Input Client +#pragma region Text Input Client + +// +// Text input client implementation. +// +// Note that we are implementing this as a row-major indexed grid of the +// current display. This is not the same as Vim's internal knowledge of the +// buffers. We don't really have access to that easily because MacVim is purely +// a GUI into Vim through a multi-process model. It's theoretically possible to +// get access to it, but it increases latency and complexity, and we won't be +// able to get access to the message output. +// +// Because of this quirk, proper marked text implementation is quite difficult. +// The OS assumes a proper text strage backing, and that marked texts are a +// contiguous region in that storage (see how markedRange API returns a single +// NSRange). This is not possible for us if Vim has the marked texts wrapped +// into multiple lines while we have split windows, or just that a long marked +// text could be hidden. Because of that, we fake it by testing for +// hasMarkedText: If we have marked texts, we always tell the OS we are +// starting from 0, and the selectedRange/markedRange/etc all treat the text +// storage as having the marked text starting from 0, and +// firstRectForCharacterRange just handles that specially to make sure we still +// draw the input method's candidate list properly. Otherwise, we just treat +// the text storage as a row-major grid of the currently displayed text, which +// works fine for dictionary lookups. +// +// Also, note that whenever the OS API uses a character index or range, it +// always refers to the unicode length, so the calculation between row/col and +// character index/range needs to go through each character and calculate its +// length. We could optimize it to cache each row's total char length if we +// want if this is an issue. +// + +/// Takes a point and convert it into a single index into the entire window's +/// text. The text is converted into a row-major format, and the lines are +/// concatenated together without injecting any spaces or newlines. Note that +/// this doesn't take into account of Vim's own window splits and whatnot for +/// now so a wrapped text in Vim would not be returned as contiguous. +/// +/// The concatenation is done without injecting newlines for simplicity and to +/// allow wrapped lines to come together but that could be changed if it's +/// undesired. +static NSUInteger utfCharIndexFromRowCol(const Grid* grid, int row, int col) +{ + // Find the raw index for the character. Note that this is not good enough. With localized / wide texts, + // some character will be single-width but have length > 1, and some character will be double-width. We + // don't pre-calculate these information (since this is needed infrequently), and so we have to search + // from first character onwards and accumulating the lengths. + // See attributedSubstringForProposedRange which also does the same thing. + const int rawIndex = row * grid->cols + col; + const int gridSize = grid->cols * grid->rows; + + NSUInteger utfIndex = 0; + for (int i = 0; i < gridSize && i < rawIndex; i++) { + NSString *str = grid->cells[i].string; + utfIndex += str == nil ? 1 : str.length; // Note: nil string means empty space. + + if (grid->cells[i].textFlags & DRAW_WIDE) { + i += 1; + } + } + return utfIndex; +} + +/// Given grid position, and a UTF-8 character offset, return the new column on +/// the same line. This doesn't support multi-line for now as there is no need +/// to. +/// +/// @param utfIndexOffset The character offset from the row/col provided. Can +/// be positive or negative. +/// +/// @return The column at the specified offset. Note that this clamps at +/// [0,cols-1] since we are only looking for the same line. +static int colFromUtfOffset(const Grid* grid, int row, int col, NSInteger utfIndexOffset) { - return nil; + if (row < 0 || col < 0 || row >= grid->rows || col >= grid->cols) { + // Should not happen + return 0; + } + if (utfIndexOffset == 0) + return col; + + const int advance = utfIndexOffset > 0 ? 1 : -1; + NSUInteger accUtfIndexOffset = 0; + + int c; + for (c = col; c > 0 && c < grid->cols - 1 && accUtfIndexOffset < labs(utfIndexOffset); c += advance) { + int rawIndex = row * grid->cols + c; + + if (advance < 0) { + // If going backwards, we have to use the last character's length + // instead, including walking back 2 chars if it happens to be a + // wide char. + rawIndex -= 1; + if (c - 2 >= 0 && grid->cells[rawIndex - 1].textFlags & DRAW_WIDE) { + c += advance; + rawIndex -= 1; + } + } + + NSString *str = grid->cells[rawIndex].string; + accUtfIndexOffset += str == nil ? 1 : str.length; // Note: nil string means empty space. + + if (advance > 0) { + if (grid->cells[rawIndex].textFlags & DRAW_WIDE) { + c += advance; + } + } + } + + // Make sure nothing out of bounds happened due to some issue with wide-character skipping. + if (c < 0) + c = 0; + if (c >= grid->cols) + c = grid->cols - 1; + + return c; +} + +/// Given a range of UTF-8 character indices, find the row/col of the beginning +/// of the range, and the end of the range *on the same line*. This doesn't +/// support searching for the end past the first line because there's no need +/// to right now. Sort of the reverse of utfCharIndexFromRowCol. +/// +/// This assumes the text representation is a row-major representation of the +/// whole grid, with no newline/spaces to separate the lines. +/// +/// @param row Return the starting character's row. +/// @param col Return the starting character's column. +/// @param firstLineNumCols Return the number of columns to the end character's +/// on the same line. If the end char is on the next line, then this +/// will just find the last column of the line. +/// @param firstLineUtf8Len Return the length of the characters on the first +/// line, in UTF-8 length. +static void rowColFromUtfRange(const Grid* grid, NSRange range, + int *row, int *col, + int *firstLineNumCols, int *firstLineUtf8Len) +{ + int startUtfIndex = -1; + int outRow = -1; + int outCol = -1; + int outFirstLineNumCols = -1; + int outFirstLineLen = -1; + + const int gridSize = grid->cols * grid->rows; + NSUInteger utfIndex = 0; + for (int i = 0; i < gridSize; i++) { + if (utfIndex >= range.location) { + // We are now past the start of the character. + const int curRow = i / grid->cols; + const int curCol = i % grid->cols; + + if (outRow == -1) { + // Record the beginning + startUtfIndex = utfIndex; + outRow = curRow; + outCol = curCol; + } + + if (utfIndex >= range.location + range.length) { + // Record the end if we found it. + if (outFirstLineNumCols == -1) { + outFirstLineLen = utfIndex - startUtfIndex; + outFirstLineNumCols = curCol - outCol; + } + break; + } + + if (curRow > outRow) { + // We didn't find the end, but we are already at next line, so + // just clamp it to the last column from the last line. + outFirstLineLen = utfIndex - startUtfIndex; + outFirstLineNumCols = grid->cols - outCol; + break; + } + + } + + NSString *str = grid->cells[i].string; + utfIndex += str == nil ? 1 : str.length; // Note: nil string means empty space. + + if (grid->cells[i].textFlags & DRAW_WIDE) { + i += 1; + } + } + + if (outRow == -1) + { + *row = 0; + *col = 1; + *firstLineNumCols = 0; + *firstLineUtf8Len = 0; + return; + } + if (outFirstLineNumCols == -1) + { + outFirstLineLen = utfIndex - startUtfIndex; + outFirstLineNumCols = grid->cols; + } + *row = outRow; + *col = outCol; + *firstLineNumCols = outFirstLineNumCols; + *firstLineUtf8Len = outFirstLineLen; } -- (NSAttributedString *)attributedSubstringFromRange:(NSRange)range +- (nonnull NSArray *)validAttributesForMarkedText { - return nil; + // Not implementing this for now. Properly implementing this would allow things like bolded underline + // for certain texts in the marked range, etc, but we would need SetMarkedTextMsgID to support it. + return @[]; } -- (NSUInteger)characterIndexForPoint:(NSPoint)point +/// Returns an attributed string containing the proposed range. This method is +/// usually called for two reasons: +/// 1. Input methods. It's unclear why the OS calls this during marked text +/// operation and returning nil doesn't seem to have any negative effect. +/// However, for operations like Hangul->Hanja (by pressing Option-Return), +/// it does rely on this after inserting the original Hangul text. +/// 2. Dictionary lookup. This is used for retrieving the formatted text that +/// the OS uses to look up and to show within the yellow box. +- (nullable NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange; { - return NSNotFound; + // Because of Unicode / wide characters, we have to unfortunately loop through the entire text to + // find the range. We could add better accelerated data structure here if for some reason this is + // slow (it should only be called when inputting special / localized characters or when doing + // quickLook lookup (e.g. Cmd-Ctrl-D). This step is important though or emojis and say Chinese + // characters would not behave properly. See characterIndexForPoint which also does the same thing. + + if (range.length == 0) { + return nil; + } + + if ([helper hasMarkedText]) { + // Since marked text changes the meaning of the text storage ranges (see above overall design), + // just don't return anything for now. We could simply return the marked text if we want to and + // have a need to do so. + return nil; + } + + NSMutableString *retStr = nil; + NSUInteger utfIndex = 0; + + const int gridSize = grid.cols * grid.rows; + for (int i = 0; i < gridSize; i++) { + NSString *str = grid.cells[i].string; + if (str == nil) { + str = @" "; + } + + if (utfIndex >= range.location) { + if (retStr == nil) { + // Lazily initialize the return string in case the passed in range is just completely + // out of bounds. + retStr = [NSMutableString stringWithCapacity:range.length];; + } + [retStr appendString:str]; + } + if (retStr.length >= range.length) { + break; + } + + // Increment counters + utfIndex += str.length; + if (grid.cells[i].textFlags & DRAW_WIDE) { + i += 1; + } + } + + if (retStr == nil) { + return nil; + } + if (actualRange != NULL) { + actualRange->length = retStr.length; + } + // Return an attributed string with the correct font so it will long right. + // Note that this won't get us a perfect replica of the displayed texts, + // but good enough. Some reasons why it's not perfect: + // - Asian characters don't get displayed in double-width under OS + // rendering and will be narrower. + // - We aren't passing through bold/italics/underline/strike-through/etc + // for now. This is probably ok. If we want to tackle this maybe just + // bold/underline is enough. Even NSTextView doesn't pass the + // underline/etc styles over, presumably because they make reading it + // hard. + // - Font substitutions aren't handled the same way. + return [[[NSAttributedString alloc] initWithString:retStr + attributes:@{NSFontAttributeName: font} + ] autorelease]; } -- (NSInteger)conversationIdentifier +- (BOOL)hasMarkedText { - return (NSInteger)self; + return [helper hasMarkedText]; } -- (NSRange)selectedRange +- (NSRange)markedRange { - return [helper imRange]; + // This will return the range marked from 0 to size of marked text. See the + // overall text input client implementation above for more description of + // the design choice of handling marked text in this API. + return [helper markedRange]; } -- (NSRect)firstRectForCharacterRange:(NSRange)range +- (NSDictionary *)markedTextAttributes { - return [helper firstRectForCharacterRange:range]; + return [helper markedTextAttributes]; } -@end // MMCoreTextView +- (void)setMarkedTextAttributes:(NSDictionary *)attr +{ + [helper setMarkedTextAttributes:attr]; +} + +- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange; +{ + // We are not using replacementRange right now + [helper setMarkedText:string selectedRange:selectedRange]; +} + +- (void)unmarkText +{ + [helper unmarkText]; +} + +/// Returns a character index to the overall text storage. +/// +/// This is used mostly for quickLookWithEvent: calls for the OS to be able to +/// understand the textual content of this text input client. +- (NSUInteger)characterIndexForPoint:(NSPoint)point +{ + // Not using convertPointFromScreen because it's 10.12+ only. + NSRect screenRect = {point, NSZeroSize}; + NSPoint windowPt = [[self window] convertRectFromScreen:screenRect].origin; + NSPoint viewPt = [self convertPoint:windowPt fromView:nil]; + int row, col; + if (![self convertPoint:viewPt toRow:&row column:&col]) { + return NSNotFound; + } + + return utfCharIndexFromRowCol(&grid, row, col); +} +- (NSRange)selectedRange +{ + if ([helper hasMarkedText]) { + // This returns the current cursor position relative to the marked + // range, starting from 0. See above overall comments on text input + // client implementation for marked text API decision. + return [helper imRange]; + } + + // Find the character index. + int row = [helper preEditRow]; + int col = [helper preEditColumn]; + NSUInteger charIndex = utfCharIndexFromRowCol(&grid, row, col); + + // We don't support selected texts for now, so always return length = 0; + NSRange result = {charIndex, 0}; + return result; +} +/// Return the first line's rectangle for a range of characters. This is +/// usually called either during marked text operation to decide where to show +/// a candidate list, or when doing dictionary lookup and the UI wants to draw +/// a box right on top of this text seamlessly. +/// +/// @param range The range to show rect for. Note that during marked text +/// operation, this could be different from imRange. For example, when using +/// Japanese input to input a long line of text, the user could use +/// left/right arrow keys to jump to different section of the +/// in-progress phrase and pick a new candidate. When doing that, this +/// will get called with different range's in order to show the +/// candidate list box right below the current section under +/// consideration. +/// @param actualRange The actual range this rect represents. Only used for +/// non-marked text situations for now. +- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange +{ + if ([helper hasMarkedText]) { + // Marked texts have special handling (see above overall comments for + // marked text API design). + + // Because we just expose the range as 0 to marked length, the range + // here doesn't represent the final screen position. Instead, we use + // the current cursor position as basis. We know that during marked + // text operations it has to be inside the marked range as specified by + // setMarkedText's range. + const int cursorRow = [helper preEditRow]; + const int cursorCol = [helper preEditColumn]; + + // Now, we retrieve the IM range that setMarkedText gave us, and + // compare with what the OS wants now (range). Find the rectangle + // surrounding that. + const NSRange imRange = [helper imRange]; + const NSInteger startIndexOffset = range.location - imRange.location; + const NSInteger endIndexOffset = range.location + range.length - imRange.location; + + const int rectBeginCol = colFromUtfOffset(&grid, cursorRow, cursorCol, startIndexOffset); + const int rectEndCol = colFromUtfOffset(&grid, cursorRow, cursorCol, endIndexOffset); + + return [helper firstRectForCharacterRange:cursorRow column:rectBeginCol length:(rectEndCol - rectBeginCol)]; + } else { + int row = 0, col = 0, firstLineNumCols = 0, firstLineUtf8Len = 0; + rowColFromUtfRange(&grid, range, &row, &col, &firstLineNumCols, &firstLineUtf8Len); + if (actualRange != NULL) { + actualRange->location = range.location; + actualRange->length = firstLineUtf8Len; + } + return [helper firstRectForCharacterRange:row column:col length:firstLineNumCols]; + } +} + +#pragma endregion // Text Input Client + +@end // MMCoreTextView @implementation MMCoreTextView (Private) diff --git a/src/MacVim/MMPreferenceController.h b/src/MacVim/MMPreferenceController.h index 4eed8df52a..9edaf9631b 100644 --- a/src/MacVim/MMPreferenceController.h +++ b/src/MacVim/MMPreferenceController.h @@ -14,12 +14,16 @@ @interface MMPreferenceController : DBPrefsWindowController { IBOutlet NSView *generalPreferences; IBOutlet NSView *appearancePreferences; + IBOutlet NSView *inputPreferences; IBOutlet NSView *advancedPreferences; // General pane IBOutlet NSPopUpButton *layoutPopUpButton; IBOutlet NSButton *autoInstallUpdateButton; IBOutlet NSView *sparkleUpdaterPane; + + // Input pane + IBOutlet NSButton *allowForceClickLookUpButton; } // General pane diff --git a/src/MacVim/MMPreferenceController.m b/src/MacVim/MMPreferenceController.m index 2628ffc72f..42dec49653 100644 --- a/src/MacVim/MMPreferenceController.m +++ b/src/MacVim/MMPreferenceController.m @@ -43,6 +43,17 @@ - (IBAction)showWindow:(id)sender { [super setCrossFade:NO]; [super showWindow:sender]; + + // Refresh enabled states for settings that may or may not make sense + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + if (allowForceClickLookUpButton != nil) { + // Only enable force click lookup setting if only the user has configured so to begin with. + // Otherwise it doesn't make sense at all. + // Note: This cannot be done in simple bindings, because NSUserDefaults don't really support + // global domain bindings from what I can tell, we have to manually read it. + const BOOL useForceClickLookup = [ud boolForKey:@"com.apple.trackpad.forceClick"]; + [allowForceClickLookUpButton setEnabled:useForceClickLookup]; + } } - (void)setupToolbar @@ -58,6 +69,10 @@ - (void)setupToolbar label:@"Appearance" image:[NSImage imageWithSystemSymbolName:@"paintbrush" accessibilityDescription:nil]]; + [self addView:inputPreferences + label:@"Input" + image:[NSImage imageWithSystemSymbolName:@"keyboard" accessibilityDescription:nil]]; + [self addView:advancedPreferences label:@"Advanced" image:[NSImage imageWithSystemSymbolName:@"gearshape.2" accessibilityDescription:nil]]; @@ -73,6 +88,10 @@ - (void)setupToolbar label:@"Appearance" image:[NSImage imageNamed:NSImageNameColorPanel]]; + [self addView:inputPreferences + label:@"Input" + image:[NSImage imageNamed:NSImageNamePreferencesGeneral]]; // not a good choice but works for now + [self addView:advancedPreferences label:@"Advanced" image:[NSImage imageNamed:NSImageNameAdvanced]]; diff --git a/src/MacVim/MMTextViewHelper.h b/src/MacVim/MMTextViewHelper.h index f519ab8f4f..72518b40e7 100644 --- a/src/MacVim/MMTextViewHelper.h +++ b/src/MacVim/MMTextViewHelper.h @@ -44,8 +44,8 @@ NSRange markedRange; NSDictionary *markedTextAttributes; NSMutableAttributedString *markedText; - int preEditRow; - int preEditColumn; + int preEditRow; ///< The cursor's row. Note that this gets set no matter what. Doesn't matter if we are in pre-edit or not. + int preEditColumn; ///< The cursor's column. BOOL imControl; BOOL imState; TISInputSourceRef lastImSource; @@ -90,6 +90,7 @@ - (NSRange)imRange; - (void)setMarkedRange:(NSRange)range; - (NSRect)firstRectForCharacterRange:(NSRange)range; +- (NSRect)firstRectForCharacterRange:(int)row column:(int)col length:(int)length; - (void)setImControl:(BOOL)enable; - (void)activateIm:(BOOL)enable; - (BOOL)useInlineIm; diff --git a/src/MacVim/MMTextViewHelper.m b/src/MacVim/MMTextViewHelper.m index f178db5b16..7db06b0bc4 100644 --- a/src/MacVim/MMTextViewHelper.m +++ b/src/MacVim/MMTextViewHelper.m @@ -503,8 +503,29 @@ - (void)pressureChangeWithEvent:(NSEvent *)event if (event.stage >= 2) { if (!inForceClick) { inForceClick = YES; - - [self sendGestureEvent:MMGestureForceClick flags:[event modifierFlags]]; + + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + + // See if the OS is configured to use Force click for data lookups + // (the other option usually being three-finger tap). + const BOOL useForceClickLookup = [ud boolForKey:@"com.apple.trackpad.forceClick"]; + + // See if the user has overriden to disallow Force click lookups. + // The usual reason for disallowing it is to support binding + // mappings in Vim. + const BOOL userAllowsForceClickLookup = [ud boolForKey:MMAllowForceClickLookUpKey]; + + if (useForceClickLookup && userAllowsForceClickLookup) { + // For some odd reason, we don't get quickLookWithEvent: even when + // the user has configured to use force click instead of 3-finger + // tap. We need to manually invoke it (this is how NSTextView does + // it as well). References for other software that do this: + // https://gitlab.com/gnachman/iterm2/-/blob/master/sources/PointerController.m + // https://searchfox.org/mozilla-central/source/widget/cocoa/nsChildView.mm + [textView quickLookWithEvent:event]; + } else { + [self sendGestureEvent:MMGestureForceClick flags:[event modifierFlags]]; + } } } else { inForceClick = NO; @@ -751,8 +772,23 @@ - (void)setMarkedRange:(NSRange)range markedRange = range; } +/// Don't use this. See comments. - (NSRect)firstRectForCharacterRange:(NSRange)range { + // + // Note: This is really quite a buggy method and relies on improper + // assumptions. It's kept alive for now because MMTextView (which is also + // deprecated and shouldn't be used for real users) uses this. + // Known bugs: + // - Assumes that preEditRow/Column is the beginning of the marked range, + // but the way it actually works is that it's the current cursor *within* + // the marked range. + // - Uses fontWide to decide to jump 1 or 2 columns per character. First, + // this is wrong, as wide texts work just fine even without guifontwide + // set. Second, some characters may have length > 1. See MMCoreTextView + // which does proper length calculation. + // + // This method is called when the input manager wants to pop up an // auxiliary window. The position where this should be is controlled by // Vim by sending SetPreEditPositionMsgID so compute a position based on @@ -761,13 +797,13 @@ - (NSRect)firstRectForCharacterRange:(NSRange)range int row = preEditRow; NSFont *theFont = [[textView markedTextAttributes] - valueForKey:NSFontAttributeName]; + valueForKey:NSFontAttributeName]; if (theFont == [textView fontWide]) { col += imRange.location * 2; if (col >= [textView maxColumns] - 1) { row += (col / [textView maxColumns]); col = col % 2 ? col % [textView maxColumns] + 1 : - col % [textView maxColumns]; + col % [textView maxColumns]; } } else { col += imRange.location; @@ -777,10 +813,15 @@ - (NSRect)firstRectForCharacterRange:(NSRange)range } } + return [self firstRectForCharacterRange:row column:col length:range.length]; +} + +- (NSRect)firstRectForCharacterRange:(int)row column:(int)col length:(int)numColumns +{ NSRect rect = [textView rectForRow:row column:col numRows:1 - numColumns:range.length]; + numColumns:numColumns]; // NOTE: If the text view is flipped then 'rect' has its origin in the top // left corner of the rect, but the methods below expect it to be in the diff --git a/src/MacVim/Miscellaneous.h b/src/MacVim/Miscellaneous.h index b9a1816d09..0000a26f54 100644 --- a/src/MacVim/Miscellaneous.h +++ b/src/MacVim/Miscellaneous.h @@ -61,6 +61,7 @@ extern NSString *MMNonNativeFullScreenShowMenuKey; extern NSString *MMNonNativeFullScreenSafeAreaBehaviorKey; extern NSString *MMSmoothResizeKey; extern NSString *MMCmdLineAlignBottomKey; +extern NSString *MMAllowForceClickLookUpKey; // Enum for MMUntitledWindowKey diff --git a/src/MacVim/Miscellaneous.m b/src/MacVim/Miscellaneous.m index 2b1bddee5e..0fdf96cd92 100644 --- a/src/MacVim/Miscellaneous.m +++ b/src/MacVim/Miscellaneous.m @@ -57,6 +57,7 @@ NSString *MMNonNativeFullScreenSafeAreaBehaviorKey = @"MMNonNativeFullScreenSafeAreaBehavior"; NSString *MMSmoothResizeKey = @"MMSmoothResize"; NSString *MMCmdLineAlignBottomKey = @"MMCmdLineAlignBottom"; +NSString *MMAllowForceClickLookUpKey = @"MMAllowForceClickLookUp"; @implementation NSIndexSet (MMExtras)