mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-01 21:59:07 +00:00
UI/AppKit: Implement an autocomplete view for the location bar
This commit is contained in:
parent
60dd5cc4ef
commit
c1fe912bf9
Notes:
github-actions[bot]
2025-04-02 12:53:38 +00:00
Author: https://github.com/trflynn89
Commit: c1fe912bf9
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4156
4 changed files with 309 additions and 11 deletions
|
@ -2,6 +2,7 @@ add_library(ladybird_impl STATIC
|
|||
${LADYBIRD_SOURCES}
|
||||
Application/Application.mm
|
||||
Application/ApplicationDelegate.mm
|
||||
Interface/Autocomplete.mm
|
||||
Interface/Event.mm
|
||||
Interface/InfoBar.mm
|
||||
Interface/LadybirdWebView.mm
|
||||
|
|
33
UI/AppKit/Interface/Autocomplete.h
Normal file
33
UI/AppKit/Interface/Autocomplete.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@protocol AutocompleteObserver <NSObject>
|
||||
|
||||
- (void)onSelectedSuggestion:(String)suggestion;
|
||||
|
||||
@end
|
||||
|
||||
@interface Autocomplete : NSPopover
|
||||
|
||||
- (instancetype)init:(id<AutocompleteObserver>)observer
|
||||
withToolbarItem:(NSToolbarItem*)toolbar_item;
|
||||
|
||||
- (void)showWithSuggestions:(Vector<String>)suggestions;
|
||||
- (BOOL)close;
|
||||
|
||||
- (Optional<String>)selectedSuggestion;
|
||||
|
||||
- (BOOL)selectNextSuggestion;
|
||||
- (BOOL)selectPreviousSuggestion;
|
||||
|
||||
@end
|
203
UI/AppKit/Interface/Autocomplete.mm
Normal file
203
UI/AppKit/Interface/Autocomplete.mm
Normal file
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#import <Interface/Autocomplete.h>
|
||||
#import <Utilities/Conversions.h>
|
||||
|
||||
static NSString* const AUTOCOMPLETE_IDENTIFIER = @"Autocomplete";
|
||||
static constexpr auto MAX_NUMBER_OF_ROWS = 8uz;
|
||||
static constexpr auto POPOVER_PADDING = 6uz;
|
||||
|
||||
@interface Autocomplete () <NSTableViewDataSource, NSTableViewDelegate>
|
||||
{
|
||||
Vector<String> m_suggestions;
|
||||
}
|
||||
|
||||
@property (nonatomic, weak) id<AutocompleteObserver> observer;
|
||||
@property (nonatomic, weak) NSToolbarItem* toolbar_item;
|
||||
|
||||
@property (nonatomic, strong) NSTableView* table_view;
|
||||
|
||||
@end
|
||||
|
||||
@implementation Autocomplete
|
||||
|
||||
- (instancetype)init:(id<AutocompleteObserver>)observer
|
||||
withToolbarItem:(NSToolbarItem*)toolbar_item
|
||||
{
|
||||
if (self = [super init]) {
|
||||
self.observer = observer;
|
||||
self.toolbar_item = toolbar_item;
|
||||
|
||||
auto* column = [[NSTableColumn alloc] init];
|
||||
[column setEditable:NO];
|
||||
|
||||
self.table_view = [[NSTableView alloc] init];
|
||||
[self.table_view setAction:@selector(selectSuggestion:)];
|
||||
[self.table_view setBackgroundColor:[NSColor clearColor]];
|
||||
[self.table_view setIntercellSpacing:NSMakeSize(0, 5)];
|
||||
[self.table_view setHeaderView:nil];
|
||||
[self.table_view setRefusesFirstResponder:YES];
|
||||
[self.table_view setRowSizeStyle:NSTableViewRowSizeStyleDefault];
|
||||
[self.table_view addTableColumn:column];
|
||||
[self.table_view setDataSource:self];
|
||||
[self.table_view setDelegate:self];
|
||||
[self.table_view setTarget:self];
|
||||
|
||||
auto* scroll_view = [[NSScrollView alloc] init];
|
||||
[scroll_view setHasVerticalScroller:YES];
|
||||
[scroll_view setDocumentView:self.table_view];
|
||||
[scroll_view setDrawsBackground:NO];
|
||||
|
||||
auto* content_view = [[NSView alloc] init];
|
||||
[content_view addSubview:scroll_view];
|
||||
|
||||
auto* controller = [[NSViewController alloc] init];
|
||||
[controller setView:content_view];
|
||||
|
||||
[self setAnimates:NO];
|
||||
[self setBehavior:NSPopoverBehaviorTransient];
|
||||
[self setContentViewController:controller];
|
||||
[self setValue:[NSNumber numberWithBool:YES] forKeyPath:@"shouldHideAnchor"];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public methods
|
||||
|
||||
- (void)showWithSuggestions:(Vector<String>)suggestions
|
||||
{
|
||||
m_suggestions = move(suggestions);
|
||||
[self.table_view reloadData];
|
||||
|
||||
if (m_suggestions.is_empty()) {
|
||||
[self close];
|
||||
} else {
|
||||
[self show];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)close
|
||||
{
|
||||
if (!self.isShown)
|
||||
return NO;
|
||||
|
||||
[super close];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (Optional<String>)selectedSuggestion
|
||||
{
|
||||
if (!self.isShown || self.table_view.numberOfRows == 0)
|
||||
return {};
|
||||
|
||||
auto row = [self.table_view selectedRow];
|
||||
if (row < 0)
|
||||
return {};
|
||||
|
||||
return m_suggestions[row];
|
||||
}
|
||||
|
||||
- (BOOL)selectNextSuggestion
|
||||
{
|
||||
if (self.table_view.numberOfRows == 0)
|
||||
return NO;
|
||||
|
||||
if (!self.isShown) {
|
||||
[self show];
|
||||
return YES;
|
||||
}
|
||||
|
||||
[self selectRow:[self.table_view selectedRow] + 1];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)selectPreviousSuggestion
|
||||
{
|
||||
if (self.table_view.numberOfRows == 0)
|
||||
return NO;
|
||||
|
||||
if (!self.isShown) {
|
||||
[self show];
|
||||
return YES;
|
||||
}
|
||||
|
||||
[self selectRow:[self.table_view selectedRow] - 1];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)selectSuggestion:(id)sender
|
||||
{
|
||||
if (auto suggestion = [self selectedSuggestion]; suggestion.has_value())
|
||||
[self.observer onSelectedSuggestion:suggestion.release_value()];
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
- (void)show
|
||||
{
|
||||
auto height = (self.table_view.rowHeight + self.table_view.intercellSpacing.height) * min(self.table_view.numberOfRows, MAX_NUMBER_OF_ROWS);
|
||||
auto frame = NSMakeRect(0, 0, [[self.toolbar_item view] frame].size.width, height);
|
||||
|
||||
[self.table_view.enclosingScrollView setFrame:NSInsetRect(frame, 0, POPOVER_PADDING)];
|
||||
[self setContentSize:frame.size];
|
||||
|
||||
[self.table_view deselectAll:nil];
|
||||
[self.table_view scrollRowToVisible:0];
|
||||
|
||||
[self showRelativeToToolbarItem:self.toolbar_item];
|
||||
|
||||
[self showRelativeToRect:self.toolbar_item.view.frame
|
||||
ofView:self.toolbar_item.view
|
||||
preferredEdge:NSRectEdgeMaxY];
|
||||
}
|
||||
|
||||
- (void)selectRow:(NSInteger)row
|
||||
{
|
||||
if (row < 0)
|
||||
row = self.table_view.numberOfRows - 1;
|
||||
else if (row >= self.table_view.numberOfRows)
|
||||
row = 0;
|
||||
|
||||
[self.table_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
|
||||
[self.table_view scrollRowToVisible:[self.table_view selectedRow]];
|
||||
}
|
||||
|
||||
#pragma mark - NSTableViewDataSource
|
||||
|
||||
- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView
|
||||
{
|
||||
return static_cast<NSInteger>(m_suggestions.size());
|
||||
}
|
||||
|
||||
#pragma mark - NSTableViewDelegate
|
||||
|
||||
- (NSView*)tableView:(NSTableView*)table_view
|
||||
viewForTableColumn:(NSTableColumn*)table_column
|
||||
row:(NSInteger)row
|
||||
{
|
||||
NSTableCellView* view = [table_view makeViewWithIdentifier:AUTOCOMPLETE_IDENTIFIER owner:self];
|
||||
|
||||
if (view == nil) {
|
||||
view = [[NSTableCellView alloc] initWithFrame:NSZeroRect];
|
||||
|
||||
NSTextField* text_field = [[NSTextField alloc] initWithFrame:NSZeroRect];
|
||||
[text_field setBezeled:NO];
|
||||
[text_field setDrawsBackground:NO];
|
||||
[text_field setEditable:NO];
|
||||
[text_field setSelectable:NO];
|
||||
|
||||
[view addSubview:text_field];
|
||||
[view setTextField:text_field];
|
||||
[view setIdentifier:AUTOCOMPLETE_IDENTIFIER];
|
||||
}
|
||||
|
||||
[view.textField setStringValue:Ladybird::string_to_ns_string(m_suggestions[row])];
|
||||
return view;
|
||||
}
|
||||
|
||||
@end
|
|
@ -6,12 +6,15 @@
|
|||
|
||||
#include <LibWeb/Loader/UserAgent.h>
|
||||
#include <LibWebView/Application.h>
|
||||
#include <LibWebView/Autocomplete.h>
|
||||
#include <LibWebView/SearchEngine.h>
|
||||
#include <LibWebView/URL.h>
|
||||
#include <LibWebView/UserAgent.h>
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
|
||||
#import <Application/ApplicationDelegate.h>
|
||||
#import <Interface/Autocomplete.h>
|
||||
#import <Interface/Event.h>
|
||||
#import <Interface/LadybirdWebView.h>
|
||||
#import <Interface/Tab.h>
|
||||
#import <Interface/TabController.h>
|
||||
|
@ -48,7 +51,7 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
|
|||
|
||||
@end
|
||||
|
||||
@interface TabController () <NSToolbarDelegate, NSSearchFieldDelegate>
|
||||
@interface TabController () <NSToolbarDelegate, NSSearchFieldDelegate, AutocompleteObserver>
|
||||
{
|
||||
u64 m_page_index;
|
||||
|
||||
|
@ -56,6 +59,8 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
|
|||
|
||||
TabSettings m_settings;
|
||||
|
||||
OwnPtr<WebView::Autocomplete> m_autocomplete;
|
||||
|
||||
bool m_can_navigate_back;
|
||||
bool m_can_navigate_forward;
|
||||
}
|
||||
|
@ -73,6 +78,8 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
|
|||
@property (nonatomic, strong) NSToolbarItem* new_tab_toolbar_item;
|
||||
@property (nonatomic, strong) NSToolbarItem* tab_overview_toolbar_item;
|
||||
|
||||
@property (nonatomic, strong) Autocomplete* autocomplete;
|
||||
|
||||
@property (nonatomic, assign) NSLayoutConstraint* location_toolbar_item_width;
|
||||
|
||||
@end
|
||||
|
@ -91,6 +98,8 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
|
|||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
__weak TabController* weak_self = self;
|
||||
|
||||
self.toolbar = [[NSToolbar alloc] initWithIdentifier:TOOLBAR_IDENTIFIER];
|
||||
[self.toolbar setDelegate:self];
|
||||
[self.toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
|
||||
|
@ -107,6 +116,18 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
|
|||
if (auto const& user_agent_preset = WebView::Application::web_content_options().user_agent_preset; user_agent_preset.has_value())
|
||||
m_settings.user_agent_name = *user_agent_preset;
|
||||
|
||||
self.autocomplete = [[Autocomplete alloc] init:self withToolbarItem:self.location_toolbar_item];
|
||||
m_autocomplete = make<WebView::Autocomplete>();
|
||||
|
||||
m_autocomplete->on_autocomplete_query_complete = [weak_self](auto suggestions) {
|
||||
TabController* self = weak_self;
|
||||
if (self == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self.autocomplete showWithSuggestions:move(suggestions)];
|
||||
};
|
||||
|
||||
m_can_navigate_back = false;
|
||||
m_can_navigate_forward = false;
|
||||
}
|
||||
|
@ -278,6 +299,22 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
|
|||
[location_search_field setAttributedStringValue:attributed_url];
|
||||
}
|
||||
|
||||
- (BOOL)navigateToLocation:(String)location
|
||||
{
|
||||
Optional<StringView> search_engine_url;
|
||||
if (auto const& search_engine = WebView::Application::settings().search_engine(); search_engine.has_value())
|
||||
search_engine_url = search_engine->query_url;
|
||||
|
||||
if (auto url = WebView::sanitize_url(location, search_engine_url); url.has_value()) {
|
||||
[self loadURL:*url];
|
||||
}
|
||||
|
||||
[self.window makeFirstResponder:nil];
|
||||
[self.autocomplete close];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)updateNavigationButtonStates
|
||||
{
|
||||
auto* navigate_back_button = (NSButton*)[[self navigate_back_toolbar_item] view];
|
||||
|
@ -685,21 +722,30 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
|
|||
textView:(NSTextView*)text_view
|
||||
doCommandBySelector:(SEL)selector
|
||||
{
|
||||
if (selector == @selector(cancelOperation:)) {
|
||||
if ([self.autocomplete close])
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (selector == @selector(moveDown:)) {
|
||||
if ([self.autocomplete selectNextSuggestion])
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (selector == @selector(moveUp:)) {
|
||||
if ([self.autocomplete selectPreviousSuggestion])
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (selector != @selector(insertNewline:)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
auto url_string = Ladybird::ns_string_to_string([[text_view textStorage] string]);
|
||||
auto location = [self.autocomplete selectedSuggestion].value_or_lazy_evaluated([&]() {
|
||||
return Ladybird::ns_string_to_string([[text_view textStorage] string]);
|
||||
});
|
||||
|
||||
Optional<StringView> search_engine_url;
|
||||
if (auto const& search_engine = WebView::Application::settings().search_engine(); search_engine.has_value())
|
||||
search_engine_url = search_engine->query_url;
|
||||
|
||||
if (auto url = WebView::sanitize_url(url_string, search_engine_url); url.has_value()) {
|
||||
[self loadURL:*url];
|
||||
}
|
||||
|
||||
[self.window makeFirstResponder:nil];
|
||||
[self navigateToLocation:move(location)];
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
@ -711,4 +757,19 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
|
|||
[self setLocationFieldText:url_string];
|
||||
}
|
||||
|
||||
- (void)controlTextDidChange:(NSNotification*)notification
|
||||
{
|
||||
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
|
||||
|
||||
auto url_string = Ladybird::ns_string_to_string([location_search_field stringValue]);
|
||||
m_autocomplete->query_autocomplete_engine(move(url_string));
|
||||
}
|
||||
|
||||
#pragma mark - AutocompleteObserver
|
||||
|
||||
- (void)onSelectedSuggestion:(String)suggestion
|
||||
{
|
||||
[self navigateToLocation:move(suggestion)];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue