UI: Show page loading state in the location field
Use the leading icon in the location field as a contextual page indicator. While a page is loading, the field shows an indeterminate activity indicator. Once loading finishes, it shows the page favicon when available and falls back to the generic page icon where the toolkit has one. Keep the GTK implementation aligned with the other frontends by using that slot only for loading and favicon state, without a separate security icon. This keeps single-tab windows from losing all visual feedback when the tab bar is hidden, without adding a fake progress bar.
This commit is contained in:
parent
d20caf48fd
commit
0a70dadfb1
12 changed files with 365 additions and 35 deletions
|
|
@ -263,11 +263,13 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800;
|
|||
self.favicon = [Tab defaultFavicon];
|
||||
[self updateTabTitleAndFavicon];
|
||||
|
||||
[[self tabController] onFaviconChange:nil];
|
||||
[[self tabController] onLoadStart:url isRedirect:is_redirect];
|
||||
}
|
||||
|
||||
- (void)onLoadFinish:(URL::URL const&)url
|
||||
{
|
||||
[[self tabController] onLoadFinish:url];
|
||||
}
|
||||
|
||||
- (void)onURLChange:(URL::URL const&)url
|
||||
|
|
@ -287,6 +289,7 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800;
|
|||
[favicon setResizingMode:NSImageResizingModeStretch];
|
||||
self.favicon = favicon;
|
||||
[self updateTabTitleAndFavicon];
|
||||
[[self tabController] onFaviconChange:favicon];
|
||||
}
|
||||
|
||||
- (BookmarksBar*)bookmarksBar
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@
|
|||
- (void)loadURL:(URL::URL const&)url;
|
||||
|
||||
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)isRedirect;
|
||||
- (void)onLoadFinish:(URL::URL const&)url;
|
||||
- (void)onFaviconChange:(NSImage*)favicon;
|
||||
|
||||
- (void)onURLChange:(URL::URL const&)url;
|
||||
|
||||
|
|
|
|||
|
|
@ -157,14 +157,50 @@ static NSInteger autocomplete_suggestion_index(NSString* suggestion_text, Vector
|
|||
return NSNotFound;
|
||||
}
|
||||
|
||||
static NSImage* location_field_globe_icon()
|
||||
{
|
||||
static NSImage* image;
|
||||
static dispatch_once_t token;
|
||||
dispatch_once(&token, ^{
|
||||
image = [NSImage imageWithSystemSymbolName:@"globe" accessibilityDescription:@"Page icon"];
|
||||
[image setSize:NSMakeSize(16, 16)];
|
||||
});
|
||||
return image;
|
||||
}
|
||||
|
||||
@interface LocationSearchField : NSSearchField
|
||||
{
|
||||
BOOL m_loading;
|
||||
BOOL m_shows_page_icon;
|
||||
NSImage* m_favicon;
|
||||
NSProgressIndicator* m_loading_indicator;
|
||||
}
|
||||
|
||||
- (BOOL)becomeFirstResponder;
|
||||
- (void)setLoading:(BOOL)loading;
|
||||
- (void)setFavicon:(NSImage*)favicon;
|
||||
- (void)setShowsPageIcon:(BOOL)showsPageIcon;
|
||||
|
||||
@end
|
||||
|
||||
@implementation LocationSearchField
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
m_loading_indicator = [[NSProgressIndicator alloc] init];
|
||||
[m_loading_indicator setStyle:NSProgressIndicatorStyleSpinning];
|
||||
[m_loading_indicator setControlSize:NSControlSizeSmall];
|
||||
[m_loading_indicator setDisplayedWhenStopped:NO];
|
||||
[m_loading_indicator setHidden:YES];
|
||||
[self addSubview:m_loading_indicator];
|
||||
|
||||
[self updateLeadingIcon];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
BOOL result = [super becomeFirstResponder];
|
||||
|
|
@ -173,6 +209,68 @@ static NSInteger autocomplete_suggestion_index(NSString* suggestion_text, Vector
|
|||
return result;
|
||||
}
|
||||
|
||||
- (void)layout
|
||||
{
|
||||
[super layout];
|
||||
|
||||
auto* cell = (NSSearchFieldCell*)[self cell];
|
||||
auto search_button_rect = [cell searchButtonRectForBounds:[self bounds]];
|
||||
auto indicator_size = NSMakeSize(16, 16);
|
||||
[m_loading_indicator setFrame:NSMakeRect(
|
||||
NSMidX(search_button_rect) - indicator_size.width / 2,
|
||||
NSMidY(search_button_rect) - indicator_size.height / 2,
|
||||
indicator_size.width,
|
||||
indicator_size.height)];
|
||||
}
|
||||
|
||||
- (void)setLoading:(BOOL)loading
|
||||
{
|
||||
if (m_loading == loading)
|
||||
return;
|
||||
|
||||
m_loading = loading;
|
||||
if (m_loading) {
|
||||
[m_loading_indicator setHidden:NO];
|
||||
[m_loading_indicator startAnimation:self];
|
||||
} else {
|
||||
[m_loading_indicator stopAnimation:self];
|
||||
[m_loading_indicator setHidden:YES];
|
||||
}
|
||||
|
||||
[self updateLeadingIcon];
|
||||
}
|
||||
|
||||
- (void)setFavicon:(NSImage*)favicon
|
||||
{
|
||||
m_favicon = [favicon copy];
|
||||
[m_favicon setSize:NSMakeSize(16, 16)];
|
||||
[m_favicon setTemplate:NO];
|
||||
[self updateLeadingIcon];
|
||||
}
|
||||
|
||||
- (void)setShowsPageIcon:(BOOL)showsPageIcon
|
||||
{
|
||||
m_shows_page_icon = showsPageIcon;
|
||||
[self updateLeadingIcon];
|
||||
}
|
||||
|
||||
- (void)updateLeadingIcon
|
||||
{
|
||||
auto* cell = (NSSearchFieldCell*)[self cell];
|
||||
auto* search_button = [cell searchButtonCell];
|
||||
|
||||
if (m_loading) {
|
||||
[search_button setImage:nil];
|
||||
} else if (m_shows_page_icon && m_favicon != nil) {
|
||||
[search_button setImage:m_favicon];
|
||||
} else if (!m_shows_page_icon) {
|
||||
[search_button setImage:[NSImage imageWithSystemSymbolName:@"magnifyingglass"
|
||||
accessibilityDescription:@"Search"]];
|
||||
} else {
|
||||
[search_button setImage:location_field_globe_icon()];
|
||||
}
|
||||
}
|
||||
|
||||
// NSSearchField does not provide an intrinsic width, which causes an ambiguous layout warning when the toolbar auto-
|
||||
// measures this view. This provides an initial fallback, which is overridden with an explicit width in windowDidResize.
|
||||
- (NSSize)intrinsicContentSize
|
||||
|
|
@ -322,6 +420,19 @@ static NSInteger autocomplete_suggestion_index(NSString* suggestion_text, Vector
|
|||
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)isRedirect
|
||||
{
|
||||
[self setLocationFieldText:url.serialize()];
|
||||
[(LocationSearchField*)[self.location_toolbar_item view] setFavicon:nil];
|
||||
[(LocationSearchField*)[self.location_toolbar_item view] setLoading:YES];
|
||||
}
|
||||
|
||||
- (void)onLoadFinish:(URL::URL const&)url
|
||||
{
|
||||
(void)url;
|
||||
[(LocationSearchField*)[self.location_toolbar_item view] setLoading:NO];
|
||||
}
|
||||
|
||||
- (void)onFaviconChange:(NSImage*)favicon
|
||||
{
|
||||
[(LocationSearchField*)[self.location_toolbar_item view] setFavicon:favicon];
|
||||
}
|
||||
|
||||
- (void)onURLChange:(URL::URL const&)url
|
||||
|
|
@ -390,6 +501,7 @@ static NSInteger autocomplete_suggestion_index(NSString* suggestion_text, Vector
|
|||
- (void)setLocationFieldText:(StringView)url
|
||||
{
|
||||
NSMutableAttributedString* attributed_url;
|
||||
auto url_parts = WebView::break_url_into_parts(url);
|
||||
|
||||
auto* dark_attributes = @{
|
||||
NSForegroundColorAttributeName : [NSColor systemGrayColor],
|
||||
|
|
@ -398,7 +510,7 @@ static NSInteger autocomplete_suggestion_index(NSString* suggestion_text, Vector
|
|||
NSForegroundColorAttributeName : [NSColor textColor],
|
||||
};
|
||||
|
||||
if (auto url_parts = WebView::break_url_into_parts(url); url_parts.has_value()) {
|
||||
if (url_parts.has_value()) {
|
||||
attributed_url = [[NSMutableAttributedString alloc] init];
|
||||
|
||||
auto* attributed_scheme_and_subdomain = [[NSAttributedString alloc]
|
||||
|
|
@ -424,6 +536,7 @@ static NSInteger autocomplete_suggestion_index(NSString* suggestion_text, Vector
|
|||
|
||||
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
|
||||
[location_search_field setAttributedStringValue:attributed_url];
|
||||
[location_search_field setShowsPageIcon:url_parts.has_value()];
|
||||
}
|
||||
|
||||
- (NSString*)currentLocationFieldQuery
|
||||
|
|
@ -1021,6 +1134,8 @@ static NSInteger autocomplete_suggestion_index(NSString* suggestion_text, Vector
|
|||
if (m_is_applying_inline_autocomplete)
|
||||
return;
|
||||
|
||||
[(LocationSearchField*)[self.location_toolbar_item view] setShowsPageIcon:NO];
|
||||
|
||||
auto* query = [self currentLocationFieldQuery];
|
||||
if (m_should_suppress_inline_autocomplete_on_next_change) {
|
||||
self.suppressed_inline_autocomplete_query = query;
|
||||
|
|
|
|||
|
|
@ -282,6 +282,8 @@ void BrowserWindow::on_tab_switched()
|
|||
}
|
||||
|
||||
bind_navigation_actions(tab->view());
|
||||
update_location_favicon(tab->favicon());
|
||||
update_location_loading(tab->is_loading());
|
||||
update_zoom_label();
|
||||
}
|
||||
|
||||
|
|
@ -429,6 +431,16 @@ void BrowserWindow::update_location_entry(StringView url)
|
|||
ladybird_location_entry_set_url(m_location_entry, byte_url.characters());
|
||||
}
|
||||
|
||||
void BrowserWindow::update_location_favicon(GdkPaintable* favicon)
|
||||
{
|
||||
ladybird_location_entry_set_favicon(m_location_entry, favicon);
|
||||
}
|
||||
|
||||
void BrowserWindow::update_location_loading(bool is_loading)
|
||||
{
|
||||
ladybird_location_entry_set_loading(m_location_entry, is_loading);
|
||||
}
|
||||
|
||||
void BrowserWindow::show_find_bar()
|
||||
{
|
||||
gtk_revealer_set_reveal_child(m_find_bar_revealer, TRUE);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ public:
|
|||
|
||||
void update_navigation_buttons(bool back_enabled, bool forward_enabled);
|
||||
void update_location_entry(StringView url);
|
||||
void update_location_favicon(GdkPaintable* favicon);
|
||||
void update_location_loading(bool is_loading);
|
||||
void update_zoom_label();
|
||||
void update_find_in_page_result(size_t current_match_index, Optional<size_t> const& total_match_count);
|
||||
|
||||
|
|
|
|||
|
|
@ -113,13 +113,22 @@ void Tab::setup_callbacks()
|
|||
};
|
||||
|
||||
m_view->on_load_start = [this](auto const&, bool) {
|
||||
m_is_loading = true;
|
||||
m_favicon.clear();
|
||||
if (m_tab_page)
|
||||
adw_tab_page_set_loading(m_tab_page, TRUE);
|
||||
if (m_window.current_tab() == this) {
|
||||
m_window.update_location_favicon(nullptr);
|
||||
m_window.update_location_loading(true);
|
||||
}
|
||||
};
|
||||
|
||||
m_view->on_load_finish = [this](auto const&) {
|
||||
m_is_loading = false;
|
||||
if (m_tab_page)
|
||||
adw_tab_page_set_loading(m_tab_page, FALSE);
|
||||
if (m_window.current_tab() == this)
|
||||
m_window.update_location_loading(false);
|
||||
};
|
||||
|
||||
m_view->on_cursor_change = [root](auto const& cursor) {
|
||||
|
|
@ -236,11 +245,13 @@ void Tab::setup_callbacks()
|
|||
};
|
||||
|
||||
m_view->on_favicon_change = [this](auto const& bitmap) {
|
||||
if (!m_tab_page)
|
||||
return;
|
||||
g_autoptr(GBytes) bytes = g_bytes_new(bitmap.scanline_u8(0), bitmap.size_in_bytes());
|
||||
GObjectPtr texture { gdk_memory_texture_new(bitmap.width(), bitmap.height(), GDK_MEMORY_B8G8R8A8_PREMULTIPLIED, bytes, bitmap.pitch()) };
|
||||
adw_tab_page_set_icon(m_tab_page, G_ICON(texture.ptr()));
|
||||
m_favicon = GObjectPtr<GdkPaintable>(GDK_PAINTABLE(g_object_ref(texture.ptr())));
|
||||
if (m_tab_page)
|
||||
adw_tab_page_set_icon(m_tab_page, G_ICON(texture.ptr()));
|
||||
if (m_window.current_tab() == this)
|
||||
m_window.update_location_favicon(m_favicon.ptr());
|
||||
};
|
||||
|
||||
m_view->on_audio_play_state_changed = [this](auto play_state) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include <LibWeb/HTML/ActivateTab.h>
|
||||
#include <LibWeb/HTML/SelectItem.h>
|
||||
#include <LibWebView/Forward.h>
|
||||
#include <UI/Gtk/GLibPtr.h>
|
||||
#include <UI/Gtk/Widgets/LadybirdWebView.h>
|
||||
|
||||
#include <adwaita.h>
|
||||
|
|
@ -34,6 +35,8 @@ public:
|
|||
WebContentView const& view() const { return *m_view; }
|
||||
|
||||
void navigate(URL::URL const& url);
|
||||
GdkPaintable* favicon() const { return m_favicon.ptr(); }
|
||||
bool is_loading() const { return m_is_loading; }
|
||||
|
||||
AdwTabPage* tab_page() const { return m_tab_page; }
|
||||
void set_tab_page(AdwTabPage* page) { m_tab_page = page; }
|
||||
|
|
@ -50,6 +53,8 @@ private:
|
|||
AdwTabPage* m_tab_page { nullptr };
|
||||
|
||||
URL::URL m_initial_url;
|
||||
GObjectPtr<GdkPaintable> m_favicon;
|
||||
bool m_is_loading { false };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,13 @@
|
|||
struct LocationEntryState {
|
||||
NonnullOwnPtr<WebView::Autocomplete> autocomplete;
|
||||
Vector<WebView::AutocompleteSuggestion> suggestions;
|
||||
Ladybird::GObjectPtr<GdkPaintable> favicon;
|
||||
int selected_index { -1 };
|
||||
String user_text;
|
||||
bool is_focused { false };
|
||||
bool is_loading { false };
|
||||
bool updating_text { false };
|
||||
guint loading_pulse_source_id { 0 };
|
||||
Function<void(String)> on_navigate;
|
||||
};
|
||||
|
||||
|
|
@ -51,6 +54,7 @@ static void ladybird_location_entry_hide_completions(LadybirdLocationEntry* self
|
|||
static void ladybird_location_entry_navigate(LadybirdLocationEntry* self);
|
||||
static void ladybird_location_entry_move_selection(LadybirdLocationEntry* self, int delta);
|
||||
static void ladybird_location_entry_apply_selected_suggestion(LadybirdLocationEntry* self);
|
||||
static void ladybird_location_entry_update_leading_icon(LadybirdLocationEntry* self);
|
||||
|
||||
static void set_entry_text_suppressed(LadybirdLocationEntry* self, char const* text, bool move_cursor_to_end = false)
|
||||
{
|
||||
|
|
@ -64,6 +68,10 @@ static void set_entry_text_suppressed(LadybirdLocationEntry* self, char const* t
|
|||
static void ladybird_location_entry_finalize(GObject* object)
|
||||
{
|
||||
auto* self = LADYBIRD_LOCATION_ENTRY(object);
|
||||
if (self->state && self->state->loading_pulse_source_id != 0) {
|
||||
g_source_remove(self->state->loading_pulse_source_id);
|
||||
self->state->loading_pulse_source_id = 0;
|
||||
}
|
||||
if (self->popover) {
|
||||
gtk_popover_popdown(self->popover);
|
||||
gtk_widget_unparent(GTK_WIDGET(self->popover));
|
||||
|
|
@ -81,7 +89,18 @@ static void ladybird_location_entry_class_init(LadybirdLocationEntryClass* klass
|
|||
|
||||
static void ladybird_location_entry_init(LadybirdLocationEntry* self)
|
||||
{
|
||||
self->state = adopt_own(*new LocationEntryState { .autocomplete = make<WebView::Autocomplete>(), .suggestions = {}, .user_text = {}, .on_navigate = {} });
|
||||
self->state = adopt_own(*new LocationEntryState {
|
||||
.autocomplete = make<WebView::Autocomplete>(),
|
||||
.suggestions = {},
|
||||
.favicon = {},
|
||||
.selected_index = -1,
|
||||
.user_text = {},
|
||||
.is_focused = false,
|
||||
.is_loading = false,
|
||||
.updating_text = false,
|
||||
.loading_pulse_source_id = 0,
|
||||
.on_navigate = {},
|
||||
});
|
||||
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(self), TRUE);
|
||||
|
||||
|
|
@ -193,21 +212,6 @@ void ladybird_location_entry_set_url(LadybirdLocationEntry* self, char const* ur
|
|||
{
|
||||
set_entry_text_suppressed(self, url ? url : "");
|
||||
|
||||
// Extract scheme for security icon
|
||||
if (url) {
|
||||
auto sv = StringView(url, strlen(url));
|
||||
auto colon = sv.find(':');
|
||||
if (colon.has_value()) {
|
||||
auto scheme = sv.substring_view(0, *colon);
|
||||
auto scheme_bs = ByteString(scheme);
|
||||
ladybird_location_entry_set_security_icon(self, scheme_bs.characters());
|
||||
} else {
|
||||
ladybird_location_entry_set_security_icon(self, nullptr);
|
||||
}
|
||||
} else {
|
||||
ladybird_location_entry_set_security_icon(self, nullptr);
|
||||
}
|
||||
|
||||
if (!self->state->is_focused)
|
||||
ladybird_location_entry_update_display_attributes(self);
|
||||
}
|
||||
|
|
@ -216,26 +220,60 @@ void ladybird_location_entry_set_text(LadybirdLocationEntry* self, char const* t
|
|||
{
|
||||
set_entry_text_suppressed(self, text ? text : "");
|
||||
gtk_entry_set_attributes(GTK_ENTRY(self), nullptr);
|
||||
ladybird_location_entry_set_security_icon(self, nullptr);
|
||||
ladybird_location_entry_set_favicon(self, nullptr);
|
||||
}
|
||||
|
||||
void ladybird_location_entry_set_security_icon(LadybirdLocationEntry* self, char const* scheme)
|
||||
void ladybird_location_entry_set_favicon(LadybirdLocationEntry* self, GdkPaintable* favicon)
|
||||
{
|
||||
if (!scheme) {
|
||||
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, nullptr);
|
||||
self->state->favicon = Ladybird::GObjectPtr<GdkPaintable>(favicon ? GDK_PAINTABLE(g_object_ref(favicon)) : nullptr);
|
||||
ladybird_location_entry_update_leading_icon(self);
|
||||
}
|
||||
|
||||
void ladybird_location_entry_set_loading(LadybirdLocationEntry* self, bool is_loading)
|
||||
{
|
||||
if (self->state->is_loading == is_loading)
|
||||
return;
|
||||
|
||||
self->state->is_loading = is_loading;
|
||||
if (self->state->is_loading) {
|
||||
gtk_entry_set_progress_pulse_step(GTK_ENTRY(self), 0.15);
|
||||
self->state->loading_pulse_source_id = g_timeout_add_full(
|
||||
G_PRIORITY_DEFAULT,
|
||||
100,
|
||||
+[](gpointer user_data) -> gboolean {
|
||||
auto* self = LADYBIRD_LOCATION_ENTRY(user_data);
|
||||
gtk_entry_progress_pulse(GTK_ENTRY(self));
|
||||
return G_SOURCE_CONTINUE;
|
||||
},
|
||||
self,
|
||||
nullptr);
|
||||
} else {
|
||||
if (self->state->loading_pulse_source_id != 0) {
|
||||
g_source_remove(self->state->loading_pulse_source_id);
|
||||
self->state->loading_pulse_source_id = 0;
|
||||
}
|
||||
gtk_entry_set_progress_fraction(GTK_ENTRY(self), 0.0);
|
||||
}
|
||||
|
||||
ladybird_location_entry_update_leading_icon(self);
|
||||
}
|
||||
|
||||
static void ladybird_location_entry_update_leading_icon(LadybirdLocationEntry* self)
|
||||
{
|
||||
if (self->state->is_loading) {
|
||||
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "process-working-symbolic");
|
||||
gtk_entry_set_icon_tooltip_text(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "Loading");
|
||||
return;
|
||||
}
|
||||
|
||||
auto sv = StringView(scheme, strlen(scheme));
|
||||
if (sv == "https"sv) {
|
||||
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "channel-secure-symbolic");
|
||||
gtk_entry_set_icon_tooltip_text(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "Secure connection");
|
||||
} else if (sv == "file"sv || sv == "resource"sv || sv == "about"sv || sv == "data"sv) {
|
||||
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, nullptr);
|
||||
} else {
|
||||
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "channel-insecure-symbolic");
|
||||
gtk_entry_set_icon_tooltip_text(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "Insecure connection");
|
||||
if (self->state->favicon.ptr()) {
|
||||
gtk_entry_set_icon_from_paintable(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, self->state->favicon.ptr());
|
||||
gtk_entry_set_icon_tooltip_text(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, "Page icon");
|
||||
return;
|
||||
}
|
||||
|
||||
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, nullptr);
|
||||
gtk_entry_set_icon_tooltip_text(GTK_ENTRY(self), GTK_ENTRY_ICON_PRIMARY, nullptr);
|
||||
}
|
||||
|
||||
void ladybird_location_entry_focus_and_select_all(LadybirdLocationEntry* self)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ LadybirdLocationEntry* ladybird_location_entry_new(void);
|
|||
|
||||
void ladybird_location_entry_set_url(LadybirdLocationEntry* self, char const* url);
|
||||
void ladybird_location_entry_set_text(LadybirdLocationEntry* self, char const* text);
|
||||
void ladybird_location_entry_set_security_icon(LadybirdLocationEntry* self, char const* scheme);
|
||||
void ladybird_location_entry_set_favicon(LadybirdLocationEntry* self, GdkPaintable* favicon);
|
||||
void ladybird_location_entry_set_loading(LadybirdLocationEntry* self, bool is_loading);
|
||||
void ladybird_location_entry_focus_and_select_all(LadybirdLocationEntry* self);
|
||||
void ladybird_location_entry_set_on_navigate(LadybirdLocationEntry* self, Function<void(String)> callback);
|
||||
|
|
|
|||
|
|
@ -11,13 +11,19 @@
|
|||
#include <LibWebView/Autocomplete.h>
|
||||
#include <LibWebView/URL.h>
|
||||
#include <UI/Qt/Autocomplete.h>
|
||||
#include <UI/Qt/Icon.h>
|
||||
#include <UI/Qt/LocationEdit.h>
|
||||
#include <UI/Qt/StringUtils.h>
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QLatin1String>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QPen>
|
||||
#include <QPixmap>
|
||||
#include <QPointF>
|
||||
#include <QTextLayout>
|
||||
#include <QTimer>
|
||||
|
||||
|
|
@ -127,11 +133,49 @@ static bool should_suppress_inline_autocomplete_for_key(QKeyEvent const* event)
|
|||
return key == Qt::Key_Backspace || key == Qt::Key_Delete;
|
||||
}
|
||||
|
||||
static QIcon loading_spinner_icon(QPalette const& palette, int frame)
|
||||
{
|
||||
static constexpr int icon_size = 16;
|
||||
static constexpr int segment_count = 12;
|
||||
|
||||
QPixmap pixmap(icon_size, icon_size);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.translate(icon_size / 2.0, icon_size / 2.0);
|
||||
|
||||
auto color = palette.color(QPalette::Text);
|
||||
for (int segment = 0; segment < segment_count; ++segment) {
|
||||
auto segment_color = color;
|
||||
segment_color.setAlpha(((segment - frame + segment_count) % segment_count + 1) * 255 / segment_count);
|
||||
|
||||
painter.save();
|
||||
painter.rotate(segment * 360.0 / segment_count);
|
||||
painter.setPen(QPen(segment_color, 2, Qt::SolidLine, Qt::RoundCap));
|
||||
painter.drawLine(QPointF(0, -4), QPointF(0, -7));
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
return QIcon(pixmap);
|
||||
}
|
||||
|
||||
LocationEdit::LocationEdit(QWidget* parent)
|
||||
: QLineEdit(parent)
|
||||
, m_autocomplete(new Autocomplete(this))
|
||||
{
|
||||
m_leading_icon_action = addAction(create_tvg_icon_with_theme_colors("search", palette()), QLineEdit::LeadingPosition);
|
||||
m_leading_icon_action->setToolTip("Search");
|
||||
|
||||
m_loading_animation_timer = new QTimer(this);
|
||||
m_loading_animation_timer->setInterval(80);
|
||||
connect(m_loading_animation_timer, &QTimer::timeout, this, [this] {
|
||||
m_loading_animation_frame = (m_loading_animation_frame + 1) % 12;
|
||||
update_loading_icon();
|
||||
});
|
||||
|
||||
update_placeholder();
|
||||
update_location_icon();
|
||||
|
||||
m_autocomplete->on_query_complete = [this](auto suggestions, WebView::AutocompleteResultKind result_kind) {
|
||||
int selected_row = apply_inline_autocomplete(suggestions);
|
||||
|
|
@ -207,7 +251,17 @@ LocationEdit::LocationEdit(QWidget* parent)
|
|||
m_autocomplete->query_autocomplete_engine(ak_string_from_qstring(query));
|
||||
});
|
||||
|
||||
connect(this, &QLineEdit::textChanged, this, &LocationEdit::highlight_location);
|
||||
connect(this, &QLineEdit::textChanged, this, [this] {
|
||||
highlight_location();
|
||||
update_location_icon();
|
||||
});
|
||||
}
|
||||
|
||||
void LocationEdit::changeEvent(QEvent* event)
|
||||
{
|
||||
QLineEdit::changeEvent(event);
|
||||
if (event->type() == QEvent::PaletteChange)
|
||||
update_location_icon();
|
||||
}
|
||||
|
||||
void LocationEdit::focusInEvent(QFocusEvent* event)
|
||||
|
|
@ -291,6 +345,35 @@ void LocationEdit::update_placeholder()
|
|||
}
|
||||
}
|
||||
|
||||
void LocationEdit::update_location_icon()
|
||||
{
|
||||
if (!m_leading_icon_action)
|
||||
return;
|
||||
|
||||
if (m_is_loading) {
|
||||
update_loading_icon();
|
||||
return;
|
||||
}
|
||||
|
||||
if (text_matches_current_url()) {
|
||||
m_leading_icon_action->setIcon(m_favicon.isNull() ? create_tvg_icon_with_theme_colors("globe", palette()) : m_favicon);
|
||||
m_leading_icon_action->setToolTip("Page icon");
|
||||
return;
|
||||
}
|
||||
|
||||
m_leading_icon_action->setIcon(create_tvg_icon_with_theme_colors("search", palette()));
|
||||
m_leading_icon_action->setToolTip("Search");
|
||||
}
|
||||
|
||||
void LocationEdit::update_loading_icon()
|
||||
{
|
||||
if (!m_leading_icon_action)
|
||||
return;
|
||||
|
||||
m_leading_icon_action->setIcon(loading_spinner_icon(palette(), m_loading_animation_frame));
|
||||
m_leading_icon_action->setToolTip("Loading");
|
||||
}
|
||||
|
||||
void LocationEdit::highlight_location()
|
||||
{
|
||||
auto url = ak_string_from_qstring(text());
|
||||
|
|
@ -342,6 +425,37 @@ void LocationEdit::set_url(Optional<URL::URL> url)
|
|||
setText(qstring_from_ak_string(m_url->serialize()));
|
||||
setCursorPosition(0);
|
||||
}
|
||||
|
||||
update_location_icon();
|
||||
}
|
||||
|
||||
void LocationEdit::set_loading(bool is_loading)
|
||||
{
|
||||
if (m_is_loading == is_loading)
|
||||
return;
|
||||
|
||||
m_is_loading = is_loading;
|
||||
m_loading_animation_frame = 0;
|
||||
|
||||
if (m_is_loading)
|
||||
m_loading_animation_timer->start();
|
||||
else
|
||||
m_loading_animation_timer->stop();
|
||||
|
||||
update_location_icon();
|
||||
}
|
||||
|
||||
void LocationEdit::set_favicon(QIcon const& favicon)
|
||||
{
|
||||
m_favicon = favicon;
|
||||
update_location_icon();
|
||||
}
|
||||
|
||||
bool LocationEdit::text_matches_current_url() const
|
||||
{
|
||||
return m_url.has_value()
|
||||
&& !m_url_is_hidden
|
||||
&& text() == qstring_from_ak_string(m_url->serialize());
|
||||
}
|
||||
|
||||
QString LocationEdit::current_query() const
|
||||
|
|
|
|||
|
|
@ -12,9 +12,14 @@
|
|||
#include <LibWebView/Autocomplete.h>
|
||||
#include <LibWebView/Settings.h>
|
||||
|
||||
#include <QIcon>
|
||||
#include <QLineEdit>
|
||||
#include <QString>
|
||||
|
||||
class QAction;
|
||||
class QEvent;
|
||||
class QTimer;
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class Autocomplete;
|
||||
|
|
@ -29,11 +34,14 @@ public:
|
|||
|
||||
Optional<URL::URL const&> url() const { return m_url; }
|
||||
void set_url(Optional<URL::URL>);
|
||||
void set_loading(bool);
|
||||
void set_favicon(QIcon const&);
|
||||
|
||||
bool url_is_hidden() const { return m_url_is_hidden; }
|
||||
void set_url_is_hidden(bool url_is_hidden) { m_url_is_hidden = url_is_hidden; }
|
||||
|
||||
private:
|
||||
virtual void changeEvent(QEvent* event) override;
|
||||
virtual void focusInEvent(QFocusEvent* event) override;
|
||||
virtual void focusOutEvent(QFocusEvent* event) override;
|
||||
virtual void keyPressEvent(QKeyEvent* event) override;
|
||||
|
|
@ -41,7 +49,10 @@ private:
|
|||
virtual void search_engine_changed() override;
|
||||
|
||||
void update_placeholder();
|
||||
void update_location_icon();
|
||||
void update_loading_icon();
|
||||
void highlight_location();
|
||||
bool text_matches_current_url() const;
|
||||
|
||||
int apply_inline_autocomplete(Vector<WebView::AutocompleteSuggestion> const&);
|
||||
bool apply_inline_autocomplete_suggestion_text(QString const& suggestion_text, QString const& query);
|
||||
|
|
@ -51,9 +62,14 @@ private:
|
|||
void reset_autocomplete_state();
|
||||
|
||||
Autocomplete* m_autocomplete { nullptr };
|
||||
QAction* m_leading_icon_action { nullptr };
|
||||
QTimer* m_loading_animation_timer { nullptr };
|
||||
|
||||
Optional<URL::URL> m_url;
|
||||
QIcon m_favicon;
|
||||
bool m_url_is_hidden { false };
|
||||
bool m_is_loading { false };
|
||||
int m_loading_animation_frame { 0 };
|
||||
|
||||
bool m_is_applying_inline_autocomplete { false };
|
||||
bool m_should_suppress_inline_autocomplete_on_next_change { false };
|
||||
|
|
|
|||
|
|
@ -181,10 +181,20 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
|
|||
m_favicon = default_favicon();
|
||||
emit favicon_changed(tab_index(), m_favicon);
|
||||
|
||||
m_location_edit->set_favicon({});
|
||||
m_location_edit->set_loading(true);
|
||||
m_location_edit->set_url(url);
|
||||
m_location_edit->setCursorPosition(0);
|
||||
};
|
||||
|
||||
view().on_load_finish = [this](auto const&) {
|
||||
m_location_edit->set_loading(false);
|
||||
};
|
||||
|
||||
view().on_web_content_crashed = [this] {
|
||||
m_location_edit->set_loading(false);
|
||||
};
|
||||
|
||||
view().on_url_change = [this](auto const& url) {
|
||||
m_location_edit->set_url(url);
|
||||
};
|
||||
|
|
@ -206,6 +216,7 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
|
|||
|
||||
m_favicon = qpixmap;
|
||||
emit favicon_changed(tab_index(), m_favicon);
|
||||
m_location_edit->set_favicon(m_favicon);
|
||||
};
|
||||
|
||||
view().on_request_alert = [this](auto const& message) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue