UI/Qt: Add functionality for reopening recently-closed tabs

Record when we close a tab, and map Ctrl+Shift+T to an action that opens
the most recently closed one.
This commit is contained in:
Sam Atkins 2026-05-06 13:04:42 +01:00
parent 9b53baa652
commit f0cb7d9a6f
4 changed files with 85 additions and 2 deletions

View file

@ -254,6 +254,14 @@ void Application::update_bookmarks_bar_display(bool show_bookmarks_bar) const
}
}
void Application::update_reopen_recently_closed_actions() const
{
for (auto* widget : QApplication::topLevelWidgets()) {
if (auto* window = as_if<BrowserWindow>(widget))
window->update_reopen_recently_closed_action();
}
}
void Application::show_bookmark_context_menu(Gfx::IntPoint content_position, Optional<WebView::BookmarkItem const&> item, Optional<String const&> target_folder_id)
{
if (auto* active_tab = this->active_tab()) {
@ -423,4 +431,9 @@ void Application::on_devtools_disabled() const
m_active_window->on_devtools_disabled();
}
void Application::on_recently_closed_entries_changed() const
{
update_reopen_recently_closed_actions();
}
}

View file

@ -29,6 +29,7 @@ public:
void set_active_window(BrowserWindow& w) { m_active_window = &w; }
Tab* active_tab() const { return m_active_window ? m_active_window->current_tab() : nullptr; }
void update_reopen_recently_closed_actions() const;
private:
explicit Application();
@ -59,6 +60,7 @@ private:
virtual void on_devtools_enabled() const override;
virtual void on_devtools_disabled() const override;
virtual void on_recently_closed_entries_changed() const override;
OwnPtr<QApplication> m_application;
BrowserWindow* m_active_window { nullptr };

View file

@ -3,7 +3,7 @@
* Copyright (c) 2022, Matthew Costa <ucosty@gmail.com>
* Copyright (c) 2022, Filiph Sandström <filiph.sandstrom@filfatstudios.com>
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024-2026, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2025, Simon Farre <simon.farre.cx@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
@ -12,6 +12,7 @@
#include <AK/RefPtr.h>
#include <AK/TypeCasts.h>
#include <LibWebView/Application.h>
#include <LibWebView/HistoryStore.h>
#include <UI/Qt/Application.h>
#include <UI/Qt/BrowserWindow.h>
#include <UI/Qt/Icon.h>
@ -42,6 +43,25 @@
namespace Ladybird {
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;
urls.ensure_capacity(tabs_container.count());
for (int index = 0; index < tabs_container.count(); ++index)
urls.append(tabs_container.tab(index)->view().url());
return urls;
}
FullscreenMode::FullscreenMode(BrowserWindow* window, ExitFullscreenButton* exit_button)
: QObject(window)
, m_window(window)
@ -216,6 +236,12 @@ BrowserWindow::BrowserWindow(Vector<URL::URL> const& initial_urls, IsPopupWindow
m_hamburger_menu->addAction(m_new_window_action);
file_menu->addAction(m_new_window_action);
m_reopen_recently_closed_tab_action = new QAction("&Reopen Recently Closed Tab", this);
m_reopen_recently_closed_tab_action->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_T));
m_hamburger_menu->addAction(m_reopen_recently_closed_tab_action);
file_menu->addAction(m_reopen_recently_closed_tab_action);
update_reopen_recently_closed_action();
auto* close_current_tab_action = new QAction("&Close Current Tab", this);
close_current_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/close-tab.png"sv));
close_current_tab_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Close));
@ -328,6 +354,18 @@ BrowserWindow::BrowserWindow(Vector<URL::URL> const& initial_urls, IsPopupWindow
QObject::connect(m_new_window_action, &QAction::triggered, this, [] {
(void)Application::the().new_window({ WebView::Application::settings().new_tab_page_url() });
});
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(open_file_action, &QAction::triggered, this, &BrowserWindow::open_file);
m_exit_button = new ExitFullscreenButton { this };
@ -520,11 +558,26 @@ void BrowserWindow::activate_tab(int index)
void BrowserWindow::definitely_close_tab(int index)
{
auto* tab = m_tabs_container->tab(index);
auto url = tab->view().url();
m_tabs_container->remove_tab(index);
Application::history_store().record_closed_tab(url);
Application::the().update_reopen_recently_closed_actions();
tab->deleteLater();
if (m_tabs_container->count() == 0)
if (m_tabs_container->count() == 0) {
m_should_record_closed_window_on_close = false;
close();
}
}
void BrowserWindow::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_action_text(recently_closed_entry));
m_reopen_recently_closed_tab_action->setEnabled(recently_closed_entry.has_value());
}
void BrowserWindow::move_tab(int old_index, int new_index)
@ -804,6 +857,13 @@ void BrowserWindow::wheelEvent(QWheelEvent* event)
void BrowserWindow::closeEvent(QCloseEvent* event)
{
Optional<Vector<URL::URL>> recently_closed_window_urls;
size_t recently_closed_window_active_tab_index { 0 };
if (m_should_record_closed_window_on_close && m_tabs_container->count() > 0) {
recently_closed_window_urls = recently_closed_urls_for_window(*m_tabs_container);
recently_closed_window_active_tab_index = static_cast<size_t>(m_tabs_container->current_index());
}
if (m_is_popup_window == IsPopupWindow::No) {
Settings::the()->set_last_position(pos());
Settings::the()->set_last_size(size());
@ -813,6 +873,11 @@ void BrowserWindow::closeEvent(QCloseEvent* event)
QObject::deleteLater();
QMainWindow::closeEvent(event);
if (event->isAccepted() && recently_closed_window_urls.has_value()) {
Application::history_store().record_closed_window(recently_closed_window_urls.release_value(), recently_closed_window_active_tab_index);
Application::the().update_reopen_recently_closed_actions();
}
}
}

View file

@ -104,6 +104,7 @@ public:
void rebuild_bookmarks_menu();
void update_bookmarks_bar_display(bool show_bookmarks_bar);
void update_reopen_recently_closed_action();
double refresh_rate() const { return m_refresh_rate; }
@ -170,6 +171,7 @@ private:
QAction* m_new_tab_action { nullptr };
QAction* m_new_window_action { nullptr };
QAction* m_reopen_recently_closed_tab_action { nullptr };
QAction* m_find_in_page_action { nullptr };
IsPopupWindow m_is_popup_window { IsPopupWindow::No };
@ -178,6 +180,7 @@ private:
FullscreenMode* m_fullscreen_mode { nullptr };
// Determine if window should restore to maximized or normal, when exiting fullscreen.
bool m_restore_to_maximized { false };
bool m_should_record_closed_window_on_close { true };
};
}