From d6732e590652d2ec3bce555b286d11297a88307e Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 30 May 2024 13:35:10 -0400 Subject: [PATCH] Ladybird/AppKit: Implement a basic find-in-page panel --- .../AppKit/Application/ApplicationDelegate.mm | 14 ++ Ladybird/AppKit/UI/LadybirdWebView.h | 6 +- Ladybird/AppKit/UI/LadybirdWebView.mm | 17 +- Ladybird/AppKit/UI/SearchPanel.h | 18 ++ Ladybird/AppKit/UI/SearchPanel.mm | 182 ++++++++++++++++++ Ladybird/AppKit/UI/Tab.mm | 42 +++- Ladybird/CMakeLists.txt | 1 + Meta/gn/secondary/Ladybird/BUILD.gn | 1 + 8 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 Ladybird/AppKit/UI/SearchPanel.h create mode 100644 Ladybird/AppKit/UI/SearchPanel.mm diff --git a/Ladybird/AppKit/Application/ApplicationDelegate.mm b/Ladybird/AppKit/Application/ApplicationDelegate.mm index b4a72a40dae..195b5ef17a7 100644 --- a/Ladybird/AppKit/Application/ApplicationDelegate.mm +++ b/Ladybird/AppKit/Application/ApplicationDelegate.mm @@ -354,6 +354,20 @@ [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Select All" action:@selector(selectAll:) keyEquivalent:@"a"]]; + [submenu addItem:[NSMenuItem separatorItem]]; + + [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Find..." + action:@selector(find:) + keyEquivalent:@"f"]]; + [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Find Next" + action:@selector(findNextMatch:) + keyEquivalent:@"g"]]; + [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Find Previous" + action:@selector(findPreviousMatch:) + keyEquivalent:@"G"]]; + [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Use Selection for Find" + action:@selector(useSelectionForFind:) + keyEquivalent:@"e"]]; [menu setSubmenu:submenu]; return menu; diff --git a/Ladybird/AppKit/UI/LadybirdWebView.h b/Ladybird/AppKit/UI/LadybirdWebView.h index f62ce10e776..2de55459d41 100644 --- a/Ladybird/AppKit/UI/LadybirdWebView.h +++ b/Ladybird/AppKit/UI/LadybirdWebView.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Tim Flynn + * Copyright (c) 2023-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -60,6 +60,10 @@ - (void)setPreferredColorScheme:(Web::CSS::PreferredColorScheme)color_scheme; +- (void)findInPage:(NSString*)query; +- (void)findInPageNextMatch; +- (void)findInPagePreviousMatch; + - (void)zoomIn; - (void)zoomOut; - (void)resetZoom; diff --git a/Ladybird/AppKit/UI/LadybirdWebView.mm b/Ladybird/AppKit/UI/LadybirdWebView.mm index c5e1867b3c9..abeeff77db9 100644 --- a/Ladybird/AppKit/UI/LadybirdWebView.mm +++ b/Ladybird/AppKit/UI/LadybirdWebView.mm @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Tim Flynn + * Copyright (c) 2023-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -180,6 +180,21 @@ struct HideCursor { m_web_view_bridge->set_system_visibility_state(is_visible); } +- (void)findInPage:(NSString*)query +{ + m_web_view_bridge->find_in_page(Ladybird::ns_string_to_string(query)); +} + +- (void)findInPageNextMatch +{ + m_web_view_bridge->find_in_page_next_match(); +} + +- (void)findInPagePreviousMatch +{ + m_web_view_bridge->find_in_page_previous_match(); +} + - (void)zoomIn { m_web_view_bridge->zoom_in(); diff --git a/Ladybird/AppKit/UI/SearchPanel.h b/Ladybird/AppKit/UI/SearchPanel.h new file mode 100644 index 00000000000..5f93fe1db5d --- /dev/null +++ b/Ladybird/AppKit/UI/SearchPanel.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#import + +@interface SearchPanel : NSStackView + +- (void)find:(id)selector; +- (void)findNextMatch:(id)selector; +- (void)findPreviousMatch:(id)selector; +- (void)useSelectionForFind:(id)selector; + +@end diff --git a/Ladybird/AppKit/UI/SearchPanel.mm b/Ladybird/AppKit/UI/SearchPanel.mm new file mode 100644 index 00000000000..a16e7babd9d --- /dev/null +++ b/Ladybird/AppKit/UI/SearchPanel.mm @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#import +#import +#import +#import + +#if !__has_feature(objc_arc) +# error "This project requires ARC" +#endif + +static constexpr CGFloat const SEARCH_FIELD_HEIGHT = 30; +static constexpr CGFloat const SEARCH_FIELD_WIDTH = 300; + +@interface SearchPanel () + +@property (nonatomic, strong) NSSearchField* search_field; + +@end + +@implementation SearchPanel + +- (instancetype)init +{ + if (self = [super init]) { + self.search_field = [[NSSearchField alloc] init]; + [self.search_field setPlaceholderString:@"Search"]; + [self.search_field setDelegate:self]; + + auto* search_previous = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameGoLeftTemplate] + target:self + action:@selector(findPreviousMatch:)]; + [search_previous setToolTip:@"Find Previous Match"]; + [search_previous setBordered:NO]; + + auto* search_next = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameGoRightTemplate] + target:self + action:@selector(findNextMatch:)]; + [search_next setToolTip:@"Find Next Match"]; + [search_next setBordered:NO]; + + auto* search_done = [NSButton buttonWithTitle:@"Done" + target:self + action:@selector(cancelSearch:)]; + [search_done setToolTip:@"Close Search Bar"]; + [search_done setBezelStyle:NSBezelStyleAccessoryBarAction]; + + [self addView:self.search_field inGravity:NSStackViewGravityLeading]; + [self addView:search_previous inGravity:NSStackViewGravityLeading]; + [self addView:search_next inGravity:NSStackViewGravityLeading]; + [self addView:search_done inGravity:NSStackViewGravityTrailing]; + + [self setOrientation:NSUserInterfaceLayoutOrientationHorizontal]; + [self setEdgeInsets:NSEdgeInsets { 0, 8, 0, 8 }]; + + [[self heightAnchor] constraintEqualToConstant:SEARCH_FIELD_HEIGHT].active = YES; + [[self.search_field widthAnchor] constraintEqualToConstant:SEARCH_FIELD_WIDTH].active = YES; + } + + return self; +} + +#pragma mark - Public methods + +- (void)find:(id)sender +{ + [self setHidden:NO]; + [self setSearchTextFromPasteBoard]; + + [self.window makeFirstResponder:self.search_field]; +} + +- (void)findNextMatch:(id)sender +{ + if ([self setSearchTextFromPasteBoard]) { + return; + } + + [[[self tab] web_view] findInPageNextMatch]; +} + +- (void)findPreviousMatch:(id)sender +{ + if ([self setSearchTextFromPasteBoard]) { + return; + } + + [[[self tab] web_view] findInPagePreviousMatch]; +} + +- (void)useSelectionForFind:(id)sender +{ + auto selected_text = [[[self tab] web_view] view].selected_text(); + auto* query = Ladybird::string_to_ns_string(selected_text); + + [self setPasteBoardContents:query]; + + if (![self isHidden]) { + [self.search_field setStringValue:query]; + [[[self tab] web_view] findInPage:query]; + + [self.window makeFirstResponder:self.search_field]; + } +} + +#pragma mark - Private methods + +- (Tab*)tab +{ + return (Tab*)[self window]; +} + +- (void)setPasteBoardContents:(NSString*)query +{ + auto* paste_board = [NSPasteboard pasteboardWithName:NSPasteboardNameFind]; + [paste_board clearContents]; + [paste_board setString:query forType:NSPasteboardTypeString]; +} + +- (BOOL)setSearchTextFromPasteBoard +{ + auto* paste_board = [NSPasteboard pasteboardWithName:NSPasteboardNameFind]; + auto* query = [paste_board stringForType:NSPasteboardTypeString]; + + if (query) { + if (![[self.search_field stringValue] isEqual:query]) { + [self.search_field setStringValue:query]; + [[[self tab] web_view] findInPage:query]; + + return YES; + } + } + + return NO; +} + +- (void)cancelSearch:(id)sender +{ + [self setHidden:YES]; +} + +#pragma mark - NSSearchFieldDelegate + +- (void)controlTextDidChange:(NSNotification*)notification +{ + auto* query = [self.search_field stringValue]; + [[[self tab] web_view] findInPage:query]; + + [self setPasteBoardContents:query]; +} + +- (BOOL)control:(NSControl*)control + textView:(NSTextView*)text_view + doCommandBySelector:(SEL)selector +{ + if (selector == @selector(insertNewline:)) { + NSEvent* event = [[self tab] currentEvent]; + + if ((event.modifierFlags & NSEventModifierFlagShift) == 0) { + [self findNextMatch:nil]; + } else { + [self findPreviousMatch:nil]; + } + + return YES; + } + + if (selector == @selector(cancelOperation:)) { + [self cancelSearch:nil]; + return YES; + } + + return NO; +} + +@end diff --git a/Ladybird/AppKit/UI/Tab.mm b/Ladybird/AppKit/UI/Tab.mm index 9bf907454ca..ac12f0e8cde 100644 --- a/Ladybird/AppKit/UI/Tab.mm +++ b/Ladybird/AppKit/UI/Tab.mm @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Tim Flynn + * Copyright (c) 2023-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -17,6 +17,7 @@ #import #import #import +#import #import #import #import @@ -33,6 +34,8 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; @property (nonatomic, strong) NSString* title; @property (nonatomic, strong) NSImage* favicon; +@property (nonatomic, strong) SearchPanel* search_panel; + @property (nonatomic, strong) InspectorController* inspector_controller; @end @@ -84,7 +87,10 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; [self setTitleVisibility:NSWindowTitleHidden]; [self setIsVisible:YES]; - auto* scroll_view = [[NSScrollView alloc] initWithFrame:[self frame]]; + self.search_panel = [[SearchPanel alloc] init]; + [self.search_panel setHidden:YES]; + + auto* scroll_view = [[NSScrollView alloc] init]; [scroll_view setHasVerticalScroller:YES]; [scroll_view setHasHorizontalScroller:YES]; [scroll_view setLineScroll:24]; @@ -92,13 +98,23 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; [scroll_view setContentView:self.web_view]; [scroll_view setDocumentView:[[NSView alloc] init]]; + auto* stack_view = [NSStackView stackViewWithViews:@[ + self.search_panel, + scroll_view, + ]]; + + [stack_view setOrientation:NSUserInterfaceLayoutOrientationVertical]; + [stack_view setSpacing:0]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContentScroll:) name:NSViewBoundsDidChangeNotification object:[scroll_view contentView]]; - [self setContentView:scroll_view]; + [self setContentView:stack_view]; + + [[self.search_panel leadingAnchor] constraintEqualToAnchor:[self.contentView leadingAnchor]].active = YES; } return self; @@ -106,6 +122,26 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; #pragma mark - Public methods +- (void)find:(id)sender +{ + [self.search_panel find:sender]; +} + +- (void)findNextMatch:(id)sender +{ + [self.search_panel findNextMatch:sender]; +} + +- (void)findPreviousMatch:(id)sender +{ + [self.search_panel findPreviousMatch:sender]; +} + +- (void)useSelectionForFind:(id)sender +{ + [self.search_panel useSelectionForFind:sender]; +} + - (void)tabWillClose { if (self.inspector_controller != nil) { diff --git a/Ladybird/CMakeLists.txt b/Ladybird/CMakeLists.txt index 88ccc6d22db..9a2fda902bb 100644 --- a/Ladybird/CMakeLists.txt +++ b/Ladybird/CMakeLists.txt @@ -145,6 +145,7 @@ elseif (APPLE) AppKit/UI/LadybirdWebView.mm AppKit/UI/LadybirdWebViewBridge.cpp AppKit/UI/Palette.mm + AppKit/UI/SearchPanel.mm AppKit/UI/Tab.mm AppKit/UI/TabController.mm AppKit/UI/TaskManager.mm diff --git a/Meta/gn/secondary/Ladybird/BUILD.gn b/Meta/gn/secondary/Ladybird/BUILD.gn index 4ee890a19be..f533cc6189a 100644 --- a/Meta/gn/secondary/Ladybird/BUILD.gn +++ b/Meta/gn/secondary/Ladybird/BUILD.gn @@ -126,6 +126,7 @@ executable("ladybird_executable") { "AppKit/UI/LadybirdWebView.mm", "AppKit/UI/LadybirdWebViewBridge.cpp", "AppKit/UI/Palette.mm", + "AppKit/UI/SearchPanel.mm", "AppKit/UI/Tab.mm", "AppKit/UI/TabController.mm", "AppKit/UI/TaskManager.mm",