Przeglądaj źródła

Ladybird/AppKit: Implement a basic find-in-page panel

Timothy Flynn 1 rok temu
rodzic
commit
d6732e5906

+ 14 - 0
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;

+ 5 - 1
Ladybird/AppKit/UI/LadybirdWebView.h

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
+ * Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
  *
  * 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;

+ 16 - 1
Ladybird/AppKit/UI/LadybirdWebView.mm

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
+ * Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
  *
  * 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();

+ 18 - 0
Ladybird/AppKit/UI/SearchPanel.h

@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#import <System/Cocoa.h>
+
+@interface SearchPanel : NSStackView
+
+- (void)find:(id)selector;
+- (void)findNextMatch:(id)selector;
+- (void)findPreviousMatch:(id)selector;
+- (void)useSelectionForFind:(id)selector;
+
+@end

+ 182 - 0
Ladybird/AppKit/UI/SearchPanel.mm

@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <UI/LadybirdWebViewBridge.h>
+
+#import <UI/LadybirdWebView.h>
+#import <UI/SearchPanel.h>
+#import <UI/Tab.h>
+#import <Utilities/Conversions.h>
+
+#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 () <NSSearchFieldDelegate>
+
+@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

+ 39 - 3
Ladybird/AppKit/UI/Tab.mm

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
+ * Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -17,6 +17,7 @@
 #import <UI/Inspector.h>
 #import <UI/InspectorController.h>
 #import <UI/LadybirdWebView.h>
+#import <UI/SearchPanel.h>
 #import <UI/Tab.h>
 #import <UI/TabController.h>
 #import <Utilities/Conversions.h>
@@ -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) {

+ 1 - 0
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

+ 1 - 0
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",