diff --git a/Libraries/LibWebView/ViewImplementation.cpp b/Libraries/LibWebView/ViewImplementation.cpp index 02bf18d7f9f0..2cf94f691e37 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -1025,4 +1025,9 @@ void ViewImplementation::did_request_media_context_menu(Badge, m_media_context_menu->on_activation(to_widget_position(content_position)); } +void ViewImplementation::request_close() +{ + client().async_request_close(page_id()); +} + } diff --git a/Libraries/LibWebView/ViewImplementation.h b/Libraries/LibWebView/ViewImplementation.h index 97ecf4251e21..15a92aa6df3a 100644 --- a/Libraries/LibWebView/ViewImplementation.h +++ b/Libraries/LibWebView/ViewImplementation.h @@ -164,6 +164,8 @@ class WEBVIEW_API ViewImplementation : public SettingsObserver { // native GUI widgets as possible. void use_native_user_style_sheet(); + void request_close(); + Function on_ready_to_paint; Function)> on_new_web_view; Function on_activate_tab; diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index 256ee03df797..f31363210ccf 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -1348,4 +1348,13 @@ void ConnectionFromClient::cookies_changed(Vector cookies) } } +// https://html.spec.whatwg.org/multipage/speculative-loading.html#nav-traversal-ui:close-a-top-level-traversable +void ConnectionFromClient::request_close(u64 page_id) +{ + // Browser user agents should offer users the ability to arbitrarily close any top-level traversable in their top-level traversable set. + // For example, by clicking a "close tab" button. + if (auto page = this->page(page_id); page.has_value()) + page->page().top_level_traversable()->close_top_level_traversable(); +} + } diff --git a/Services/WebContent/ConnectionFromClient.h b/Services/WebContent/ConnectionFromClient.h index b2b27a702247..a1b8de7aa9d8 100644 --- a/Services/WebContent/ConnectionFromClient.h +++ b/Services/WebContent/ConnectionFromClient.h @@ -162,6 +162,8 @@ class ConnectionFromClient final virtual void system_time_zone_changed() override; virtual void cookies_changed(Vector) override; + virtual void request_close(u64 page_id) override; + NonnullOwnPtr m_page_host; HashMap m_requested_files {}; diff --git a/Services/WebContent/WebContentServer.ipc b/Services/WebContent/WebContentServer.ipc index 4774f731cc38..7f927669cf8d 100644 --- a/Services/WebContent/WebContentServer.ipc +++ b/Services/WebContent/WebContentServer.ipc @@ -132,4 +132,6 @@ endpoint WebContentServer system_time_zone_changed() =| cookies_changed(Vector cookies) =| + + request_close(u64 page_id) =| } diff --git a/UI/AppKit/Application/ApplicationDelegate.mm b/UI/AppKit/Application/ApplicationDelegate.mm index c5d960222847..5a966735d161 100644 --- a/UI/AppKit/Application/ApplicationDelegate.mm +++ b/UI/AppKit/Application/ApplicationDelegate.mm @@ -197,7 +197,7 @@ - (void)initializeTabController:(TabController*)controller - (void)closeCurrentTab:(id)sender { auto* current_window = [NSApp keyWindow]; - [current_window close]; + [current_window performClose:self]; } - (void)clearHistory:(id)sender diff --git a/UI/AppKit/Interface/LadybirdWebView.h b/UI/AppKit/Interface/LadybirdWebView.h index 956b00f9143a..ccb457955581 100644 --- a/UI/AppKit/Interface/LadybirdWebView.h +++ b/UI/AppKit/Interface/LadybirdWebView.h @@ -63,4 +63,6 @@ - (void)findInPageNextMatch; - (void)findInPagePreviousMatch; +- (void)requestClose; + @end diff --git a/UI/AppKit/Interface/LadybirdWebView.mm b/UI/AppKit/Interface/LadybirdWebView.mm index a7204e319179..d64a1b20d30f 100644 --- a/UI/AppKit/Interface/LadybirdWebView.mm +++ b/UI/AppKit/Interface/LadybirdWebView.mm @@ -204,6 +204,11 @@ - (void)findInPagePreviousMatch m_web_view_bridge->find_in_page_previous_match(); } +- (void)requestClose +{ + m_web_view_bridge->request_close(); +} + #pragma mark - Private methods - (void)updateViewportRect diff --git a/UI/AppKit/Interface/TabController.mm b/UI/AppKit/Interface/TabController.mm index f99303dfa3ba..7ecf0cddd651 100644 --- a/UI/AppKit/Interface/TabController.mm +++ b/UI/AppKit/Interface/TabController.mm @@ -55,6 +55,8 @@ @interface TabController () m_autocomplete; } +@property (nonatomic, assign) BOOL already_requested_close; + @property (nonatomic, strong) Tab* parent; @property (nonatomic, strong) NSToolbar* toolbar; @@ -397,6 +399,21 @@ - (void)windowDidBecomeMain:(NSNotification*)notification [delegate setActiveTab:[self tab]]; } +- (BOOL)windowShouldClose:(NSWindow*)sender +{ + // Prevent closing on first request so WebContent can cleanly shutdown (e.g. asking if the user is sure they want + // to leave, closing WebSocket connections, etc.) + if (!self.already_requested_close) { + self.already_requested_close = true; + [[[self tab] web_view] requestClose]; + return false; + } + + // If the user has already requested a close, then respect the user's request and just close the tab. + // For example, the WebContent process may not be responding. + return true; +} + - (void)windowWillClose:(NSNotification*)notification { auto* delegate = (ApplicationDelegate*)[NSApp delegate]; diff --git a/UI/Qt/BrowserWindow.cpp b/UI/Qt/BrowserWindow.cpp index 8a32ba66368d..1ee8e735ee1c 100644 --- a/UI/Qt/BrowserWindow.cpp +++ b/UI/Qt/BrowserWindow.cpp @@ -237,8 +237,8 @@ BrowserWindow::BrowserWindow(Vector const& initial_urls, IsPopupWindow set_current_tab(tab); }); - QObject::connect(m_tabs_container, &QTabWidget::tabCloseRequested, this, &BrowserWindow::close_tab); - QObject::connect(close_current_tab_action, &QAction::triggered, this, &BrowserWindow::close_current_tab); + QObject::connect(m_tabs_container, &QTabWidget::tabCloseRequested, this, &BrowserWindow::request_to_close_tab); + QObject::connect(close_current_tab_action, &QAction::triggered, this, &BrowserWindow::request_to_close_current_tab); for (int i = 0; i <= 7; ++i) { new QShortcut(QKeySequence(Qt::CTRL | static_cast(Qt::Key_1 + i)), this, [this, i] { @@ -378,7 +378,7 @@ void BrowserWindow::activate_tab(int index) m_tabs_container->setCurrentIndex(index); } -void BrowserWindow::close_tab(int index) +void BrowserWindow::definitely_close_tab(int index) { auto* tab = m_tabs_container->widget(index); m_tabs_container->removeTab(index); @@ -398,9 +398,15 @@ void BrowserWindow::open_file() m_current_tab->open_file(); } -void BrowserWindow::close_current_tab() +void BrowserWindow::request_to_close_tab(int index) { - close_tab(m_tabs_container->currentIndex()); + auto* tab = as(m_tabs_container->widget(index)); + tab->request_close(); +} + +void BrowserWindow::request_to_close_current_tab() +{ + request_to_close_tab(m_tabs_container->currentIndex()); } int BrowserWindow::tab_index(Tab* tab) @@ -452,7 +458,7 @@ void BrowserWindow::create_close_button_for_tab(Tab* tab) connect(button, &QPushButton::clicked, this, [this, tab]() { auto index = m_tabs_container->indexOf(tab); - close_tab(index); + request_to_close_tab(index); }); m_tabs_container->tabBar()->setTabButton(index, position, button); @@ -625,7 +631,7 @@ bool BrowserWindow::eventFilter(QObject* obj, QEvent* event) if (obj == m_tabs_container) { auto const tab_index = m_tabs_container->tabBar()->tabAt(mouse_event->pos()); if (tab_index != -1) { - close_tab(tab_index); + request_to_close_tab(tab_index); return true; } } diff --git a/UI/Qt/BrowserWindow.h b/UI/Qt/BrowserWindow.h index 0d79953523e3..362eb228df04 100644 --- a/UI/Qt/BrowserWindow.h +++ b/UI/Qt/BrowserWindow.h @@ -63,9 +63,10 @@ public slots: Tab& new_tab_from_url(URL::URL const&, Web::HTML::ActivateTab); Tab& new_child_tab(Web::HTML::ActivateTab, Tab& parent, Optional page_index); void activate_tab(int index); - void close_tab(int index); + void definitely_close_tab(int index); void move_tab(int old_index, int new_index); - void close_current_tab(); + void request_to_close_tab(int index); + void request_to_close_current_tab(); void open_next_tab(); void open_previous_tab(); void open_file(); diff --git a/UI/Qt/Tab.cpp b/UI/Qt/Tab.cpp index ef4014aede27..adbc1d748a6c 100644 --- a/UI/Qt/Tab.cpp +++ b/UI/Qt/Tab.cpp @@ -115,7 +115,7 @@ Tab::Tab(BrowserWindow* window, RefPtr parent_client, }; view().on_close = [this] { - m_window->close_tab(tab_index()); + m_window->definitely_close_tab(tab_index()); }; view().on_link_hover = [this](auto const& url) { @@ -374,20 +374,20 @@ Tab::Tab(BrowserWindow* window, RefPtr parent_client, auto* close_tab_action = new QAction("&Close Tab", this); QObject::connect(close_tab_action, &QAction::triggered, this, [this]() { - view().on_close(); + request_close(); }); auto* close_tabs_to_left_action = new QAction("C&lose Tabs to Left", this); QObject::connect(close_tabs_to_left_action, &QAction::triggered, this, [this]() { for (auto i = tab_index() - 1; i >= 0; i--) { - m_window->close_tab(i); + m_window->request_to_close_tab(i); } }); auto* close_tabs_to_right_action = new QAction("Close Tabs to R&ight", this); QObject::connect(close_tabs_to_right_action, &QAction::triggered, this, [this]() { for (auto i = m_window->tab_count() - 1; i > tab_index(); i--) { - m_window->close_tab(i); + m_window->request_to_close_tab(i); } }); @@ -397,7 +397,7 @@ Tab::Tab(BrowserWindow* window, RefPtr parent_client, if (i == tab_index()) continue; - m_window->close_tab(i); + m_window->request_to_close_tab(i); } }); @@ -513,4 +513,19 @@ void Tab::find_next() m_find_in_page->find_next(); } +void Tab::request_close() +{ + // Prevent closing on first request so WebContent can cleanly shutdown (e.g. asking if the user is sure they want + // to leave, closing WebSocket connections, etc.) + if (!m_already_requested_close) { + m_already_requested_close = true; + view().request_close(); + return; + } + + // If the user has already requested a close, then respect the user's request and just close the tab. + // For example, the WebContent process may not be responding. + m_window->definitely_close_tab(tab_index()); +} + } diff --git a/UI/Qt/Tab.h b/UI/Qt/Tab.h index 5149fff2b235..88cf900cbbf3 100644 --- a/UI/Qt/Tab.h +++ b/UI/Qt/Tab.h @@ -61,6 +61,8 @@ class Tab final : public QWidget { void find_previous(); void find_next(); + void request_close(); + QIcon const& favicon() const { return m_favicon; } QString const& title() const { return m_title; } @@ -113,6 +115,8 @@ public slots: QAction* m_reload_action { nullptr }; QPointer m_dialog; + + bool m_already_requested_close { false }; }; }