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.
This commit is contained in:
Andreas Kling 2026-06-21 12:28:23 +02:00 committed by Andreas Kling
parent d707d112a2
commit b91ff9ebfd
6 changed files with 303 additions and 48 deletions

View file

@ -1683,6 +1683,8 @@ void Application::create_bookmark_menu_items(Optional<MenuData> 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);

View file

@ -111,7 +111,7 @@ public:
virtual Optional<ViewImplementation&> active_web_view() const { return {}; }
virtual Optional<ViewImplementation&> 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; }

View file

@ -6,10 +6,12 @@
#include <LibCore/ArgsParser.h>
#include <LibURL/InternalURLs.h>
#include <LibWebView/HistoryStore.h>
#include <LibWebView/URL.h>
#include <UI/Qt/Application.h>
#include <UI/Qt/ChromeStyle.h>
#include <UI/Qt/EventLoopImplementationQt.h>
#include <UI/Qt/Menu.h>
#include <UI/Qt/Settings.h>
#include <UI/Qt/StringUtils.h>
#include <UI/Qt/WebContentView.h>
@ -18,6 +20,7 @@
# include <UI/Qt/MacWindow.h>
#endif
#include <QAction>
#include <QClipboard>
#include <QDesktopServices>
#include <QDialog>
@ -25,10 +28,14 @@
#include <QFileDialog>
#include <QFileOpenEvent>
#include <QFormLayout>
#include <QKeySequence>
#include <QLineEdit>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QMimeData>
#include <QStandardPaths>
#include <QTimer>
#if defined(AK_OS_WINDOWS)
# include <AK/Windows.h>
@ -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<QKeySequence> 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<URL::URL> 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<URL::URL> 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<int>(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<BrowserWindow>(widget) && widget->isVisible())
return;
}
QApplication::quit();
}
void Application::initialize_macos_application_menu()
{
#if defined(AK_OS_MACOS)
if (m_application)
static_cast<LadybirdQApplication*>(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<LadybirdQApplication*>(m_application.ptr())->bookmarks_menu();
#endif
return nullptr;
}
Optional<WebView::ViewImplementation&> Application::active_web_view() const
{
if (auto* active_tab = this->active_tab())
@ -173,10 +392,27 @@ Optional<WebView::ViewImplementation&> Application::active_web_view() const
Optional<WebView::ViewImplementation&> Application::open_blank_new_tab(Web::HTML::ActivateTab activate_tab) const
{
if (!m_active_window) {
auto& window = const_cast<Application&>(*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<Application&>(*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<LadybirdQApplication*>(m_application.ptr())->rebuild_bookmarks_menu();
#endif
for (auto* widget : QApplication::topLevelWidgets()) {
if (auto* window = as_if<BrowserWindow>(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<LadybirdQApplication*>(m_application.ptr())->update_reopen_recently_closed_action();
#endif
for (auto* widget : QApplication::topLevelWidgets()) {
if (auto* window = as_if<BrowserWindow>(widget))
window->update_reopen_recently_closed_action();

View file

@ -13,6 +13,8 @@
#include <QApplication>
class QMenu;
namespace Ladybird {
struct WindowConfiguration {
@ -31,9 +33,18 @@ public:
Function<void(URL::URL)> on_open_file;
BrowserWindow& new_window(Vector<URL::URL> const& initial_urls, WindowConfiguration const& = {}, BrowserWindow::IsPopupWindow is_popup_window = BrowserWindow::IsPopupWindow::No, Tab* parent_tab = nullptr, Optional<u64> 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<WebView::ViewImplementation&> active_web_view() const override;
virtual Optional<WebView::ViewImplementation&> 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;

View file

@ -90,14 +90,6 @@ static Optional<u64> display_id_for_screen(QScreen* screen)
});
}
static QString reopen_recently_closed_action_text(Optional<WebView::RecentlyClosedEntry const&> entry)
{
if (entry.has_value() && entry->was_window)
return "&Reopen Recently Closed Window";
return "&Reopen Recently Closed Tab";
}
static Vector<URL::URL> recently_closed_urls_for_window(TabWidget const& tabs_container)
{
Vector<URL::URL> urls;
@ -373,6 +365,8 @@ BrowserWindow::BrowserWindow(Vector<URL::URL> 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 = 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<URL::URL> 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<int>(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,6 +499,7 @@ void BrowserWindow::update_tabs_display()
void BrowserWindow::rebuild_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) {
@ -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());
}

View file

@ -53,6 +53,7 @@ ErrorOr<int> 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<int> 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<int> 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);
};