mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-25 17:25:08 +02:00
Synthesize an extra AutocompleteSuggestion at the top of the Search Suggestions section whenever there is a configured search engine and the typed query is not URL-shaped. Use the query as the row's primary text, carry a "Search with <engine>" subtitle, and render that subtitle in the AppKit and Qt popups so the explicit search fallback stays visible and readable even when history fills the list.
759 lines
27 KiB
Plaintext
759 lines
27 KiB
Plaintext
/*
|
|
* 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 NSString* const AUTOCOMPLETE_SECTION_HEADER_IDENTIFIER = @"AutocompleteSectionHeader";
|
|
static constexpr CGFloat const POPOVER_PADDING = 8;
|
|
static constexpr CGFloat const MINIMUM_WIDTH = 100;
|
|
static constexpr CGFloat const CELL_HORIZONTAL_PADDING = 8;
|
|
static constexpr CGFloat const CELL_VERTICAL_PADDING = 10;
|
|
static constexpr CGFloat const CELL_ICON_SIZE = 16;
|
|
static constexpr CGFloat const CELL_ICON_TEXT_SPACING = 6;
|
|
static constexpr CGFloat const CELL_LABEL_VERTICAL_SPACING = 5;
|
|
static constexpr CGFloat const SECTION_HEADER_HORIZONTAL_PADDING = 10;
|
|
static constexpr CGFloat const SECTION_HEADER_VERTICAL_PADDING = 4;
|
|
|
|
static NSFont* autocomplete_primary_font();
|
|
static NSFont* autocomplete_secondary_font();
|
|
static NSFont* autocomplete_section_header_font();
|
|
|
|
enum class AutocompleteRowKind {
|
|
SectionHeader,
|
|
Suggestion,
|
|
};
|
|
|
|
struct AutocompleteRowModel {
|
|
AutocompleteRowKind kind;
|
|
String text;
|
|
size_t suggestion_index { 0 };
|
|
};
|
|
|
|
static CGFloat autocomplete_text_field_height(NSFont* font)
|
|
{
|
|
static CGFloat primary_text_field_height = 0;
|
|
static CGFloat secondary_text_field_height = 0;
|
|
static CGFloat section_header_text_field_height = 0;
|
|
static dispatch_once_t token;
|
|
dispatch_once(&token, ^{
|
|
auto* text_field = [[NSTextField alloc] initWithFrame:NSZeroRect];
|
|
[text_field setBezeled:NO];
|
|
[text_field setDrawsBackground:NO];
|
|
[text_field setEditable:NO];
|
|
[text_field setStringValue:@"Ladybird"];
|
|
|
|
[text_field setFont:autocomplete_primary_font()];
|
|
primary_text_field_height = ceil([text_field fittingSize].height);
|
|
|
|
[text_field setFont:autocomplete_secondary_font()];
|
|
secondary_text_field_height = ceil([text_field fittingSize].height);
|
|
|
|
[text_field setFont:autocomplete_section_header_font()];
|
|
section_header_text_field_height = ceil([text_field fittingSize].height);
|
|
});
|
|
|
|
if (font == autocomplete_secondary_font())
|
|
return secondary_text_field_height;
|
|
if (font == autocomplete_section_header_font())
|
|
return section_header_text_field_height;
|
|
return primary_text_field_height;
|
|
}
|
|
|
|
static CGFloat autocomplete_row_height()
|
|
{
|
|
static CGFloat row_height = 0;
|
|
static dispatch_once_t token;
|
|
dispatch_once(&token, ^{
|
|
auto content_height = max(CELL_ICON_SIZE,
|
|
autocomplete_text_field_height(autocomplete_primary_font())
|
|
+ CELL_LABEL_VERTICAL_SPACING
|
|
+ autocomplete_text_field_height(autocomplete_secondary_font()));
|
|
row_height = ceil(content_height + (CELL_VERTICAL_PADDING * 2));
|
|
});
|
|
return row_height;
|
|
}
|
|
|
|
static CGFloat autocomplete_section_header_height()
|
|
{
|
|
static CGFloat row_height = 0;
|
|
static dispatch_once_t token;
|
|
dispatch_once(&token, ^{
|
|
row_height = ceil(autocomplete_text_field_height(autocomplete_section_header_font()) + (SECTION_HEADER_VERTICAL_PADDING * 2));
|
|
});
|
|
return row_height;
|
|
}
|
|
|
|
static NSFont* autocomplete_primary_font()
|
|
{
|
|
static NSFont* font;
|
|
static dispatch_once_t token;
|
|
dispatch_once(&token, ^{
|
|
font = [NSFont systemFontOfSize:[NSFont systemFontSize] weight:NSFontWeightSemibold];
|
|
});
|
|
return font;
|
|
}
|
|
|
|
static NSFont* autocomplete_secondary_font()
|
|
{
|
|
static NSFont* font;
|
|
static dispatch_once_t token;
|
|
dispatch_once(&token, ^{
|
|
font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
|
|
});
|
|
return font;
|
|
}
|
|
|
|
static NSFont* autocomplete_section_header_font()
|
|
{
|
|
static NSFont* font;
|
|
static dispatch_once_t token;
|
|
dispatch_once(&token, ^{
|
|
font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize] weight:NSFontWeightSemibold];
|
|
});
|
|
return font;
|
|
}
|
|
|
|
static NSImage* search_suggestion_icon()
|
|
{
|
|
static NSImage* image;
|
|
static dispatch_once_t token;
|
|
dispatch_once(&token, ^{
|
|
image = [NSImage imageWithSystemSymbolName:@"magnifyingglass" accessibilityDescription:@""];
|
|
[image setSize:NSMakeSize(CELL_ICON_SIZE, CELL_ICON_SIZE)];
|
|
});
|
|
return image;
|
|
}
|
|
|
|
static NSImage* literal_url_suggestion_icon()
|
|
{
|
|
static NSImage* image;
|
|
static dispatch_once_t token;
|
|
dispatch_once(&token, ^{
|
|
image = [NSImage imageWithSystemSymbolName:@"globe" accessibilityDescription:@""];
|
|
[image setSize:NSMakeSize(CELL_ICON_SIZE, CELL_ICON_SIZE)];
|
|
});
|
|
return image;
|
|
}
|
|
|
|
@protocol AutocompleteTableViewHoverObserver <NSObject>
|
|
|
|
- (void)autocompleteTableViewHoveredRowChanged:(NSInteger)row;
|
|
|
|
@end
|
|
|
|
@interface AutocompleteRowView : NSTableRowView
|
|
@end
|
|
|
|
@implementation AutocompleteRowView
|
|
|
|
- (void)drawSelectionInRect:(NSRect)dirtyRect
|
|
{
|
|
auto selection_rect = NSInsetRect(self.bounds, 2, 3);
|
|
auto* selection_path = [NSBezierPath bezierPathWithRoundedRect:selection_rect xRadius:6 yRadius:6];
|
|
|
|
[[[NSColor controlAccentColor] colorWithAlphaComponent:0.25] setFill];
|
|
[selection_path fill];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface AutocompleteSuggestionView : NSTableCellView
|
|
|
|
@property (nonatomic, strong) NSImageView* icon_view;
|
|
@property (nonatomic, strong) NSTextField* title_text_field;
|
|
@property (nonatomic, strong) NSTextField* url_text_field;
|
|
|
|
@end
|
|
|
|
@implementation AutocompleteSuggestionView
|
|
@end
|
|
|
|
@interface AutocompleteSectionHeaderView : NSTableCellView
|
|
|
|
@property (nonatomic, strong) NSTextField* text_field;
|
|
|
|
@end
|
|
|
|
@implementation AutocompleteSectionHeaderView
|
|
@end
|
|
|
|
@interface AutocompleteScrollView : NSScrollView
|
|
@end
|
|
|
|
@implementation AutocompleteScrollView
|
|
|
|
- (void)scrollWheel:(NSEvent*)event
|
|
{
|
|
}
|
|
|
|
@end
|
|
|
|
@interface AutocompleteTableView : NSTableView
|
|
|
|
@property (nonatomic, weak) id<AutocompleteTableViewHoverObserver> hover_observer;
|
|
@property (nonatomic, strong) NSTrackingArea* tracking_area;
|
|
|
|
@end
|
|
|
|
@implementation AutocompleteTableView
|
|
|
|
- (void)updateTrackingAreas
|
|
{
|
|
if (self.tracking_area != nil)
|
|
[self removeTrackingArea:self.tracking_area];
|
|
|
|
self.tracking_area = [[NSTrackingArea alloc] initWithRect:NSZeroRect
|
|
options:NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited
|
|
owner:self
|
|
userInfo:nil];
|
|
[self addTrackingArea:self.tracking_area];
|
|
|
|
[super updateTrackingAreas];
|
|
}
|
|
|
|
- (void)mouseMoved:(NSEvent*)event
|
|
{
|
|
[super mouseMoved:event];
|
|
|
|
auto point = [self convertPoint:event.locationInWindow fromView:nil];
|
|
[self.hover_observer autocompleteTableViewHoveredRowChanged:[self rowAtPoint:point]];
|
|
}
|
|
|
|
- (void)mouseExited:(NSEvent*)event
|
|
{
|
|
[super mouseExited:event];
|
|
[self.hover_observer autocompleteTableViewHoveredRowChanged:-1];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface AutocompleteWindow : NSWindow
|
|
@end
|
|
|
|
@implementation AutocompleteWindow
|
|
|
|
- (BOOL)canBecomeKeyWindow
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)canBecomeMainWindow
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface Autocomplete () <AutocompleteTableViewHoverObserver, NSTableViewDataSource, NSTableViewDelegate>
|
|
{
|
|
Vector<WebView::AutocompleteSuggestion> m_suggestions;
|
|
Vector<AutocompleteRowModel> m_rows;
|
|
}
|
|
|
|
@property (nonatomic, weak) id<AutocompleteObserver> observer;
|
|
@property (nonatomic, weak) NSToolbarItem* toolbar_item;
|
|
|
|
@property (nonatomic, strong) AutocompleteWindow* popup_window;
|
|
@property (nonatomic, strong) NSView* content_view;
|
|
@property (nonatomic, strong) NSScrollView* scroll_view;
|
|
@property (nonatomic, strong) NSTableView* table_view;
|
|
@property (nonatomic, strong) NSMutableDictionary<NSString*, NSImage*>* suggestion_icons;
|
|
|
|
- (NSInteger)tableRowForSuggestionIndex:(NSInteger)suggestion_index;
|
|
- (BOOL)isSelectableRow:(NSInteger)row;
|
|
- (CGFloat)heightOfRowAtIndex:(size_t)row;
|
|
- (CGFloat)tableHeightForVisibleSuggestionCount:(size_t)visible_suggestion_count;
|
|
- (void)rebuildRows;
|
|
- (void)selectRow:(NSInteger)row notifyObserver:(BOOL)notify_observer;
|
|
- (NSInteger)stepToSelectableRowFrom:(NSInteger)row direction:(NSInteger)direction;
|
|
|
|
@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 = [[AutocompleteTableView alloc] initWithFrame:NSZeroRect];
|
|
[self.table_view setAction:@selector(selectSuggestion:)];
|
|
[self.table_view setBackgroundColor:[NSColor clearColor]];
|
|
[self.table_view setHeaderView:nil];
|
|
[self.table_view setIntercellSpacing:NSMakeSize(0, 0)];
|
|
[self.table_view setRefusesFirstResponder:YES];
|
|
[self.table_view setStyle:NSTableViewStyleFullWidth];
|
|
[self.table_view setRowSizeStyle:NSTableViewRowSizeStyleCustom];
|
|
[self.table_view setRowHeight:autocomplete_row_height()];
|
|
[self.table_view setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleRegular];
|
|
[self.table_view addTableColumn:column];
|
|
[self.table_view setDataSource:self];
|
|
[self.table_view setDelegate:self];
|
|
[self.table_view setTarget:self];
|
|
[(AutocompleteTableView*)self.table_view setHover_observer:self];
|
|
|
|
self.scroll_view = [[AutocompleteScrollView alloc] initWithFrame:NSZeroRect];
|
|
[self.scroll_view setAutohidesScrollers:YES];
|
|
[self.scroll_view setBorderType:NSNoBorder];
|
|
[self.scroll_view setDrawsBackground:NO];
|
|
[self.scroll_view setHasHorizontalScroller:NO];
|
|
[self.scroll_view setHasVerticalScroller:NO];
|
|
[self.scroll_view setHorizontalScrollElasticity:NSScrollElasticityNone];
|
|
[self.scroll_view setVerticalScrollElasticity:NSScrollElasticityNone];
|
|
[self.scroll_view setDocumentView:self.table_view];
|
|
|
|
self.content_view = [[NSView alloc] initWithFrame:NSZeroRect];
|
|
[self.content_view setWantsLayer:YES];
|
|
[self.content_view.layer setBackgroundColor:[NSColor windowBackgroundColor].CGColor];
|
|
[self.content_view.layer setCornerRadius:8];
|
|
[self.content_view addSubview:self.scroll_view];
|
|
self.suggestion_icons = [NSMutableDictionary dictionary];
|
|
|
|
self.popup_window = [[AutocompleteWindow alloc] initWithContentRect:NSZeroRect
|
|
styleMask:NSWindowStyleMaskBorderless
|
|
backing:NSBackingStoreBuffered
|
|
defer:NO];
|
|
[self.popup_window setAcceptsMouseMovedEvents:YES];
|
|
[self.popup_window setBackgroundColor:[NSColor clearColor]];
|
|
[self.popup_window setContentView:self.content_view];
|
|
[self.popup_window setHasShadow:YES];
|
|
[self.popup_window setLevel:NSPopUpMenuWindowLevel];
|
|
[self.popup_window setOpaque:NO];
|
|
[self.popup_window setReleasedWhenClosed:NO];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - Public methods
|
|
|
|
- (void)showWithSuggestions:(Vector<WebView::AutocompleteSuggestion>)suggestions
|
|
selectedRow:(NSInteger)selected_row
|
|
{
|
|
m_suggestions = move(suggestions);
|
|
[self rebuildRows];
|
|
[self.suggestion_icons removeAllObjects];
|
|
|
|
for (auto const& suggestion : m_suggestions) {
|
|
if (suggestion.favicon_base64_png.has_value()) {
|
|
auto* suggestion_text = Ladybird::string_to_ns_string(suggestion.text);
|
|
if (auto* favicon = Ladybird::image_from_base64_png(*suggestion.favicon_base64_png, NSMakeSize(CELL_ICON_SIZE, CELL_ICON_SIZE)); favicon != nil)
|
|
[self.suggestion_icons setObject:favicon forKey:suggestion_text];
|
|
}
|
|
}
|
|
|
|
[self.table_view reloadData];
|
|
|
|
if (m_rows.is_empty())
|
|
[self close];
|
|
else
|
|
[self show];
|
|
|
|
auto table_row = [self tableRowForSuggestionIndex:selected_row];
|
|
if (table_row == NSNotFound)
|
|
[self clearSelection];
|
|
else if (table_row != self.table_view.selectedRow) {
|
|
// Refreshing the default row should not behave like an explicit
|
|
// highlight, or the location field will re-preview the suggestion.
|
|
[self selectRow:table_row notifyObserver:NO];
|
|
}
|
|
}
|
|
|
|
- (BOOL)close
|
|
{
|
|
if (!self.popup_window.isVisible)
|
|
return NO;
|
|
|
|
if (auto* parent_window = [self.toolbar_item.view window])
|
|
[parent_window removeChildWindow:self.popup_window];
|
|
|
|
[self.popup_window orderOut:nil];
|
|
[self.observer onAutocompleteDidClose];
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isVisible
|
|
{
|
|
return self.popup_window.isVisible;
|
|
}
|
|
|
|
- (void)clearSelection
|
|
{
|
|
[self.table_view deselectAll:nil];
|
|
}
|
|
|
|
- (Optional<String>)selectedSuggestion
|
|
{
|
|
if (!self.popup_window.isVisible || self.table_view.numberOfRows == 0)
|
|
return {};
|
|
|
|
auto row = [self.table_view selectedRow];
|
|
if (![self isSelectableRow:row])
|
|
return {};
|
|
|
|
return m_suggestions[m_rows[row].suggestion_index].text;
|
|
}
|
|
|
|
- (BOOL)selectNextSuggestion
|
|
{
|
|
if (self.table_view.numberOfRows == 0)
|
|
return NO;
|
|
|
|
if (!self.popup_window.isVisible) {
|
|
[self show];
|
|
if (auto row = [self stepToSelectableRowFrom:-1 direction:1]; row != NSNotFound)
|
|
[self selectRow:row notifyObserver:YES];
|
|
return YES;
|
|
}
|
|
|
|
if (auto row = [self stepToSelectableRowFrom:[self.table_view selectedRow] direction:1]; row != NSNotFound)
|
|
[self selectRow:row notifyObserver:YES];
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)selectPreviousSuggestion
|
|
{
|
|
if (self.table_view.numberOfRows == 0)
|
|
return NO;
|
|
|
|
if (!self.popup_window.isVisible) {
|
|
[self show];
|
|
if (auto row = [self stepToSelectableRowFrom:0 direction:-1]; row != NSNotFound)
|
|
[self selectRow:row notifyObserver:YES];
|
|
return YES;
|
|
}
|
|
|
|
if (auto row = [self stepToSelectableRowFrom:[self.table_view selectedRow] direction:-1]; row != NSNotFound)
|
|
[self selectRow:row notifyObserver:YES];
|
|
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)rebuildRows
|
|
{
|
|
m_rows.clear();
|
|
m_rows.ensure_capacity(m_suggestions.size() * 2);
|
|
|
|
auto current_section = WebView::AutocompleteSuggestionSection::None;
|
|
for (size_t suggestion_index = 0; suggestion_index < m_suggestions.size(); ++suggestion_index) {
|
|
auto const& suggestion = m_suggestions[suggestion_index];
|
|
if (suggestion.section != WebView::AutocompleteSuggestionSection::None && suggestion.section != current_section) {
|
|
current_section = suggestion.section;
|
|
m_rows.append({
|
|
.kind = AutocompleteRowKind::SectionHeader,
|
|
.text = MUST(String::from_utf8(WebView::autocomplete_section_title(current_section))),
|
|
});
|
|
}
|
|
|
|
m_rows.append({ .kind = AutocompleteRowKind::Suggestion, .suggestion_index = suggestion_index });
|
|
}
|
|
}
|
|
|
|
- (BOOL)isSelectableRow:(NSInteger)row
|
|
{
|
|
if (row < 0 || row >= static_cast<NSInteger>(m_rows.size()))
|
|
return NO;
|
|
return m_rows[row].kind == AutocompleteRowKind::Suggestion;
|
|
}
|
|
|
|
- (NSInteger)tableRowForSuggestionIndex:(NSInteger)suggestion_index
|
|
{
|
|
if (suggestion_index < 0)
|
|
return NSNotFound;
|
|
|
|
for (size_t row = 0; row < m_rows.size(); ++row) {
|
|
auto const& row_model = m_rows[row];
|
|
if (row_model.kind == AutocompleteRowKind::Suggestion
|
|
&& row_model.suggestion_index == static_cast<size_t>(suggestion_index))
|
|
return static_cast<NSInteger>(row);
|
|
}
|
|
|
|
return NSNotFound;
|
|
}
|
|
|
|
- (CGFloat)heightOfRowAtIndex:(size_t)row
|
|
{
|
|
VERIFY(row < m_rows.size());
|
|
return m_rows[row].kind == AutocompleteRowKind::SectionHeader
|
|
? autocomplete_section_header_height()
|
|
: autocomplete_row_height();
|
|
}
|
|
|
|
- (CGFloat)tableHeightForVisibleSuggestionCount:(size_t)visible_suggestion_count
|
|
{
|
|
if (visible_suggestion_count == 0)
|
|
return 0;
|
|
|
|
CGFloat total_height = 0;
|
|
size_t seen_suggestion_count = 0;
|
|
|
|
for (size_t row = 0; row < m_rows.size(); ++row) {
|
|
total_height += [self heightOfRowAtIndex:row];
|
|
if (m_rows[row].kind == AutocompleteRowKind::Suggestion) {
|
|
++seen_suggestion_count;
|
|
if (seen_suggestion_count >= visible_suggestion_count)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ceil(total_height);
|
|
}
|
|
|
|
- (NSInteger)stepToSelectableRowFrom:(NSInteger)row direction:(NSInteger)direction
|
|
{
|
|
if (self.table_view.numberOfRows == 0)
|
|
return NSNotFound;
|
|
|
|
auto candidate = row;
|
|
for (NSInteger attempt = 0; attempt < self.table_view.numberOfRows; ++attempt) {
|
|
candidate += direction;
|
|
if (candidate < 0)
|
|
candidate = self.table_view.numberOfRows - 1;
|
|
else if (candidate >= self.table_view.numberOfRows)
|
|
candidate = 0;
|
|
|
|
if ([self isSelectableRow:candidate])
|
|
return candidate;
|
|
}
|
|
|
|
return NSNotFound;
|
|
}
|
|
|
|
- (void)autocompleteTableViewHoveredRowChanged:(NSInteger)row
|
|
{
|
|
if (![self isSelectableRow:row])
|
|
return;
|
|
|
|
if (row == self.table_view.selectedRow)
|
|
return;
|
|
|
|
[self selectRow:row notifyObserver:YES];
|
|
}
|
|
|
|
- (void)show
|
|
{
|
|
auto* toolbar_view = self.toolbar_item.view;
|
|
auto* parent_window = [toolbar_view window];
|
|
if (parent_window == nil)
|
|
return;
|
|
auto was_visible = self.popup_window.isVisible;
|
|
|
|
size_t visible_suggestion_count = 0;
|
|
for (auto const& row_model : m_rows) {
|
|
if (row_model.kind == AutocompleteRowKind::Suggestion)
|
|
++visible_suggestion_count;
|
|
}
|
|
|
|
auto visible_table_height = [self tableHeightForVisibleSuggestionCount:visible_suggestion_count];
|
|
auto width = max<CGFloat>(toolbar_view.frame.size.width, MINIMUM_WIDTH);
|
|
auto content_size = NSMakeSize(width, visible_table_height + (POPOVER_PADDING * 2));
|
|
|
|
[self.content_view setFrame:NSMakeRect(0, 0, content_size.width, content_size.height)];
|
|
[self.scroll_view setFrame:NSInsetRect(self.content_view.bounds, 0, POPOVER_PADDING)];
|
|
|
|
CGFloat document_width = self.scroll_view.contentSize.width;
|
|
[self.table_view setFrame:NSMakeRect(0, 0, document_width, visible_table_height)];
|
|
|
|
if (auto* column = self.table_view.tableColumns.firstObject)
|
|
[column setWidth:document_width];
|
|
|
|
if (!was_visible)
|
|
[self.table_view deselectAll:nil];
|
|
[self.table_view scrollRowToVisible:self.table_view.selectedRow >= 0 ? self.table_view.selectedRow : 0];
|
|
|
|
auto anchor_rect = [toolbar_view convertRect:toolbar_view.bounds toView:nil];
|
|
auto popup_rect = [parent_window convertRectToScreen:anchor_rect];
|
|
popup_rect.origin.y -= content_size.height;
|
|
popup_rect.size = content_size;
|
|
|
|
[self.popup_window setFrame:popup_rect display:NO];
|
|
|
|
if (!was_visible)
|
|
[parent_window addChildWindow:self.popup_window ordered:NSWindowAbove];
|
|
|
|
[self.popup_window orderFront:nil];
|
|
}
|
|
|
|
- (void)selectRow:(NSInteger)row
|
|
notifyObserver:(BOOL)notify_observer
|
|
{
|
|
if (![self isSelectableRow:row])
|
|
return;
|
|
|
|
[self.table_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
|
|
[self.table_view scrollRowToVisible:[self.table_view selectedRow]];
|
|
|
|
if (notify_observer) {
|
|
if (auto suggestion = [self selectedSuggestion]; suggestion.has_value())
|
|
[self.observer onHighlightedSuggestion:suggestion.release_value()];
|
|
}
|
|
}
|
|
|
|
#pragma mark - NSTableViewDataSource
|
|
|
|
- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView
|
|
{
|
|
return static_cast<NSInteger>(m_rows.size());
|
|
}
|
|
|
|
#pragma mark - NSTableViewDelegate
|
|
|
|
- (CGFloat)tableView:(NSTableView*)tableView heightOfRow:(NSInteger)row
|
|
{
|
|
return [self heightOfRowAtIndex:static_cast<size_t>(row)];
|
|
}
|
|
|
|
- (NSTableRowView*)tableView:(NSTableView*)tableView
|
|
rowViewForRow:(NSInteger)row
|
|
{
|
|
return [[AutocompleteRowView alloc] initWithFrame:NSMakeRect(0, 0, NSWidth(tableView.bounds), [self tableView:tableView heightOfRow:row])];
|
|
}
|
|
|
|
- (NSView*)tableView:(NSTableView*)table_view
|
|
viewForTableColumn:(NSTableColumn*)table_column
|
|
row:(NSInteger)row
|
|
{
|
|
auto const& row_model = m_rows[row];
|
|
if (row_model.kind == AutocompleteRowKind::SectionHeader) {
|
|
AutocompleteSectionHeaderView* view = (AutocompleteSectionHeaderView*)[table_view makeViewWithIdentifier:AUTOCOMPLETE_SECTION_HEADER_IDENTIFIER owner:self];
|
|
|
|
if (view == nil) {
|
|
view = [[AutocompleteSectionHeaderView alloc] initWithFrame:NSZeroRect];
|
|
|
|
NSTextField* text_field = [[NSTextField alloc] initWithFrame:NSZeroRect];
|
|
[text_field setBezeled:NO];
|
|
[text_field setDrawsBackground:NO];
|
|
[text_field setEditable:NO];
|
|
[text_field setFont:autocomplete_section_header_font()];
|
|
[text_field setSelectable:NO];
|
|
[view addSubview:text_field];
|
|
[view setText_field:text_field];
|
|
[view setIdentifier:AUTOCOMPLETE_SECTION_HEADER_IDENTIFIER];
|
|
}
|
|
|
|
auto* header_text = Ladybird::string_to_ns_string(row_model.text);
|
|
auto header_height = autocomplete_text_field_height(autocomplete_section_header_font());
|
|
[view setFrame:NSMakeRect(0, 0, NSWidth(table_view.bounds), [self tableView:table_view heightOfRow:row])];
|
|
[view.text_field setStringValue:header_text];
|
|
[view.text_field setTextColor:[NSColor tertiaryLabelColor]];
|
|
[view.text_field setFrame:NSMakeRect(
|
|
SECTION_HEADER_HORIZONTAL_PADDING,
|
|
floor((NSHeight(view.bounds) - header_height) / 2.f),
|
|
NSWidth(view.bounds) - (SECTION_HEADER_HORIZONTAL_PADDING * 2),
|
|
header_height)];
|
|
return view;
|
|
}
|
|
|
|
AutocompleteSuggestionView* view = (AutocompleteSuggestionView*)[table_view makeViewWithIdentifier:AUTOCOMPLETE_IDENTIFIER owner:self];
|
|
|
|
if (view == nil) {
|
|
view = [[AutocompleteSuggestionView alloc] initWithFrame:NSZeroRect];
|
|
|
|
NSImageView* icon_view = [[NSImageView alloc] initWithFrame:NSZeroRect];
|
|
[icon_view setImageScaling:NSImageScaleProportionallyDown];
|
|
[view addSubview:icon_view];
|
|
[view setIcon_view:icon_view];
|
|
|
|
NSTextField* title_text_field = [[NSTextField alloc] initWithFrame:NSZeroRect];
|
|
[title_text_field setBezeled:NO];
|
|
[title_text_field setDrawsBackground:NO];
|
|
[title_text_field setEditable:NO];
|
|
[title_text_field setFont:autocomplete_primary_font()];
|
|
[title_text_field setLineBreakMode:NSLineBreakByTruncatingTail];
|
|
[title_text_field setSelectable:NO];
|
|
[view addSubview:title_text_field];
|
|
[view setTitle_text_field:title_text_field];
|
|
|
|
NSTextField* url_text_field = [[NSTextField alloc] initWithFrame:NSZeroRect];
|
|
[url_text_field setBezeled:NO];
|
|
[url_text_field setDrawsBackground:NO];
|
|
[url_text_field setEditable:NO];
|
|
[url_text_field setLineBreakMode:NSLineBreakByTruncatingTail];
|
|
[url_text_field setSelectable:NO];
|
|
[view addSubview:url_text_field];
|
|
[view setUrl_text_field:url_text_field];
|
|
|
|
[view setIdentifier:AUTOCOMPLETE_IDENTIFIER];
|
|
}
|
|
|
|
auto const& suggestion = m_suggestions[row_model.suggestion_index];
|
|
auto* suggestion_text = Ladybird::string_to_ns_string(suggestion.text);
|
|
auto* title_text = suggestion.title.has_value() ? Ladybird::string_to_ns_string(*suggestion.title) : nil;
|
|
auto* secondary_text = suggestion.subtitle.has_value() ? Ladybird::string_to_ns_string(*suggestion.subtitle) : suggestion_text;
|
|
auto* favicon = [self.suggestion_icons objectForKey:suggestion_text];
|
|
auto* icon = suggestion.source == WebView::AutocompleteSuggestionSource::LiteralURL
|
|
? literal_url_suggestion_icon()
|
|
: suggestion.source == WebView::AutocompleteSuggestionSource::Search ? search_suggestion_icon()
|
|
: favicon;
|
|
|
|
[view setFrame:NSMakeRect(0, 0, NSWidth(table_view.bounds), [self tableView:table_view heightOfRow:row])];
|
|
|
|
auto primary_text_height = autocomplete_text_field_height(autocomplete_primary_font());
|
|
auto secondary_text_height = autocomplete_text_field_height(autocomplete_secondary_font());
|
|
CGFloat text_origin_x = CELL_HORIZONTAL_PADDING + CELL_ICON_SIZE + CELL_ICON_TEXT_SPACING;
|
|
CGFloat text_width = NSWidth(view.bounds) - text_origin_x - CELL_HORIZONTAL_PADDING;
|
|
|
|
[view.icon_view setFrame:NSMakeRect(
|
|
CELL_HORIZONTAL_PADDING,
|
|
floor((NSHeight(view.bounds) - CELL_ICON_SIZE) / 2.f),
|
|
CELL_ICON_SIZE,
|
|
CELL_ICON_SIZE)];
|
|
[view.icon_view setImage:icon];
|
|
[view.icon_view setContentTintColor:suggestion.source != WebView::AutocompleteSuggestionSource::History ? [NSColor secondaryLabelColor] : nil];
|
|
[view.icon_view setHidden:(icon == nil)];
|
|
|
|
if (title_text != nil) {
|
|
CGFloat text_block_height = primary_text_height + CELL_LABEL_VERTICAL_SPACING + secondary_text_height;
|
|
CGFloat text_block_origin_y = floor((NSHeight(view.bounds) - text_block_height) / 2.f);
|
|
|
|
[view.title_text_field setHidden:NO];
|
|
[view.title_text_field setStringValue:title_text];
|
|
[view.title_text_field setTextColor:[NSColor textColor]];
|
|
[view.title_text_field setFrame:NSMakeRect(
|
|
text_origin_x,
|
|
text_block_origin_y + secondary_text_height + CELL_LABEL_VERTICAL_SPACING,
|
|
text_width,
|
|
primary_text_height)];
|
|
|
|
[view.url_text_field setFont:autocomplete_secondary_font()];
|
|
[view.url_text_field setTextColor:[NSColor secondaryLabelColor]];
|
|
[view.url_text_field setFrame:NSMakeRect(
|
|
text_origin_x,
|
|
text_block_origin_y,
|
|
text_width,
|
|
secondary_text_height)];
|
|
} else {
|
|
[view.title_text_field setHidden:YES];
|
|
|
|
[view.url_text_field setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]];
|
|
[view.url_text_field setTextColor:[NSColor textColor]];
|
|
[view.url_text_field setFrame:NSMakeRect(
|
|
text_origin_x,
|
|
floor((NSHeight(view.bounds) - primary_text_height) / 2.f),
|
|
text_width,
|
|
primary_text_height)];
|
|
}
|
|
|
|
[view.url_text_field setStringValue:(title_text != nil ? secondary_text : suggestion_text)];
|
|
return view;
|
|
}
|
|
|
|
@end
|