diff --git a/src/MacVim/Base.lproj/Preferences.xib b/src/MacVim/Base.lproj/Preferences.xib index c5ad95715a..c439432d4b 100644 --- a/src/MacVim/Base.lproj/Preferences.xib +++ b/src/MacVim/Base.lproj/Preferences.xib @@ -8,6 +8,7 @@ + @@ -24,11 +25,11 @@ - + - + @@ -77,7 +78,7 @@ - + @@ -156,7 +157,7 @@ - + @@ -189,7 +190,7 @@ - + + + + + + + + + + + + diff --git a/src/MacVim/MMAppController.m b/src/MacVim/MMAppController.m index 596dd4da4c..4692148080 100644 --- a/src/MacVim/MMAppController.m +++ b/src/MacVim/MMAppController.m @@ -233,6 +233,7 @@ + (void)registerDefaults @"", MMLastUsedBundleVersionKey, [NSNumber numberWithBool:YES], MMShowWhatsNewOnStartupKey, [NSNumber numberWithBool:0], MMScrollOneDirectionOnlyKey, + [NSNumber numberWithBool:NO], MMFindBarInlineKey, nil]; [ud registerDefaults:macvimDefaults]; diff --git a/src/MacVim/MMFindBarView.h b/src/MacVim/MMFindBarView.h new file mode 100644 index 0000000000..32a81857b9 --- /dev/null +++ b/src/MacVim/MMFindBarView.h @@ -0,0 +1,37 @@ +/* vi:set ts=8 sts=4 sw=4 ft=objc: + * + * VIM - Vi IMproved by Bram Moolenaar + * MacVim GUI port by Bjorn Winckler + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + */ + +#import + + +@class MMFindBarView; + +@protocol MMFindBarViewDelegate +- (void)findBarView:(MMFindBarView *)view findNext:(BOOL)forward; +- (void)findBarView:(MMFindBarView *)view replace:(BOOL)replaceAll; +- (void)findBarViewDidClose:(MMFindBarView *)view; +// Returns the rect (in MMFindBarView's superview coordinates) within which the +// bar may be dragged. Typically this is the text-view frame, excluding the +// tabline and scrollbars. +- (NSRect)findBarViewDraggableBounds:(MMFindBarView *)view; +@end + + +@interface MMFindBarView : NSView + +@property (nonatomic, assign) id delegate; + +- (void)showWithText:(NSString *)text flags:(int)flags; +- (NSString *)findString; +- (NSString *)replaceString; +- (BOOL)ignoreCase; +- (BOOL)matchWord; + +@end diff --git a/src/MacVim/MMFindBarView.m b/src/MacVim/MMFindBarView.m new file mode 100644 index 0000000000..28448325bf --- /dev/null +++ b/src/MacVim/MMFindBarView.m @@ -0,0 +1,297 @@ +/* vi:set ts=8 sts=4 sw=4 ft=objc: + * + * VIM - Vi IMproved by Bram Moolenaar + * MacVim GUI port by Bjorn Winckler + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + */ +/* + * MMFindBarView - inline find/replace bar + * + * An NSView overlay anchored to the top-right corner of MMVimView that + * provides the same find/replace functionality as the floating + * MMFindReplaceController panel, but without leaving the editor window. + * The UI is built programmatically (no xib). + * + * Activation: controlled by the MMFindBarInlineKey user default. + * When enabled, ShowFindReplaceDialogMsgID routes here instead of to + * MMFindReplaceController. + */ + +#import "MMFindBarView.h" +#import "MMTabline/MMHoverButton.h" + + +// FRD flag bits (must match FRD_ defines in Vim's gui.h) +enum { + MMFRDForward = 0, + MMFRDBackward = 0x100, + MMFRDReplace = 0x03, + MMFRDReplaceAll= 0x04, + MMFRDMatchWord = 0x08, + MMFRDExactMatch= 0x10, // no ignore-case when set +}; + +static const CGFloat kBarWidth = 490; +static const CGFloat kBarHeight = 148; // 2*kMargin + 4*kFieldH + 3*kRowGap +static const CGFloat kMargin = 12; // ~3 mm outer padding on all four sides +static const CGFloat kLabelW = 90; +static const CGFloat kFieldH = 22; +static const CGFloat kRowH = 34; // kFieldH + kMargin, keeps row gap ~3 mm + + +@implementation MMFindBarView { + NSTextField *_findBox; + NSTextField *_replaceBox; + NSButton *_ignoreCaseButton; + NSButton *_matchWordButton; + NSButton *_replaceButton; + NSButton *_replaceAllButton; + NSButton *_prevButton; + NSButton *_nextButton; + NSButton *_closeButton; // MMHoverButton + NSPoint _dragOffset; // mouse-down offset for dragging +} + +- (instancetype)init { + self = [super initWithFrame:NSMakeRect(0, 0, kBarWidth, kBarHeight)]; + if (!self) return nil; + [self _buildUI]; + return self; +} + +- (instancetype)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + if (!self) return nil; + [self _buildUI]; + return self; +} + +- (void)_buildUI { + self.wantsLayer = YES; + // Access [self layer] to force layer creation before configuring properties; + // simply setting wantsLayer=YES does not guarantee self.layer is non-nil yet. + CALayer *layer = [self layer]; + layer.zPosition = 100; + layer.borderWidth = 1; + layer.borderColor = [NSColor separatorColor].CGColor; + layer.cornerRadius = 4; + + // ── Step 1: Pre-create buttons and measure uniform width ───────────────── + // Must happen first so we can derive the right-alignment edge for fields. + _replaceAllButton = [NSButton buttonWithTitle:@"Replace All" target:self action:@selector(_replaceAll:)]; + _replaceButton = [NSButton buttonWithTitle:@"Replace" target:self action:@selector(_replace:)]; + _prevButton = [NSButton buttonWithTitle:@"Previous" target:self action:@selector(_findPrevious:)]; + _nextButton = [NSButton buttonWithTitle:@"Next" target:self action:@selector(_findNext:)]; + + NSArray *actionButtons = @[_replaceAllButton, _replaceButton, _prevButton, _nextButton]; + CGFloat maxBtnW = 0; + for (NSButton *btn in actionButtons) { + [btn sizeToFit]; + maxBtnW = MAX(maxBtnW, btn.frame.size.width); + } + maxBtnW += 8; + + // Right edge of the button row (4 buttons + 3 gaps, starting at kMargin) + CGFloat buttonsRight = kMargin + 4 * maxBtnW + 3 * 4; + + // ── Step 2: Field geometry — right edge aligned to buttonsRight ────────── + CGFloat fieldX = kMargin + kLabelW + 4; + CGFloat fieldW = buttonsRight - fieldX - 6; // both fields share this width + + // ── Close button: MMHoverButton (tab-style × with circular hover bg) ───── + // Positioned at bar's top-right corner with a tighter 6pt margin so it + // sits closer to the corner than the content rows (margin = 12pt). + MMHoverButton *closeBtn = [MMHoverButton new]; + closeBtn.imageTemplate = [MMHoverButton imageFromType:MMHoverButtonImageCloseTab]; + closeBtn.target = self; + closeBtn.action = @selector(_close:); + closeBtn.bordered = NO; + CGFloat closeSize = 15; + closeBtn.frame = NSMakeRect( + kBarWidth - 6 - closeSize, + kBarHeight - 6 - closeSize, + closeSize, closeSize); + _closeButton = closeBtn; + + // ── Row 1: Find ────────────────────────────────────────────────────────── + CGFloat y = kBarHeight - kMargin - kFieldH; + + NSTextField *findLabel = [NSTextField labelWithString:@"Find:"]; + findLabel.alignment = NSTextAlignmentRight; + findLabel.frame = NSMakeRect(kMargin, y, kLabelW, kFieldH); + [self addSubview:findLabel]; + + _findBox = [[NSTextField alloc] initWithFrame: + NSMakeRect(fieldX, y, fieldW, kFieldH)]; + _findBox.placeholderString = @"Search"; + _findBox.delegate = self; + _findBox.target = self; + _findBox.action = @selector(_findNext:); + [self addSubview:_findBox]; + // Close button floats above the find field + [self addSubview:_closeButton positioned:NSWindowAbove relativeTo:_findBox]; + + // ── Row 2: Replace with ────────────────────────────────────────────────── + y -= kRowH; + NSTextField *replaceLabel = [NSTextField labelWithString:@"Replace with:"]; + replaceLabel.alignment = NSTextAlignmentRight; + replaceLabel.frame = NSMakeRect(kMargin, y, kLabelW, kFieldH); + [self addSubview:replaceLabel]; + + _replaceBox = [[NSTextField alloc] initWithFrame: + NSMakeRect(fieldX, y, fieldW, kFieldH)]; + _replaceBox.placeholderString = @"Replace"; + _replaceBox.delegate = self; + [self addSubview:_replaceBox]; + + // ── Row 3: Checkboxes ──────────────────────────────────────────────────── + y -= kRowH; + _ignoreCaseButton = [NSButton checkboxWithTitle:@"Ignore case" + target:nil action:nil]; + _ignoreCaseButton.frame = NSMakeRect(fieldX, y, 110, kFieldH); + [self addSubview:_ignoreCaseButton]; + + _matchWordButton = [NSButton checkboxWithTitle:@"Match whole word only" + target:nil action:nil]; + _matchWordButton.frame = NSMakeRect(fieldX + 114, y, 170, kFieldH); + [self addSubview:_matchWordButton]; + + // ── Row 4: Action buttons ──────────────────────────────────────────────── + y -= kRowH; + CGFloat bx = kMargin; + for (NSButton *btn in actionButtons) { + btn.frame = NSMakeRect(bx, y, maxBtnW, kFieldH); + [self addSubview:btn]; + bx += maxBtnW + 4; + } +} + +// ── Public API ─────────────────────────────────────────────────────────────── + +- (void)showWithText:(NSString *)text flags:(int)flags { + if (text && text.length > 0) + _findBox.stringValue = text; + + // Restore checkbox state from flags + _ignoreCaseButton.state = (flags & MMFRDExactMatch) ? NSControlStateValueOff + : NSControlStateValueOn; + _matchWordButton.state = (flags & MMFRDMatchWord) ? NSControlStateValueOn + : NSControlStateValueOff; + + self.hidden = NO; + [[self window] makeFirstResponder:_findBox]; +} + +- (NSString *)findString { return _findBox.stringValue; } +- (NSString *)replaceString { return _replaceBox.stringValue; } +- (BOOL)ignoreCase { return _ignoreCaseButton.state == NSControlStateValueOn; } +- (BOOL)matchWord { return _matchWordButton.state == NSControlStateValueOn; } + +// ── Background ─────────────────────────────────────────────────────────────── + +- (void)drawRect:(NSRect)dirtyRect { + [[NSColor windowBackgroundColor] setFill]; + NSRectFill(dirtyRect); + [super drawRect:dirtyRect]; +} + +- (void)updateLayer { + self.layer.borderColor = [NSColor separatorColor].CGColor; +} + +// ── Button actions ─────────────────────────────────────────────────────────── + +- (void)_findNext:(id)sender { + [_delegate findBarView:self findNext:YES]; +} + +- (void)_findPrevious:(id)sender { + [_delegate findBarView:self findNext:NO]; +} + +- (void)_replace:(id)sender { + [_delegate findBarView:self replace:NO]; +} + +- (void)_replaceAll:(id)sender { + [_delegate findBarView:self replace:YES]; +} + +- (void)_close:(id)sender { + self.hidden = YES; + [_delegate findBarViewDidClose:self]; +} + +// ── Dragging ────────────────────────────────────────────────────────────────── +// Allow the bar to be dragged anywhere within the text-editing area. +// The delegate supplies the allowed rect; if unavailable we fall back to the +// superview bounds so the bar can never leave the window. + +- (void)mouseDown:(NSEvent *)event { + // Record where inside the bar the user clicked so we can keep that point + // under the cursor during the drag. + NSPoint locInSelf = [self convertPoint:event.locationInWindow fromView:nil]; + _dragOffset = locInSelf; +} + +- (void)mouseDragged:(NSEvent *)event { + NSPoint locInSuper = [self.superview convertPoint:event.locationInWindow + fromView:nil]; + NSRect bounds = [_delegate findBarViewDraggableBounds:self]; + NSSize barSize = self.frame.size; + + CGFloat newX = locInSuper.x - _dragOffset.x; + CGFloat newY = locInSuper.y - _dragOffset.y; + + // Clamp so the bar stays fully inside the draggable bounds. + newX = MAX(NSMinX(bounds), MIN(newX, NSMaxX(bounds) - barSize.width)); + newY = MAX(NSMinY(bounds), MIN(newY, NSMaxY(bounds) - barSize.height)); + + [self setFrameOrigin:NSMakePoint(newX, newY)]; +} + +// ── NSControlTextEditingDelegate ───────────────────────────────────────────── +// Called by the field editor for each key command while a text field is active. +// This is the reliable way to intercept Escape and Return from NSTextField — +// overriding cancelOperation:/keyDown: on the text field itself is unreliable +// because the field editor (an NSTextView) is the actual first responder during +// editing and handles those events before the control sees them. + +- (BOOL)control:(NSControl *)control + textView:(NSTextView *)textView +doCommandBySelector:(SEL)cmd +{ + if (cmd == @selector(cancelOperation:)) { + // Escape → close the find bar + [self _close:nil]; + return YES; + } + if (cmd == @selector(insertNewline:)) { + // Return / Shift+Return → find next / previous + NSUInteger mods = [NSApp currentEvent].modifierFlags + & NSEventModifierFlagDeviceIndependentFlagsMask; + if (mods & NSEventModifierFlagShift) + [self _findPrevious:control]; + else + [self _findNext:control]; + return YES; + } + if (cmd == @selector(insertTab:)) { + // Tab → move focus: find → replace → find → … + NSTextField *next = (control == _findBox) ? _replaceBox : _findBox; + [[self window] makeFirstResponder:next]; + return YES; + } + if (cmd == @selector(insertBacktab:)) { + // Shift+Tab → move focus in reverse + NSTextField *prev = (control == _replaceBox) ? _findBox : _replaceBox; + [[self window] makeFirstResponder:prev]; + return YES; + } + return NO; +} + +@end diff --git a/src/MacVim/MMPreferenceController.h b/src/MacVim/MMPreferenceController.h index 5cc152616a..f721396f7a 100644 --- a/src/MacVim/MMPreferenceController.h +++ b/src/MacVim/MMPreferenceController.h @@ -21,6 +21,7 @@ IBOutlet NSPopUpButton *layoutPopUpButton; IBOutlet NSButton *autoInstallUpdateButton; IBOutlet NSView *sparkleUpdaterPane; + IBOutlet NSButton *findBarInlineButton; // Input pane IBOutlet NSButton *allowForceClickLookUpButton; @@ -36,6 +37,7 @@ - (IBAction)checkForUpdatesChanged:(id)sender; - (IBAction)appearanceChanged:(id)sender; - (IBAction)smoothResizeChanged:(id)sender; +- (IBAction)findBarModeChanged:(id)sender; // Appearance pane - (IBAction)fontPropertiesChanged:(id)sender; diff --git a/src/MacVim/MMPreferenceController.m b/src/MacVim/MMPreferenceController.m index 32f19ee84f..8e912372a9 100644 --- a/src/MacVim/MMPreferenceController.m +++ b/src/MacVim/MMPreferenceController.m @@ -177,6 +177,12 @@ - (IBAction)smoothResizeChanged:(id)sender [[MMAppController sharedInstance] refreshAllResizeConstraints]; } +- (IBAction)findBarModeChanged:(id)sender +{ + // Setting is read via NSUserDefaults each time a find dialog is shown, + // so no refresh of existing windows is required. +} + - (IBAction)cmdlineAlignBottomChanged:(id)sender { [[MMAppController sharedInstance] refreshAllTextViews]; diff --git a/src/MacVim/MMVimController.m b/src/MacVim/MMVimController.m index e48f55f411..af42c33cb9 100644 --- a/src/MacVim/MMVimController.m +++ b/src/MacVim/MMVimController.m @@ -1231,9 +1231,17 @@ - (void)handleMessage:(int)msgid data:(NSData *)data { NSDictionary *dict = [NSDictionary dictionaryWithData:data]; if (dict) { - [[MMFindReplaceController sharedInstance] - showWithText:[dict objectForKey:@"text"] - flags:[[dict objectForKey:@"flags"] intValue]]; + BOOL useInline = [[NSUserDefaults standardUserDefaults] + boolForKey:MMFindBarInlineKey]; + if (useInline) { + [[windowController vimView] + showFindBarWithText:[dict objectForKey:@"text"] + flags:[[dict objectForKey:@"flags"] intValue]]; + } else { + [[MMFindReplaceController sharedInstance] + showWithText:[dict objectForKey:@"text"] + flags:[[dict objectForKey:@"flags"] intValue]]; + } } } break; diff --git a/src/MacVim/MMVimView.h b/src/MacVim/MMVimView.h index f1a49009b3..ec00984ac8 100644 --- a/src/MacVim/MMVimView.h +++ b/src/MacVim/MMVimView.h @@ -9,6 +9,7 @@ */ #import +#import "MMFindBarView.h" @@ -19,13 +20,14 @@ @class MMVimController; -@interface MMVimView : NSView { +@interface MMVimView : NSView { /// The tab that has been requested to be closed and waiting on Vim to respond NSInteger pendingCloseTabID; MMTabline *tabline; MMVimController *vimController; MMTextView *textView; NSMutableArray *scrollbars; + MMFindBarView *findBarView; } @property BOOL pendingPlaceScrollbars; @@ -35,6 +37,7 @@ - (MMVimView *)initWithFrame:(NSRect)frame vimController:(MMVimController *)c; - (MMTextView *)textView; +- (MMFindBarView *)findBarView; - (void)cleanup; - (NSSize)desiredSize; @@ -70,4 +73,7 @@ - (void)setFrameSizeKeepGUISize:(NSSize)size; - (void)setFrame:(NSRect)frame; +- (void)showFindBarWithText:(NSString *)text flags:(int)flags; +- (void)hideFindBar; + @end diff --git a/src/MacVim/MMVimView.m b/src/MacVim/MMVimView.m index 6f8f4a3ded..e33ca4c3be 100644 --- a/src/MacVim/MMVimView.m +++ b/src/MacVim/MMVimView.m @@ -84,6 +84,7 @@ - (void)liveResizeDidEnd; @implementation MMVimView { NSColor *tabColors[MMTabColorTypeCount]; + NSRect _lastTextRectForFindBar; // textView.frame at last find bar layout } - (MMVimView *)initWithFrame:(NSRect)frame @@ -135,7 +136,13 @@ - (MMVimView *)initWithFrame:(NSRect)frame tabline.addTabButton.action = @selector(addNewTab:); [tabline registerForDraggedTypes:@[getPasteboardFilenamesType()]]; [self addSubview:tabline]; - + + // Create the inline find bar (initially hidden, placed in top-right corner). + findBarView = [[MMFindBarView alloc] init]; + findBarView.hidden = YES; + findBarView.delegate = self; + [self addSubview:findBarView positioned:NSWindowAbove relativeTo:nil]; + return self; } @@ -224,6 +231,11 @@ - (MMTextView *)textView return textView; } +- (MMFindBarView *)findBarView +{ + return findBarView; +} + - (MMTabline *)tabline { return tabline; @@ -675,6 +687,58 @@ - (void)viewDidChangeEffectiveAppearance [winController performSelectorOnMainThread:@selector(refreshTabProperties) withObject:nil waitUntilDone:NO]; } } + +// ── Find bar public API ───────────────────────────────────────────────────── + +- (void)showFindBarWithText:(NSString *)text flags:(int)flags { + NSRect textRect = [textView frame]; + + // Only snap to the default top-right position when the bar is currently + // hidden. If it is already visible the user may have dragged it, so we + // keep its current position. + if (findBarView.hidden) { + NSSize barSize = findBarView.frame.size; + [findBarView setFrame:NSMakeRect( + NSMaxX(textRect) - barSize.width - 8, + NSMaxY(textRect) - barSize.height - 8, + barSize.width, + barSize.height)]; + } + _lastTextRectForFindBar = textRect; + + [findBarView showWithText:text flags:flags]; +} + +- (void)hideFindBar { + findBarView.hidden = YES; + if ([self window]) + [[self window] makeFirstResponder:textView]; +} + +// ── MMFindBarViewDelegate ──────────────────────────────────────────────────── + +- (void)findBarView:(MMFindBarView *)view findNext:(BOOL)forward { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; + item.tag = forward ? 0 : 1; + [[vimController windowController] findAndReplace:item]; +} + +- (void)findBarView:(MMFindBarView *)view replace:(BOOL)replaceAll { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; + item.tag = replaceAll ? 3 : 2; + [[vimController windowController] findAndReplace:item]; +} + +- (void)findBarViewDidClose:(MMFindBarView *)view { + [self hideFindBar]; +} + +- (NSRect)findBarViewDraggableBounds:(MMFindBarView *)view { + // Allow dragging anywhere within the text-editing area so the bar never + // overlaps the tabline or scrollbars. + return [textView frame]; +} + @end // MMVimView @@ -929,6 +993,34 @@ - (void)frameSizeMayHaveChanged:(BOOL)keepGUISize self.pendingPlaceScrollbars = NO; [self placeScrollbars]; + // Keep the find bar inside the text area when the window is resized. + // Use the delta of the text area's edges so that the bar tracks tabline + // show/hide and window resizes correctly, regardless of where the user + // may have dragged it. + if (findBarView) { + NSRect newTextRect = [textView frame]; + + if (!findBarView.hidden) { + NSRect barFrame = findBarView.frame; + + // Shift bar by the same amount the text area's top-right corner moved. + CGFloat dx = NSMaxX(newTextRect) - NSMaxX(_lastTextRectForFindBar); + CGFloat dy = NSMaxY(newTextRect) - NSMaxY(_lastTextRectForFindBar); + CGFloat newX = barFrame.origin.x + dx; + CGFloat newY = barFrame.origin.y + dy; + + // Clamp so the bar stays fully inside the text area. + newX = MAX(NSMinX(newTextRect), MIN(newX, NSMaxX(newTextRect) - barFrame.size.width)); + newY = MAX(NSMinY(newTextRect), MIN(newY, NSMaxY(newTextRect) - barFrame.size.height)); + + [findBarView setFrameOrigin:NSMakePoint(newX, newY)]; + } + + // Always update the cached rect so that the delta is correct the next + // time the bar becomes visible (avoids stale-rect jump on re-show). + _lastTextRectForFindBar = newTextRect; + } + // It is possible that the current number of (rows,columns) is too big or // too small to fit the new frame. If so, notify Vim that the text // dimensions should change, but don't actually change the number of diff --git a/src/MacVim/MMWindowController.m b/src/MacVim/MMWindowController.m index fc977f5ab1..035b232958 100644 --- a/src/MacVim/MMWindowController.m +++ b/src/MacVim/MMWindowController.m @@ -58,7 +58,7 @@ * * The window is always kept centered and resizing works more or less the same * way as in windowed mode. - * + * */ #import "MMAppController.h" @@ -122,7 +122,7 @@ @implementation MMWindowController - (id)initWithVimController:(MMVimController *)controller { backgroundDark = NO; - + unsigned styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable @@ -169,7 +169,7 @@ - (id)initWithVimController:(MMVimController *)controller vimController = controller; decoratedWindow = [win retain]; - + [self refreshApperanceMode]; // Window cascading is handled by MMAppController. @@ -216,7 +216,7 @@ - (id)initWithVimController:(MMVimController *)controller // Make us safe on pre-tiger OSX if ([win respondsToSelector:@selector(_setContentHasShadow:)]) [win _setContentHasShadow:NO]; - + // This adds the title bar full-screen button (which calls // toggleFullScreen:) and also populates the Window menu itmes for full // screen tiling. Even if we are using non-native full screen, we still set @@ -544,7 +544,7 @@ - (void)setTitle:(NSString *)title [title retain]; // retain the title first before release lastSetTitle, since you can call setTitle on lastSetTitle itself. [lastSetTitle release]; lastSetTitle = title; - + // While in live resize the window title displays the dimensions of the // window so don't clobber this with the new title. We have already set // lastSetTitle above so once live resize is done we will set it back. @@ -622,7 +622,7 @@ - (void)createScrollbarWithIdentifier:(int32_t)ident type:(int)type - (BOOL)destroyScrollbarWithIdentifier:(int32_t)ident { - BOOL scrollbarHidden = [vimView destroyScrollbarWithIdentifier:ident]; + BOOL scrollbarHidden = [vimView destroyScrollbarWithIdentifier:ident]; return scrollbarHidden; } @@ -660,14 +660,14 @@ - (void)refreshApperanceMode // titlebar settings) to use for this window, depending on what the user // has selected as a preference. NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - + // Transparent title bar setting #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_10 if (AVAILABLE_MAC_OS(10, 10)) { decoratedWindow.titlebarAppearsTransparent = [ud boolForKey:MMTitlebarAppearsTransparentKey]; } #endif - + // No title bar setting if ([ud boolForKey:MMNoTitleBarWindowKey]) { [decoratedWindow setStyleMask:([decoratedWindow styleMask] & ~NSWindowStyleMaskTitled)]; @@ -720,7 +720,7 @@ - (void)refreshApperanceMode break; } } - + decoratedWindow.appearance = desiredAppearance; fullScreenWindow.appearance = desiredAppearance; } @@ -1087,7 +1087,7 @@ - (void)liveResizeDidEnd - (void)setBlurRadius:(int)radius { blurRadius = radius; - if (windowPresented) { + if (windowPresented) { [decoratedWindow setBlurRadius:radius]; if (fullScreenWindow) { [MMWindow setBlurRadius:radius onWindow:fullScreenWindow]; @@ -1352,6 +1352,14 @@ - (IBAction)findAndReplace:(id)sender { NSInteger tag = [sender tag]; MMFindReplaceController *fr = [MMFindReplaceController sharedInstance]; + MMFindBarView *fb = [[vimView findBarView] isHidden] ? nil : [vimView findBarView]; + BOOL useInline = [[NSUserDefaults standardUserDefaults] boolForKey:MMFindBarInlineKey] + && fb != nil; + BOOL matchWord = useInline ? [fb matchWord] : [fr matchWord]; + BOOL ignoreCase = useInline ? [fb ignoreCase] : [fr ignoreCase]; + NSString *findStr = useInline ? [fb findString] : [fr findString]; + NSString *replStr = useInline ? [fb replaceString] : [fr replaceString]; + int flags = 0; // NOTE: The 'flags' values must match the FRD_ defines in gui.h (except @@ -1362,14 +1370,14 @@ - (IBAction)findAndReplace:(id)sender case 3: flags = 4; break; } - if ([fr matchWord]) + if (matchWord) flags |= 0x08; - if (![fr ignoreCase]) + if (!ignoreCase) flags |= 0x10; NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys: - [fr findString], @"find", - [fr replaceString], @"replace", + findStr, @"find", + replStr, @"replace", [NSNumber numberWithInt:flags], @"flags", nil]; @@ -2069,7 +2077,7 @@ - (void)doFindNext:(BOOL)next input = [NSString stringWithFormat:@":let @/='%@'%c", query, next ? 'n' : 'N']; } else { - input = next ? @"n" : @"N"; + input = next ? @"n" : @"N"; } [vimController addVimInput:input]; diff --git a/src/MacVim/MacVim.xcodeproj/project.pbxproj b/src/MacVim/MacVim.xcodeproj/project.pbxproj index e6eaa210e2..a503e08e45 100644 --- a/src/MacVim/MacVim.xcodeproj/project.pbxproj +++ b/src/MacVim/MacVim.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 1D384A0E100D671700D3C22F /* KeyBinding.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1D384A0D100D671700D3C22F /* KeyBinding.plist */; }; 1D44972211FCA9B400B0630F /* MMCoreTextView+ToolTip.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D44972111FCA9B400B0630F /* MMCoreTextView+ToolTip.m */; }; 1D493D580C5247BF00AB718C /* Vim in Copy Executables */ = {isa = PBXBuildFile; fileRef = 1D493D570C5247BF00AB718C /* Vim */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + AA000003000000000000000C /* MMFindBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000000000000000B /* MMFindBarView.m */; }; 1D60088B0E96A0B2003763F0 /* MMFindReplaceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D60088A0E96A0B2003763F0 /* MMFindReplaceController.m */; }; 1D80591F0E1185EA001699D1 /* Miscellaneous.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D80591D0E1185EA001699D1 /* Miscellaneous.m */; }; 1D80FBD40CBBD3B700102A1C /* MMFullScreenWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D80FBD00CBBD3B700102A1C /* MMFullScreenWindow.m */; }; @@ -189,6 +190,8 @@ 1D384A0D100D671700D3C22F /* KeyBinding.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = KeyBinding.plist; sourceTree = ""; }; 1D44972111FCA9B400B0630F /* MMCoreTextView+ToolTip.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MMCoreTextView+ToolTip.m"; sourceTree = ""; }; 1D493D570C5247BF00AB718C /* Vim */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = Vim; path = ../Vim; sourceTree = SOURCE_ROOT; }; + AA000001000000000000000A /* MMFindBarView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MMFindBarView.h; sourceTree = ""; }; + AA000002000000000000000B /* MMFindBarView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMFindBarView.m; sourceTree = ""; }; 1D6008890E96A0B2003763F0 /* MMFindReplaceController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MMFindReplaceController.h; sourceTree = ""; }; 1D60088A0E96A0B2003763F0 /* MMFindReplaceController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMFindReplaceController.m; sourceTree = ""; }; 1D80591D0E1185EA001699D1 /* Miscellaneous.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Miscellaneous.m; sourceTree = ""; }; @@ -515,6 +518,8 @@ 9098943B2A56ECF6007B84A3 /* MMWhatsNewController.m */, 925B55CB254B604A006B047E /* MMTabline */, 1D44972111FCA9B400B0630F /* MMCoreTextView+ToolTip.m */, + AA000001000000000000000A /* MMFindBarView.h */, + AA000002000000000000000B /* MMFindBarView.m */, 1D6008890E96A0B2003763F0 /* MMFindReplaceController.h */, 1D60088A0E96A0B2003763F0 /* MMFindReplaceController.m */, 1DE63FF90E71820F00959BDB /* MMCoreTextView.h */, @@ -1240,6 +1245,7 @@ 1D80591F0E1185EA001699D1 /* Miscellaneous.m in Sources */, 9098943C2A56ECF6007B84A3 /* MMWhatsNewController.m in Sources */, 1D145C7F0E5227CE00691AA0 /* MMTextViewHelper.m in Sources */, + AA000003000000000000000C /* MMFindBarView.m in Sources */, 1D60088B0E96A0B2003763F0 /* MMFindReplaceController.m in Sources */, 1DE63FFB0E71820F00959BDB /* MMCoreTextView.m in Sources */, 92C6F6E825587E1C007AE21E /* MMTab.m in Sources */, diff --git a/src/MacVim/MacVimTests/MacVimTests.m b/src/MacVim/MacVimTests/MacVimTests.m index 213c649e9f..b0dd542904 100644 --- a/src/MacVim/MacVimTests/MacVimTests.m +++ b/src/MacVim/MacVimTests/MacVimTests.m @@ -15,6 +15,7 @@ #import "Miscellaneous.h" #import "MMAppController.h" #import "MMApplication.h" +#import "MMFindBarView.h" #import "MMFullScreenWindow.h" #import "MMWindow.h" #import "MMTabline.h" @@ -1576,4 +1577,78 @@ - (void)testIPCSelectedText { [self waitForEventHandlingAndVimProcess]; } +// ── MMFindBarView tests ─────────────────────────────────────────────────────── + +/// Test that showWithText:flags: correctly restores the Ignore Case and Match +/// Word checkbox states from the flags bitmask. +- (void)testFindBarShowWithFlags { + MMFindBarView *bar = [[MMFindBarView alloc] init]; + + // flags = 0: ignoreCase ON (no ExactMatch bit), matchWord OFF + [bar showWithText:@"hello" flags:0]; + XCTAssertTrue([bar ignoreCase], @"ignoreCase should be ON when ExactMatch bit is clear"); + XCTAssertFalse([bar matchWord], @"matchWord should be OFF when MatchWord bit is clear"); + XCTAssertEqualObjects([bar findString], @"hello"); + + // flags = MMFRDExactMatch (0x10): ignoreCase OFF + [bar showWithText:@"world" flags:0x10]; + XCTAssertFalse([bar ignoreCase], @"ignoreCase should be OFF when ExactMatch bit (0x10) is set"); + XCTAssertFalse([bar matchWord], @"matchWord should still be OFF"); + XCTAssertEqualObjects([bar findString], @"world"); + + // flags = MMFRDMatchWord (0x08): matchWord ON + [bar showWithText:@"foo" flags:0x08]; + XCTAssertTrue([bar ignoreCase], @"ignoreCase should be ON (ExactMatch bit clear)"); + XCTAssertTrue([bar matchWord], @"matchWord should be ON when MatchWord bit (0x08) is set"); + + // flags = MMFRDExactMatch | MMFRDMatchWord (0x18): both set + [bar showWithText:@"bar" flags:0x18]; + XCTAssertFalse([bar ignoreCase], @"ignoreCase should be OFF"); + XCTAssertTrue([bar matchWord], @"matchWord should be ON"); + + // Passing nil text should not crash and should not clear existing text + NSString *prevText = [bar findString]; + [bar showWithText:nil flags:0]; + XCTAssertEqualObjects([bar findString], prevText, @"nil text should not clear the find field"); +} + +/// Test that calling showFindBarWithText:flags: on MMVimView snaps the bar to +/// the top-right corner only when the bar is hidden, and preserves the current +/// position when the bar is already visible. +- (void)testFindBarPositionPreservedOnReshow { + [self createTestVimWindow]; + + MMAppController *app = MMAppController.sharedInstance; + MMVimView *vimView = app.keyVimController.windowController.vimView; + MMFindBarView *bar = [vimView findBarView]; + + // Bar starts hidden; first show should snap to top-right. + XCTAssertTrue(bar.hidden, @"find bar should start hidden"); + [self setDefault:MMFindBarInlineKey toValue:@YES]; + [vimView showFindBarWithText:@"test" flags:0]; + + NSRect snapFrame = bar.frame; + NSRect textRect = vimView.textView.frame; + + // Verify snapped position is near the top-right corner (within 1pt tolerance). + XCTAssertEqualWithAccuracy(NSMaxX(snapFrame), NSMaxX(textRect) - 8, 1, + @"find bar right edge should be 8pt inside text area right edge on first show"); + XCTAssertEqualWithAccuracy(NSMaxY(snapFrame), NSMaxY(textRect) - 8, 1, + @"find bar top edge should be 8pt below text area top edge on first show"); + + // Move the bar to a different position. + NSPoint movedOrigin = NSMakePoint(snapFrame.origin.x - 50, snapFrame.origin.y - 30); + [bar setFrameOrigin:movedOrigin]; + + // Call showFindBarWithText: again while bar is already visible. + [vimView showFindBarWithText:@"test2" flags:0]; + + XCTAssertEqualWithAccuracy(bar.frame.origin.x, movedOrigin.x, 1, + @"X position should be preserved when bar is already visible"); + XCTAssertEqualWithAccuracy(bar.frame.origin.y, movedOrigin.y, 1, + @"Y position should be preserved when bar is already visible"); + XCTAssertEqualObjects([bar findString], @"test2", + @"find text should be updated even when position is preserved"); +} + @end diff --git a/src/MacVim/Miscellaneous.h b/src/MacVim/Miscellaneous.h index 0216de6f21..8e3905f289 100644 --- a/src/MacVim/Miscellaneous.h +++ b/src/MacVim/Miscellaneous.h @@ -75,6 +75,7 @@ extern NSString *MMUpdaterPrereleaseChannelKey; extern NSString *MMLastUsedBundleVersionKey; ///< The last used version of MacVim before this launch extern NSString *MMShowWhatsNewOnStartupKey; extern NSString *MMScrollOneDirectionOnlyKey; +extern NSString *MMFindBarInlineKey; // Enum for MMUntitledWindowKey diff --git a/src/MacVim/Miscellaneous.m b/src/MacVim/Miscellaneous.m index 3618db398e..dbe479a615 100644 --- a/src/MacVim/Miscellaneous.m +++ b/src/MacVim/Miscellaneous.m @@ -71,6 +71,7 @@ NSString *MMLastUsedBundleVersionKey = @"MMLastUsedBundleVersion"; NSString *MMShowWhatsNewOnStartupKey = @"MMShowWhatsNewOnStartup"; NSString *MMScrollOneDirectionOnlyKey = @"MMScrollOneDirectionOnly"; +NSString *MMFindBarInlineKey = @"MMFindBarInline"; @implementation NSIndexSet (MMExtras)