From b91ff9ebfd0452c6269f630fd25e98f2409c97a3 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 21 Jun 2026 12:28:23 +0200 Subject: [PATCH] UI/Qt: Keep macOS app alive without windows Disable Qt quit-on-last-window behavior on macOS and install an application-level menu so useful commands remain available after the last browser window closes. Reuse shared application actions and menus for Settings, Bookmarks, History, Help, and global appearance controls. Centralize window-aware commands such as New Tab, New Window, Open File, Open Location, and Reopen Recently Closed so the browser window and windowless app menu use the same behavior. Keep the location editor focused for new blank windows after the native window has been activated. --- Libraries/LibWebView/Application.cpp | 2 + Libraries/LibWebView/Application.h | 2 +- UI/Qt/Application.cpp | 254 ++++++++++++++++++++++++++- UI/Qt/Application.h | 12 ++ UI/Qt/BrowserWindow.cpp | 54 +++--- UI/Qt/main.cpp | 27 ++- 6 files changed, 303 insertions(+), 48 deletions(-) diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index 9fab7d0f6e..7489177875 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -1683,6 +1683,8 @@ void Application::create_bookmark_menu_items(Optional data) auto action = Action::create(bookmark.title.value_or({}), ActionID::BookmarkItem, [this, url = bookmark.url]() { if (auto view = active_web_view(); view.has_value()) view->load(url); + else + open_url_in_new_tab(url, Web::HTML::ActivateTab::Yes); }); action->set_base64_png_icon(bookmark.favicon_base64_png); diff --git a/Libraries/LibWebView/Application.h b/Libraries/LibWebView/Application.h index 4c459943e8..3eea66249c 100644 --- a/Libraries/LibWebView/Application.h +++ b/Libraries/LibWebView/Application.h @@ -111,7 +111,7 @@ public: virtual Optional active_web_view() const { return {}; } virtual Optional open_blank_new_tab(Web::HTML::ActivateTab) const { return {}; } virtual bool activate_tab_with_url(URL::URL const&) const { return false; } - void open_url_in_new_tab(URL::URL const&, Web::HTML::ActivateTab) const; + virtual void open_url_in_new_tab(URL::URL const&, Web::HTML::ActivateTab) const; void open_bookmark_in_new_tab(String const& bookmark_id, Web::HTML::ActivateTab) const; Main::Arguments const& command_line_arguments() const { return m_arguments; } diff --git a/UI/Qt/Application.cpp b/UI/Qt/Application.cpp index 10db079bfc..133a1fc20a 100644 --- a/UI/Qt/Application.cpp +++ b/UI/Qt/Application.cpp @@ -6,10 +6,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -18,6 +20,7 @@ # include #endif +#include #include #include #include @@ -25,10 +28,14 @@ #include #include #include +#include #include +#include +#include #include #include #include +#include #if defined(AK_OS_WINDOWS) # include @@ -69,6 +76,7 @@ public: : QApplication(arguments.argc, arguments.argv) { #if defined(AK_OS_MACOS) + setQuitOnLastWindowClosed(false); install_appkit_event_capture(); #endif update_chrome_style(); @@ -111,11 +119,121 @@ public: return handled; } +#if defined(AK_OS_MACOS) + void update_reopen_recently_closed_action() + { + if (!m_reopen_recently_closed_tab_action) + return; + + auto recently_closed_entry = Application::history_store().most_recently_closed_entry(); + m_reopen_recently_closed_tab_action->setText("&Reopen Recently Closed Tab"); + m_reopen_recently_closed_tab_action->setEnabled(recently_closed_entry.has_value()); + } + +#endif + +#if defined(AK_OS_MACOS) + void create_application_menu_bar() + { + if (m_application_menu_bar) + return; + + m_application_menu_bar = new QMenuBar; + auto& application = Application::the(); + + auto* file_menu = m_application_menu_bar->addMenu("&File"); + + auto* new_tab_action = add_application_menu_action(*file_menu, "New &Tab", QKeySequence::keyBindings(QKeySequence::StandardKey::AddTab)); + QObject::connect(new_tab_action, &QAction::triggered, this, [] { + Application::the().open_new_tab(); + }); + + auto* new_window_action = add_application_menu_action(*file_menu, "New &Window", QKeySequence::keyBindings(QKeySequence::StandardKey::New)); + QObject::connect(new_window_action, &QAction::triggered, this, [] { + Application::the().open_new_window(); + }); + + m_reopen_recently_closed_tab_action = add_application_menu_action(*file_menu, {}, { QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_T) }); + QObject::connect(m_reopen_recently_closed_tab_action, &QAction::triggered, this, [] { + Application::the().reopen_recently_closed_tab(); + }); + + auto* open_file_action = add_application_menu_action(*file_menu, "&Open File...", QKeySequence::keyBindings(QKeySequence::StandardKey::Open)); + QObject::connect(open_file_action, &QAction::triggered, this, [] { + Application::the().open_file(); + }); + + auto* open_location_action = add_application_menu_action(*file_menu, "Open &Location", { QKeySequence("Ctrl+L"), QKeySequence("Alt+D") }); + QObject::connect(open_location_action, &QAction::triggered, this, [] { + Application::the().focus_location_editor(); + }); + + file_menu->addSeparator(); + + auto* quit_action = add_application_menu_action(*file_menu, "&Quit", QKeySequence::keyBindings(QKeySequence::StandardKey::Quit)); + QObject::connect(quit_action, &QAction::triggered, this, [] { + Application::the().quit(); + }); + + auto* edit_menu = m_application_menu_bar->addMenu("&Edit"); + edit_menu->addAction(create_application_action(*edit_menu, application.cut_selection_action(), IncludeActionIcon::No)); + edit_menu->addAction(create_application_action(*edit_menu, application.copy_selection_action(), IncludeActionIcon::No)); + edit_menu->addAction(create_application_action(*edit_menu, application.paste_action(), IncludeActionIcon::No)); + edit_menu->addAction(create_application_action(*edit_menu, application.select_all_action(), IncludeActionIcon::No)); + edit_menu->addSeparator(); + edit_menu->addAction(create_application_action(*edit_menu, application.open_settings_page_action(), IncludeActionIcon::No)); + + auto* view_menu = m_application_menu_bar->addMenu("&View"); + view_menu->addMenu(create_application_menu(*view_menu, application.color_scheme_menu())); + view_menu->addMenu(create_application_menu(*view_menu, application.contrast_menu())); + view_menu->addMenu(create_application_menu(*view_menu, application.motion_menu())); + + m_application_menu_bar->addMenu(bookmarks_menu()); + m_application_menu_bar->addMenu(create_application_menu(*m_application_menu_bar, application.history_menu())); + + auto* help_menu = m_application_menu_bar->addMenu("&Help"); + help_menu->addAction(create_application_action(*help_menu, application.open_about_page_action(), IncludeActionIcon::No)); + + update_reopen_recently_closed_action(); + } +#endif + +#if defined(AK_OS_MACOS) + QMenu* bookmarks_menu() + { + if (!m_bookmarks_menu) + m_bookmarks_menu = create_application_menu(*m_application_menu_bar, Application::the().bookmarks_menu()); + return m_bookmarks_menu; + } + + void rebuild_bookmarks_menu() + { + if (m_bookmarks_menu) + repopulate_application_menu(*m_bookmarks_menu, *m_application_menu_bar, Application::the().bookmarks_menu()); + } +#endif + private: +#if defined(AK_OS_MACOS) + QAction* add_application_menu_action(QMenu& menu, QString const& text, QList shortcuts) + { + auto* action = new QAction(text, m_application_menu_bar); + action->setShortcuts(shortcuts); + menu.addAction(action); + return action; + } +#endif + void update_chrome_style() { setStyleSheet(ChromeStyle::application_style_sheet(palette())); } + +#if defined(AK_OS_MACOS) + QMenuBar* m_application_menu_bar { nullptr }; + QMenu* m_bookmarks_menu { nullptr }; + QAction* m_reopen_recently_closed_tab_action { nullptr }; +#endif }; Application::Application() = default; @@ -145,12 +263,15 @@ BrowserWindow& Application::new_window(Vector const& initial_urls, Win { auto* window = new BrowserWindow(initial_urls, is_popup_window, parent_tab, move(page_index)); set_active_window(*window); + QObject::connect(window, &QObject::destroyed, m_application.ptr(), [this, window] { + if (m_active_window == window) + m_active_window = nullptr; + }); - if (initial_urls.size() == 1 && initial_urls.first() == URL::about_newtab()) { - if (auto* tab = window->current_tab()) { + auto should_focus_location_editor = initial_urls.size() == 1 && initial_urls.first() == WebView::Application::settings().new_tab_page_url(); + if (should_focus_location_editor) { + if (auto* tab = window->current_tab()) tab->set_url_is_hidden(true); - tab->focus_location_editor(); - } } window->set_window_rect(configuration.x, configuration.y, configuration.width, configuration.height); @@ -161,9 +282,107 @@ BrowserWindow& Application::new_window(Vector const& initial_urls, Win window->activateWindow(); window->raise(); + if (should_focus_location_editor) { + QTimer::singleShot(0, window, [window] { + if (auto* tab = window->current_tab()) + tab->focus_location_editor(); + }); + } return *window; } +void Application::open_new_tab() +{ + if (!m_active_window) { + new_window({ WebView::Application::settings().new_tab_page_url() }); + return; + } + + auto& tab = m_active_window->new_tab_from_url(WebView::Application::settings().new_tab_page_url(), Web::HTML::ActivateTab::Yes); + tab.set_url_is_hidden(true); + tab.focus_location_editor(); +} + +void Application::open_new_window() +{ + WindowConfiguration configuration {}; + if (auto* previous_active_window = active_window_if_any()) { + configuration.width = previous_active_window->width(); + configuration.height = previous_active_window->height(); + configuration.maximized = previous_active_window->isMaximized(); + } + new_window({ WebView::Application::settings().new_tab_page_url() }, configuration); +} + +void Application::focus_location_editor() +{ + if (!m_active_window) { + new_window({ WebView::Application::settings().new_tab_page_url() }); + return; + } + + if (auto* tab = m_active_window->current_tab()) + tab->focus_location_editor(); +} + +void Application::reopen_recently_closed_tab() +{ + auto recently_closed_entry = Application::history_store().pop_most_recently_closed_entry(); + if (recently_closed_entry.has_value()) { + if (recently_closed_entry->was_window) { + auto& window = new_window(recently_closed_entry->urls); + window.activate_tab(static_cast(recently_closed_entry->active_tab_index)); + } else if (!recently_closed_entry->urls.is_empty()) { + if (!m_active_window) + new_window({ recently_closed_entry->urls[0] }); + else + m_active_window->new_tab_from_url(recently_closed_entry->urls[0], Web::HTML::ActivateTab::Yes); + } + } + update_reopen_recently_closed_actions(); +} + +void Application::open_file() +{ + if (!m_active_window) { + auto filename = QFileDialog::getOpenFileUrl(nullptr, "Open file", QDir::homePath(), "All Files (*.*)"); + if (filename.isValid()) + new_window({ ak_url_from_qurl(filename) }); + return; + } + + m_active_window->open_file(); +} + +void Application::quit() +{ + QApplication::closeAllWindows(); + + for (auto* widget : QApplication::topLevelWidgets()) { + if (as_if(widget) && widget->isVisible()) + return; + } + + QApplication::quit(); +} + +void Application::initialize_macos_application_menu() +{ +#if defined(AK_OS_MACOS) + if (m_application) + static_cast(m_application.ptr())->create_application_menu_bar(); +#endif +} + +QMenu* Application::qt_bookmarks_menu() const +{ +#if defined(AK_OS_MACOS) + if (m_application) + return static_cast(m_application.ptr())->bookmarks_menu(); +#endif + return nullptr; +} + Optional Application::active_web_view() const { if (auto* active_tab = this->active_tab()) @@ -173,10 +392,27 @@ Optional Application::active_web_view() const Optional Application::open_blank_new_tab(Web::HTML::ActivateTab activate_tab) const { + if (!m_active_window) { + auto& window = const_cast(*this).new_window({ WebView::Application::settings().new_tab_page_url() }); + if (auto* tab = window.current_tab()) + return tab->view(); + return {}; + } + auto& tab = active_window().create_new_tab(activate_tab); return tab.view(); } +void Application::open_url_in_new_tab(URL::URL const& url, Web::HTML::ActivateTab activate_tab) const +{ + if (!m_active_window) { + const_cast(*this).new_window({ url }); + return; + } + + active_window().new_tab_from_url(url, activate_tab); +} + bool Application::activate_tab_with_url(URL::URL const& url) const { if (!m_active_window) @@ -322,6 +558,11 @@ void Application::update_tabs_display() const void Application::rebuild_bookmarks_menu() const { +#if defined(AK_OS_MACOS) + if (m_application) + static_cast(m_application.ptr())->rebuild_bookmarks_menu(); +#endif + for (auto* widget : QApplication::topLevelWidgets()) { if (auto* window = as_if(widget)) window->rebuild_bookmarks_menu(); @@ -330,6 +571,11 @@ void Application::rebuild_bookmarks_menu() const void Application::update_reopen_recently_closed_actions() const { +#if defined(AK_OS_MACOS) + if (m_application) + static_cast(m_application.ptr())->update_reopen_recently_closed_action(); +#endif + for (auto* widget : QApplication::topLevelWidgets()) { if (auto* window = as_if(widget)) window->update_reopen_recently_closed_action(); diff --git a/UI/Qt/Application.h b/UI/Qt/Application.h index d3e130dec4..46c15abc96 100644 --- a/UI/Qt/Application.h +++ b/UI/Qt/Application.h @@ -13,6 +13,8 @@ #include +class QMenu; + namespace Ladybird { struct WindowConfiguration { @@ -31,9 +33,18 @@ public: Function on_open_file; BrowserWindow& new_window(Vector const& initial_urls, WindowConfiguration const& = {}, BrowserWindow::IsPopupWindow is_popup_window = BrowserWindow::IsPopupWindow::No, Tab* parent_tab = nullptr, Optional page_index = {}); + void open_new_tab(); + void open_new_window(); + void focus_location_editor(); + void reopen_recently_closed_tab(); + void open_file(); + void quit(); + void initialize_macos_application_menu(); + QMenu* qt_bookmarks_menu() const; BrowserWindow& active_window() const { return *m_active_window; } void set_active_window(BrowserWindow& w) { m_active_window = &w; } + BrowserWindow* active_window_if_any() const { return m_active_window; } Tab* active_tab() const { return m_active_window ? m_active_window->current_tab() : nullptr; } void update_reopen_recently_closed_actions() const; @@ -46,6 +57,7 @@ private: virtual Optional active_web_view() const override; virtual Optional open_blank_new_tab(Web::HTML::ActivateTab) const override; + virtual void open_url_in_new_tab(URL::URL const&, Web::HTML::ActivateTab) const override; virtual bool activate_tab_with_url(URL::URL const&) const override; virtual void open_url_in_new_window(URL::URL const& url) override; diff --git a/UI/Qt/BrowserWindow.cpp b/UI/Qt/BrowserWindow.cpp index 297e1065c6..2b4f9fb4bd 100644 --- a/UI/Qt/BrowserWindow.cpp +++ b/UI/Qt/BrowserWindow.cpp @@ -90,14 +90,6 @@ static Optional display_id_for_screen(QScreen* screen) }); } -static QString reopen_recently_closed_action_text(Optional entry) -{ - if (entry.has_value() && entry->was_window) - return "&Reopen Recently Closed Window"; - - return "&Reopen Recently Closed Tab"; -} - static Vector recently_closed_urls_for_window(TabWidget const& tabs_container) { Vector urls; @@ -373,7 +365,9 @@ BrowserWindow::BrowserWindow(Vector const& initial_urls, IsPopupWindow if (show_menubar_option_available()) view_menu->addAction(create_application_action(*view_menu, application.toggle_menu_bar_action(), IncludeActionIcon::No)); - m_bookmarks_menu = create_application_menu(*this, application.bookmarks_menu()); + m_bookmarks_menu = Application::the().qt_bookmarks_menu(); + if (!m_bookmarks_menu) + m_bookmarks_menu = create_application_menu(*this, application.bookmarks_menu()); m_hamburger_menu->addMenu(m_bookmarks_menu); menuBar()->addMenu(m_bookmarks_menu); @@ -401,35 +395,26 @@ BrowserWindow::BrowserWindow(Vector const& initial_urls, IsPopupWindow quit_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Quit)); m_hamburger_menu->addAction(quit_action); file_menu->addAction(quit_action); +#if defined(AK_OS_MACOS) + QObject::connect(quit_action, &QAction::triggered, this, [] { + Application::the().quit(); + }); +#else QObject::connect(quit_action, &QAction::triggered, this, &QMainWindow::close); +#endif - QObject::connect(m_new_tab_action, &QAction::triggered, this, [this] { - auto& tab = new_tab_from_url(WebView::Application::settings().new_tab_page_url(), Web::HTML::ActivateTab::Yes); - tab.set_url_is_hidden(true); - tab.focus_location_editor(); + QObject::connect(m_new_tab_action, &QAction::triggered, this, [] { + Application::the().open_new_tab(); }); QObject::connect(m_new_window_action, &QAction::triggered, this, [] { - auto const& previous_active_window = Application::the().active_window(); - WindowConfiguration configuration { - .width = previous_active_window.width(), - .height = previous_active_window.height(), - .maximized = previous_active_window.isMaximized(), - }; - Application::the().new_window({ WebView::Application::settings().new_tab_page_url() }, configuration); + Application::the().open_new_window(); }); - QObject::connect(m_reopen_recently_closed_tab_action, &QAction::triggered, this, [this] { - auto recently_closed_entry = Application::history_store().pop_most_recently_closed_entry(); - if (recently_closed_entry.has_value()) { - if (recently_closed_entry->was_window) { - auto& window = Application::the().new_window(recently_closed_entry->urls); - window.activate_tab(static_cast(recently_closed_entry->active_tab_index)); - } else if (!recently_closed_entry->urls.is_empty()) { - new_tab_from_url(recently_closed_entry->urls[0], Web::HTML::ActivateTab::Yes); - } - } - Application::the().update_reopen_recently_closed_actions(); + QObject::connect(m_reopen_recently_closed_tab_action, &QAction::triggered, this, [] { + Application::the().reopen_recently_closed_tab(); + }); + QObject::connect(open_file_action, &QAction::triggered, this, [] { + Application::the().open_file(); }); - QObject::connect(open_file_action, &QAction::triggered, this, &BrowserWindow::open_file); m_exit_button = new ExitFullscreenButton { this }; m_fullscreen_mode = new FullscreenMode { this, m_exit_button }; @@ -514,7 +499,8 @@ void BrowserWindow::update_tabs_display() void BrowserWindow::rebuild_bookmarks_menu() { - repopulate_application_menu(*m_bookmarks_menu, *this, Application::the().bookmarks_menu()); + if (m_bookmarks_menu != Application::the().qt_bookmarks_menu()) + repopulate_application_menu(*m_bookmarks_menu, *this, Application::the().bookmarks_menu()); for_each_tab([](Tab& tab) { tab.bookmarks_bar().rebuild(); @@ -751,7 +737,7 @@ void BrowserWindow::update_reopen_recently_closed_action() return; auto recently_closed_entry = Application::history_store().most_recently_closed_entry(); - m_reopen_recently_closed_tab_action->setText(reopen_recently_closed_action_text(recently_closed_entry)); + m_reopen_recently_closed_tab_action->setText("&Reopen Recently Closed Tab"); m_reopen_recently_closed_tab_action->setEnabled(recently_closed_entry.has_value()); } diff --git a/UI/Qt/main.cpp b/UI/Qt/main.cpp index 316009ce9c..3b8cf90748 100644 --- a/UI/Qt/main.cpp +++ b/UI/Qt/main.cpp @@ -53,6 +53,7 @@ ErrorOr ladybird_main(Main::Arguments arguments) #endif auto app = TRY(Ladybird::Application::create(arguments)); + app->initialize_macos_application_menu(); WebView::BrowserProcess browser_process; WebView::copy_default_config_files(Ladybird::Settings::the()->directory()); @@ -68,12 +69,20 @@ ErrorOr ladybird_main(Main::Arguments arguments) } app->on_open_file = [&](auto const& file_url) { - auto& window = app->active_window(); - window.view().load(file_url); + if (auto* window = app->active_window_if_any()) { + window->view().load(file_url); + return; + } + app->new_window({ file_url }); }; browser_process.on_new_tab = [&](auto const& urls) { - auto& window = app->active_window(); + if (!app->active_window_if_any()) { + app->new_window(urls); + return; + } + + auto& window = *app->active_window_if_any(); for (size_t i = 0; i < urls.size(); ++i) { window.new_tab_from_url(urls[i], (i == 0) ? Web::HTML::ActivateTab::Yes : Web::HTML::ActivateTab::No); } @@ -83,12 +92,12 @@ ErrorOr ladybird_main(Main::Arguments arguments) }; browser_process.on_new_window = [&](auto const& urls) { - auto const& previous_active_window = app->active_window(); - Ladybird::WindowConfiguration configuration { - .width = previous_active_window.width(), - .height = previous_active_window.height(), - .maximized = previous_active_window.isMaximized(), - }; + Ladybird::WindowConfiguration configuration {}; + if (auto* previous_active_window = app->active_window_if_any()) { + configuration.width = previous_active_window->width(); + configuration.height = previous_active_window->height(); + configuration.maximized = previous_active_window->isMaximized(); + } app->new_window(urls, configuration); };