ladybird/UI/Qt/Tab.cpp

517 lines
18 KiB
C++
Raw Normal View History

/*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022, Matthew Costa <ucosty@gmail.com>
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibURL/URL.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWebView/Application.h>
#include <UI/Qt/BrowserWindow.h>
#include <UI/Qt/Icon.h>
#include <UI/Qt/Menu.h>
#include <UI/Qt/Settings.h>
#include <UI/Qt/StringUtils.h>
#include <QColorDialog>
2023-05-25 23:47:12 +02:00
#include <QFileDialog>
#include <QFont>
#include <QFontMetrics>
#include <QImage>
#include <QInputDialog>
#include <QMenu>
#include <QMessageBox>
#include <QMimeData>
#include <QMimeDatabase>
#include <QResizeEvent>
namespace Ladybird {
static QIcon default_favicon()
{
static QIcon icon = load_icon_from_uri("resource://icons/48x48/app-browser.png"sv);
return icon;
}
Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client, size_t page_index)
: QWidget(window)
, m_window(window)
{
m_layout = new QBoxLayout(QBoxLayout::Direction::TopToBottom, this);
m_layout->setSpacing(0);
m_layout->setContentsMargins(0, 0, 0, 0);
auto view_initial_state = WebContentViewInitialState {
.maximum_frames_per_second = window->refresh_rate(),
};
m_view = new WebContentView(this, parent_client, page_index, AK::move(view_initial_state));
2024-05-29 20:12:21 +01:00
m_find_in_page = new FindInPageWidget(this, m_view);
m_find_in_page->setVisible(false);
m_toolbar = new QToolBar(this);
m_location_edit = new LocationEdit(this);
m_hover_label = new HyperlinkLabel(this);
m_hover_label->hide();
m_hover_label->setFrameShape(QFrame::Shape::Box);
m_hover_label->setAutoFillBackground(true);
QObject::connect(m_hover_label, &HyperlinkLabel::mouse_entered, [this] {
update_hover_label();
});
auto* focus_location_editor_action = new QAction("Edit Location", this);
focus_location_editor_action->setShortcuts({ QKeySequence("Ctrl+L"), QKeySequence("Alt+D") });
addAction(focus_location_editor_action);
m_layout->addWidget(m_toolbar);
m_layout->addWidget(m_view);
2024-05-29 20:12:21 +01:00
m_layout->addWidget(m_find_in_page);
m_hamburger_button = new QToolButton(m_toolbar);
m_hamburger_button->setText("Show &Menu");
m_hamburger_button->setToolTip("Show Menu");
m_hamburger_button->setIcon(create_tvg_icon_with_theme_colors("hamburger", palette()));
m_hamburger_button->setPopupMode(QToolButton::InstantPopup);
m_hamburger_button->setMenu(&m_window->hamburger_menu());
m_hamburger_button->setStyleSheet(":menu-indicator {image: none}");
m_navigate_back_action = create_application_action(*this, view().navigate_back_action());
m_navigate_forward_action = create_application_action(*this, view().navigate_forward_action());
m_reload_action = create_application_action(*this, WebView::Application::the().reload_action());
recreate_toolbar_icons();
m_favicon = default_favicon();
m_page_context_menu = create_context_menu(*this, view(), view().page_context_menu());
m_link_context_menu = create_context_menu(*this, view(), view().link_context_menu());
m_image_context_menu = create_context_menu(*this, view(), view().image_context_menu());
m_media_context_menu = create_context_menu(*this, view(), view().media_context_menu());
m_toolbar->addAction(m_navigate_back_action);
m_toolbar->addAction(m_navigate_forward_action);
m_toolbar->addAction(m_reload_action);
m_toolbar->addWidget(m_location_edit);
m_toolbar->addAction(create_application_action(*m_toolbar, view().reset_zoom_action()));
m_hamburger_button_action = m_toolbar->addWidget(m_hamburger_button);
m_toolbar->setIconSize({ 16, 16 });
m_toolbar->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
// This is a little awkward, but without this Qt shrinks the button to the size of the icon.
// Note: toolButtonStyle="0" -> ToolButtonIconOnly.
m_toolbar->setStyleSheet("QToolButton[toolButtonStyle=\"0\"]{width:24px;height:24px}");
m_hamburger_button_action->setVisible(!Settings::the()->show_menubar());
QObject::connect(Settings::the(), &Settings::show_menubar_changed, this, [this](bool show_menubar) {
m_hamburger_button_action->setVisible(!show_menubar);
});
view().on_activate_tab = [this] {
m_window->activate_tab(tab_index());
};
view().on_close = [this] {
m_window->close_tab(tab_index());
};
view().on_link_hover = [this](auto const& url) {
m_hover_label->setText(qstring_from_ak_string(url.to_byte_string()));
update_hover_label();
m_hover_label->show();
};
view().on_link_unhover = [this]() {
m_hover_label->hide();
};
view().on_load_start = [this](URL::URL const& url, bool) {
auto url_serialized = qstring_from_ak_string(url.serialize());
m_title = url_serialized;
emit title_changed(tab_index(), url_serialized);
m_favicon = default_favicon();
emit favicon_changed(tab_index(), m_favicon);
m_location_edit->set_url(url);
m_location_edit->setCursorPosition(0);
};
view().on_url_change = [this](auto const& url) {
m_location_edit->set_url(url);
};
QObject::connect(m_location_edit, &QLineEdit::returnPressed, this, &Tab::location_edit_return_pressed);
view().on_title_change = [this](auto const& title) {
m_title = qstring_from_utf16_string(title);
emit title_changed(tab_index(), m_title);
};
view().on_favicon_change = [this](auto const& bitmap) {
auto qimage = QImage(bitmap.scanline_u8(0), bitmap.width(), bitmap.height(), QImage::Format_ARGB32);
if (qimage.isNull())
return;
auto qpixmap = QPixmap::fromImage(qimage);
if (qpixmap.isNull())
return;
m_favicon = qpixmap;
emit favicon_changed(tab_index(), m_favicon);
};
view().on_request_alert = [this](auto const& message) {
m_dialog = new QMessageBox(QMessageBox::Icon::Warning, "Ladybird", qstring_from_ak_string(message), QMessageBox::StandardButton::Ok, &view());
QObject::connect(m_dialog, &QDialog::finished, this, [this]() {
view().alert_closed();
m_dialog = nullptr;
});
m_dialog->open();
};
view().on_request_confirm = [this](auto const& message) {
m_dialog = new QMessageBox(QMessageBox::Icon::Question, "Ladybird", qstring_from_ak_string(message), QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel, &view());
QObject::connect(m_dialog, &QDialog::finished, this, [this](auto result) {
view().confirm_closed(result == QMessageBox::StandardButton::Ok || result == QDialog::Accepted);
m_dialog = nullptr;
});
m_dialog->open();
};
view().on_request_prompt = [this](auto const& message, auto const& default_) {
m_dialog = new QInputDialog(&view());
auto& dialog = static_cast<QInputDialog&>(*m_dialog);
dialog.setWindowTitle("Ladybird");
dialog.setLabelText(qstring_from_ak_string(message));
dialog.setTextValue(qstring_from_ak_string(default_));
QObject::connect(m_dialog, &QDialog::finished, this, [this](auto result) {
if (result == QDialog::Accepted) {
auto& dialog = static_cast<QInputDialog&>(*m_dialog);
view().prompt_closed(ak_string_from_qstring(dialog.textValue()));
} else {
view().prompt_closed({});
}
m_dialog = nullptr;
});
m_dialog->open();
};
view().on_request_set_prompt_text = [this](auto const& message) {
if (m_dialog && is<QInputDialog>(*m_dialog))
static_cast<QInputDialog&>(*m_dialog).setTextValue(qstring_from_ak_string(message));
};
view().on_request_accept_dialog = [this]() {
if (m_dialog)
m_dialog->accept();
};
view().on_request_dismiss_dialog = [this]() {
if (m_dialog)
m_dialog->reject();
};
view().on_request_color_picker = [this](Color current_color) {
m_dialog = new QColorDialog(QColor(current_color.red(), current_color.green(), current_color.blue()), &view());
auto& dialog = static_cast<QColorDialog&>(*m_dialog);
dialog.setWindowTitle("Ladybird");
dialog.setOption(QColorDialog::ShowAlphaChannel, false);
QObject::connect(&dialog, &QColorDialog::currentColorChanged, this, [this](QColor const& color) {
view().color_picker_update(Color(color.red(), color.green(), color.blue()), Web::HTML::ColorPickerUpdateState::Update);
});
QObject::connect(m_dialog, &QDialog::finished, this, [this](auto result) {
if (result == QDialog::Accepted) {
auto& dialog = static_cast<QColorDialog&>(*m_dialog);
view().color_picker_update(Color(dialog.selectedColor().red(), dialog.selectedColor().green(), dialog.selectedColor().blue()), Web::HTML::ColorPickerUpdateState::Closed);
} else {
view().color_picker_update({}, Web::HTML::ColorPickerUpdateState::Closed);
}
m_dialog = nullptr;
});
m_dialog->open();
};
view().on_request_file_picker = [this](auto const& accepted_file_types, auto allow_multiple_files) {
Vector<Web::HTML::SelectedFile> selected_files;
auto create_selected_file = [&](auto const& qfile_path) {
auto file_path = ak_byte_string_from_qstring(qfile_path);
if (auto file = Web::HTML::SelectedFile::from_file_path(file_path); file.is_error())
warnln("Unable to open file {}: {}", file_path, file.error());
else
selected_files.append(file.release_value());
};
QStringList accepted_file_filters;
QMimeDatabase mime_database;
for (auto const& filter : accepted_file_types.filters) {
filter.visit(
[&](Web::HTML::FileFilter::FileType type) {
QString title;
QString filter;
switch (type) {
case Web::HTML::FileFilter::FileType::Audio:
title = "Audio files";
filter = "audio/";
break;
case Web::HTML::FileFilter::FileType::Image:
title = "Image files";
filter = "image/";
break;
case Web::HTML::FileFilter::FileType::Video:
title = "Video files";
filter = "video/";
break;
}
QStringList extensions;
for (auto const& mime_type : mime_database.allMimeTypes()) {
if (mime_type.name().startsWith(filter))
extensions.append(mime_type.globPatterns());
}
accepted_file_filters.append(QString("%1 (%2)").arg(title, extensions.join(" ")));
},
[&](Web::HTML::FileFilter::MimeType const& filter) {
if (auto mime_type = mime_database.mimeTypeForName(qstring_from_ak_string(filter.value)); mime_type.isValid())
accepted_file_filters.append(mime_type.filterString());
},
[&](Web::HTML::FileFilter::Extension const& filter) {
auto extension = MUST(String::formatted("*.{}", filter.value));
accepted_file_filters.append(qstring_from_ak_string(extension));
});
}
accepted_file_filters.size() > 1 ? accepted_file_filters.prepend("All files (*)") : accepted_file_filters.append("All files (*)");
auto filters = accepted_file_filters.join(";;");
if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) {
auto paths = QFileDialog::getOpenFileNames(this, "Select files", QDir::homePath(), filters);
selected_files.ensure_capacity(static_cast<size_t>(paths.size()));
for (auto const& path : paths)
create_selected_file(path);
} else {
auto path = QFileDialog::getOpenFileName(this, "Select file", QDir::homePath(), filters);
create_selected_file(path);
}
view().file_picker_closed(std::move(selected_files));
};
view().on_find_in_page = [this](auto current_match_index, auto const& total_match_count) {
m_find_in_page->update_result_label(current_match_index, total_match_count);
};
QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor);
view().on_restore_window = [this]() {
m_window->showNormal();
};
view().on_reposition_window = [this](auto const& position) {
m_window->move(position.x(), position.y());
view().did_update_window_rect();
};
view().on_resize_window = [this](auto const& size) {
m_window->resize(size.width(), size.height());
view().did_update_window_rect();
};
view().on_maximize_window = [this]() {
m_window->showMaximized();
view().did_update_window_rect();
};
view().on_minimize_window = [this]() {
m_window->showMinimized();
};
view().on_fullscreen_window = [this]() {
m_window->showFullScreen();
view().did_update_window_rect();
};
view().on_audio_play_state_changed = [this](auto play_state) {
emit audio_play_state_changed(tab_index(), play_state);
};
auto* duplicate_tab_action = new QAction("&Duplicate Tab", this);
QObject::connect(duplicate_tab_action, &QAction::triggered, this, [this]() {
m_window->new_tab_from_url(view().url(), Web::HTML::ActivateTab::Yes);
});
auto* move_to_start_action = new QAction("Move to &Start", this);
QObject::connect(move_to_start_action, &QAction::triggered, this, [this]() {
m_window->move_tab(tab_index(), 0);
});
auto* move_to_end_action = new QAction("Move to &End", this);
QObject::connect(move_to_end_action, &QAction::triggered, this, [this]() {
m_window->move_tab(tab_index(), m_window->tab_count() - 1);
});
auto* close_tab_action = new QAction("&Close Tab", this);
QObject::connect(close_tab_action, &QAction::triggered, this, [this]() {
view().on_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);
}
});
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);
}
});
auto* close_other_tabs_action = new QAction("Cl&ose Other Tabs", this);
QObject::connect(close_other_tabs_action, &QAction::triggered, this, [this]() {
for (auto i = m_window->tab_count() - 1; i >= 0; i--) {
if (i == tab_index())
continue;
m_window->close_tab(i);
}
});
m_context_menu = new QMenu("Context menu", this);
m_context_menu->addAction(create_application_action(*this, WebView::Application::the().reload_action()));
m_context_menu->addAction(duplicate_tab_action);
m_context_menu->addSeparator();
auto* move_tab_menu = m_context_menu->addMenu("Mo&ve Tab");
move_tab_menu->addAction(move_to_start_action);
move_tab_menu->addAction(move_to_end_action);
m_context_menu->addSeparator();
m_context_menu->addAction(close_tab_action);
auto* close_multiple_tabs_menu = m_context_menu->addMenu("Close &Multiple Tabs");
close_multiple_tabs_menu->addAction(close_tabs_to_left_action);
close_multiple_tabs_menu->addAction(close_tabs_to_right_action);
close_multiple_tabs_menu->addAction(close_other_tabs_action);
}
Tab::~Tab() = default;
void Tab::focus_location_editor()
{
m_location_edit->setFocus();
m_location_edit->selectAll();
}
void Tab::navigate(URL::URL const& url)
{
view().load(url);
}
void Tab::load_html(StringView html)
{
view().load_html(html);
}
void Tab::location_edit_return_pressed()
{
if (m_location_edit->text().isEmpty())
return;
navigate(m_location_edit->url());
}
2023-05-25 23:47:12 +02:00
void Tab::open_file()
{
auto filename = QFileDialog::getOpenFileUrl(this, "Open file", QDir::homePath(), "All Files (*.*)");
if (filename.isValid()) {
navigate(ak_url_from_qurl(filename));
}
2023-05-25 23:47:12 +02:00
}
int Tab::tab_index()
{
return m_window->tab_index(this);
}
void Tab::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
if (m_hover_label->isVisible())
update_hover_label();
}
void Tab::update_hover_label()
{
m_hover_label->setText(QFontMetrics(m_hover_label->font()).elidedText(m_hover_label->text(), Qt::ElideRight, width() / 2 - 10));
m_hover_label->resize(QFontMetrics(m_hover_label->font()).boundingRect(m_hover_label->text()).adjusted(-4, -2, 4, 2).size());
auto hover_label_height = height() - m_hover_label->height();
2024-05-29 20:12:21 +01:00
if (m_find_in_page->isVisible())
hover_label_height -= m_find_in_page->height();
if (m_hover_label->underMouse() && m_hover_label->x() == 0)
m_hover_label->move(width() / 2 + (width() / 2 - m_hover_label->width()), hover_label_height);
else
m_hover_label->move(0, hover_label_height);
2024-05-29 20:12:21 +01:00
m_hover_label->raise();
}
bool Tab::event(QEvent* event)
{
if (event->type() == QEvent::PaletteChange) {
recreate_toolbar_icons();
return QWidget::event(event);
}
return QWidget::event(event);
}
void Tab::recreate_toolbar_icons()
{
m_navigate_back_action->setIcon(create_tvg_icon_with_theme_colors("back", palette()));
m_navigate_forward_action->setIcon(create_tvg_icon_with_theme_colors("forward", palette()));
m_reload_action->setIcon(create_tvg_icon_with_theme_colors("reload", palette()));
m_window->new_tab_action().setIcon(create_tvg_icon_with_theme_colors("new_tab", palette()));
m_hamburger_button->setIcon(create_tvg_icon_with_theme_colors("hamburger", palette()));
}
2024-05-29 20:12:21 +01:00
void Tab::show_find_in_page()
{
m_find_in_page->setVisible(true);
m_find_in_page->setFocus();
}
void Tab::find_previous()
{
m_find_in_page->find_previous();
}
void Tab::find_next()
{
m_find_in_page->find_next();
}
}