mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-11 05:25:32 +03:00
Previously, line box borders were only set for the current tab when the option was toggled.
965 lines
38 KiB
C++
965 lines
38 KiB
C++
/*
|
|
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
|
|
* Copyright (c) 2022, Matthew Costa <ucosty@gmail.com>
|
|
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "BrowserWindow.h"
|
|
#include "Icon.h"
|
|
#include "InspectorWidget.h"
|
|
#include "Settings.h"
|
|
#include "StringUtils.h"
|
|
#include <AK/TemporaryChange.h>
|
|
#include <LibGfx/ImageFormats/BMPWriter.h>
|
|
#include <LibGfx/Painter.h>
|
|
#include <LibWeb/HTML/SelectedFile.h>
|
|
#include <LibWebView/SearchEngine.h>
|
|
#include <LibWebView/SourceHighlighter.h>
|
|
#include <LibWebView/URL.h>
|
|
#include <QClipboard>
|
|
#include <QColorDialog>
|
|
#include <QCoreApplication>
|
|
#include <QCursor>
|
|
#include <QDesktopServices>
|
|
#include <QFileDialog>
|
|
#include <QFont>
|
|
#include <QFontMetrics>
|
|
#include <QGuiApplication>
|
|
#include <QImage>
|
|
#include <QInputDialog>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QMimeData>
|
|
#include <QMimeDatabase>
|
|
#include <QMimeType>
|
|
#include <QPainter>
|
|
#include <QPoint>
|
|
#include <QPushButton>
|
|
#include <QResizeEvent>
|
|
|
|
namespace Ladybird {
|
|
|
|
static QIcon default_favicon()
|
|
{
|
|
static QIcon icon = load_icon_from_uri("resource://icons/16x16/app-browser.png"sv);
|
|
return icon;
|
|
}
|
|
|
|
Tab::Tab(BrowserWindow* window, WebContentOptions const& web_content_options, StringView webdriver_content_ipc_path, 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);
|
|
|
|
m_view = new WebContentView(this, web_content_options, webdriver_content_ipc_path, parent_client, page_index);
|
|
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 QLabel(this);
|
|
m_hover_label->hide();
|
|
m_hover_label->setFrameShape(QFrame::Shape::Box);
|
|
m_hover_label->setAutoFillBackground(true);
|
|
|
|
auto* focus_location_editor_action = new QAction("Edit Location", this);
|
|
focus_location_editor_action->setShortcut(QKeySequence("Ctrl+L"));
|
|
addAction(focus_location_editor_action);
|
|
|
|
m_layout->addWidget(m_toolbar);
|
|
m_layout->addWidget(m_view);
|
|
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}");
|
|
|
|
recreate_toolbar_icons();
|
|
|
|
m_favicon = default_favicon();
|
|
|
|
m_toolbar->addAction(&m_window->go_back_action());
|
|
m_toolbar->addAction(&m_window->go_forward_action());
|
|
m_toolbar->addAction(&m_window->reload_action());
|
|
m_toolbar->addWidget(m_location_edit);
|
|
m_toolbar->addAction(&m_window->new_tab_action());
|
|
m_hamburger_button_action = m_toolbar->addWidget(m_hamburger_button);
|
|
m_toolbar->setIconSize({ 16, 16 });
|
|
// 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);
|
|
});
|
|
|
|
m_reset_zoom_button = new QToolButton(m_toolbar);
|
|
m_reset_zoom_button->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
|
m_reset_zoom_button->setToolTip("Reset zoom level");
|
|
m_reset_zoom_button_action = m_toolbar->addWidget(m_reset_zoom_button);
|
|
m_reset_zoom_button_action->setVisible(false);
|
|
|
|
QObject::connect(m_reset_zoom_button, &QAbstractButton::clicked, [this] {
|
|
view().reset_zoom();
|
|
update_reset_zoom_button();
|
|
m_window->update_zoom_menu();
|
|
});
|
|
|
|
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](const URL::URL& url, bool) {
|
|
if (m_inspector_widget)
|
|
m_inspector_widget->reset();
|
|
|
|
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->setText(url_serialized);
|
|
m_location_edit->setCursorPosition(0);
|
|
};
|
|
|
|
view().on_load_finish = [this](auto&) {
|
|
if (m_inspector_widget != nullptr && m_inspector_widget->isVisible())
|
|
m_inspector_widget->inspect();
|
|
};
|
|
|
|
view().on_url_change = [this](auto const& url) {
|
|
m_location_edit->setText(qstring_from_ak_string(url.serialize()));
|
|
};
|
|
|
|
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_ak_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());
|
|
m_dialog->exec();
|
|
|
|
view().alert_closed();
|
|
m_dialog = nullptr;
|
|
};
|
|
|
|
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());
|
|
auto result = m_dialog->exec();
|
|
|
|
view().confirm_closed(result == QMessageBox::StandardButton::Ok || result == QDialog::Accepted);
|
|
m_dialog = nullptr;
|
|
};
|
|
|
|
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_));
|
|
|
|
if (dialog.exec() == QDialog::Accepted)
|
|
view().prompt_closed(ak_string_from_qstring(dialog.textValue()));
|
|
else
|
|
view().prompt_closed({});
|
|
|
|
m_dialog = nullptr;
|
|
};
|
|
|
|
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);
|
|
});
|
|
|
|
if (dialog.exec() == QDialog::Accepted)
|
|
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;
|
|
};
|
|
|
|
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));
|
|
};
|
|
|
|
m_select_dropdown = new QMenu("Select Dropdown", this);
|
|
QObject::connect(m_select_dropdown, &QMenu::aboutToHide, this, [this]() {
|
|
if (!m_select_dropdown->activeAction())
|
|
view().select_dropdown_closed({});
|
|
});
|
|
|
|
view().on_request_select_dropdown = [this](Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items) {
|
|
m_select_dropdown->clear();
|
|
m_select_dropdown->setMinimumWidth(minimum_width / view().device_pixel_ratio());
|
|
|
|
auto add_menu_item = [this](Web::HTML::SelectItemOption const& item_option, bool in_option_group) {
|
|
QAction* action = new QAction(qstring_from_ak_string(in_option_group ? MUST(String::formatted(" {}", item_option.label)) : item_option.label), this);
|
|
action->setCheckable(true);
|
|
action->setChecked(item_option.selected);
|
|
action->setDisabled(item_option.disabled);
|
|
action->setData(QVariant(static_cast<uint>(item_option.id)));
|
|
QObject::connect(action, &QAction::triggered, this, &Tab::select_dropdown_action);
|
|
m_select_dropdown->addAction(action);
|
|
};
|
|
|
|
for (auto const& item : items) {
|
|
if (item.has<Web::HTML::SelectItemOptionGroup>()) {
|
|
auto const& item_option_group = item.get<Web::HTML::SelectItemOptionGroup>();
|
|
QAction* subtitle = new QAction(qstring_from_ak_string(item_option_group.label), this);
|
|
subtitle->setDisabled(true);
|
|
m_select_dropdown->addAction(subtitle);
|
|
|
|
for (auto const& item_option : item_option_group.items)
|
|
add_menu_item(item_option, true);
|
|
}
|
|
|
|
if (item.has<Web::HTML::SelectItemOption>())
|
|
add_menu_item(item.get<Web::HTML::SelectItemOption>(), false);
|
|
|
|
if (item.has<Web::HTML::SelectItemSeparator>())
|
|
m_select_dropdown->addSeparator();
|
|
}
|
|
|
|
m_select_dropdown->exec(view().map_point_to_global_position(content_position));
|
|
};
|
|
|
|
QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor);
|
|
|
|
view().on_received_source = [this](auto const& url, auto const& source) {
|
|
auto html = WebView::highlight_source(url, source);
|
|
m_window->new_tab_from_content(html, Web::HTML::ActivateTab::Yes);
|
|
};
|
|
|
|
view().on_navigate_back = [this]() {
|
|
back();
|
|
};
|
|
|
|
view().on_navigate_forward = [this]() {
|
|
forward();
|
|
};
|
|
|
|
view().on_refresh = [this]() {
|
|
reload();
|
|
};
|
|
|
|
view().on_restore_window = [this]() {
|
|
m_window->showNormal();
|
|
};
|
|
|
|
view().on_reposition_window = [this](auto const& position) {
|
|
m_window->move(position.x(), position.y());
|
|
return Gfx::IntPoint { m_window->x(), m_window->y() };
|
|
};
|
|
|
|
view().on_resize_window = [this](auto const& size) {
|
|
m_window->resize(size.width(), size.height());
|
|
return Gfx::IntSize { m_window->width(), m_window->height() };
|
|
};
|
|
|
|
view().on_maximize_window = [this]() {
|
|
m_window->showMaximized();
|
|
return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
|
|
};
|
|
|
|
view().on_minimize_window = [this]() {
|
|
m_window->showMinimized();
|
|
return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
|
|
};
|
|
|
|
view().on_fullscreen_window = [this]() {
|
|
m_window->showFullScreen();
|
|
return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
|
|
};
|
|
|
|
view().on_insert_clipboard_entry = [](auto const& data, auto const&, auto const& mime_type) {
|
|
QByteArray qdata { data.bytes_as_string_view().characters_without_null_termination(), static_cast<qsizetype>(data.bytes_as_string_view().length()) };
|
|
|
|
auto* mime_data = new QMimeData();
|
|
mime_data->setData(qstring_from_ak_string(mime_type), qdata);
|
|
|
|
auto* clipboard = QGuiApplication::clipboard();
|
|
clipboard->setMimeData(mime_data);
|
|
};
|
|
|
|
view().on_audio_play_state_changed = [this](auto play_state) {
|
|
emit audio_play_state_changed(tab_index(), play_state);
|
|
};
|
|
|
|
view().on_navigation_buttons_state_changed = [this](auto back_enabled, auto forward_enabled) {
|
|
m_can_navigate_back = back_enabled;
|
|
m_can_navigate_forward = forward_enabled;
|
|
emit navigation_buttons_state_changed(tab_index());
|
|
};
|
|
|
|
auto* reload_tab_action = new QAction("&Reload Tab", this);
|
|
QObject::connect(reload_tab_action, &QAction::triggered, this, [this]() {
|
|
reload();
|
|
});
|
|
|
|
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(reload_tab_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);
|
|
|
|
auto* search_selected_text_action = new QAction("&Search for <query>", this);
|
|
search_selected_text_action->setIcon(load_icon_from_uri("resource://icons/16x16/find.png"sv));
|
|
QObject::connect(search_selected_text_action, &QAction::triggered, this, [this]() {
|
|
auto url = MUST(String::formatted(Settings::the()->search_engine().query_url, URL::percent_encode(*m_page_context_menu_search_text)));
|
|
m_window->new_tab_from_url(URL::URL(url), Web::HTML::ActivateTab::Yes);
|
|
});
|
|
|
|
auto take_screenshot = [this](auto type) {
|
|
auto& view = this->view();
|
|
|
|
view.take_screenshot(type)
|
|
->when_resolved([this](auto const& path) {
|
|
auto message = MUST(String::formatted("Screenshot saved to: {}", path));
|
|
|
|
QMessageBox dialog(this);
|
|
dialog.setWindowTitle("Ladybird");
|
|
dialog.setIcon(QMessageBox::Information);
|
|
dialog.setText(qstring_from_ak_string(message));
|
|
dialog.addButton(QMessageBox::Ok);
|
|
dialog.addButton(QMessageBox::Open)->setText("Open folder");
|
|
|
|
if (dialog.exec() == QMessageBox::Open) {
|
|
auto path_url = QUrl::fromLocalFile(qstring_from_ak_string(path.dirname()));
|
|
QDesktopServices::openUrl(path_url);
|
|
}
|
|
})
|
|
.when_rejected([this](auto const& error) {
|
|
auto error_message = MUST(String::formatted("{}", error));
|
|
QMessageBox::warning(this, "Ladybird", qstring_from_ak_string(error_message));
|
|
});
|
|
};
|
|
|
|
auto* take_visible_screenshot_action = new QAction("Take &Visible Screenshot", this);
|
|
take_visible_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
|
|
QObject::connect(take_visible_screenshot_action, &QAction::triggered, this, [take_screenshot]() {
|
|
take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible);
|
|
});
|
|
|
|
auto* take_full_screenshot_action = new QAction("Take &Full Screenshot", this);
|
|
take_full_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
|
|
QObject::connect(take_full_screenshot_action, &QAction::triggered, this, [take_screenshot]() {
|
|
take_screenshot(WebView::ViewImplementation::ScreenshotType::Full);
|
|
});
|
|
|
|
m_page_context_menu = new QMenu("Context menu", this);
|
|
m_page_context_menu->addAction(&m_window->go_back_action());
|
|
m_page_context_menu->addAction(&m_window->go_forward_action());
|
|
m_page_context_menu->addAction(&m_window->reload_action());
|
|
m_page_context_menu->addSeparator();
|
|
m_page_context_menu->addAction(&m_window->copy_selection_action());
|
|
m_page_context_menu->addAction(&m_window->paste_action());
|
|
m_page_context_menu->addAction(&m_window->select_all_action());
|
|
m_page_context_menu->addSeparator();
|
|
m_page_context_menu->addAction(search_selected_text_action);
|
|
m_page_context_menu->addSeparator();
|
|
m_page_context_menu->addAction(take_visible_screenshot_action);
|
|
m_page_context_menu->addAction(take_full_screenshot_action);
|
|
m_page_context_menu->addSeparator();
|
|
m_page_context_menu->addAction(&m_window->view_source_action());
|
|
m_page_context_menu->addAction(&m_window->inspect_dom_node_action());
|
|
|
|
view().on_context_menu_request = [this, search_selected_text_action](Gfx::IntPoint content_position) {
|
|
auto selected_text = Settings::the()->enable_search()
|
|
? view().selected_text_with_whitespace_collapsed()
|
|
: OptionalNone {};
|
|
TemporaryChange change_url { m_page_context_menu_search_text, std::move(selected_text) };
|
|
|
|
if (m_page_context_menu_search_text.has_value()) {
|
|
auto action_text = WebView::format_search_query_for_display(Settings::the()->search_engine().query_url, *m_page_context_menu_search_text);
|
|
search_selected_text_action->setText(qstring_from_ak_string(action_text));
|
|
search_selected_text_action->setVisible(true);
|
|
} else {
|
|
search_selected_text_action->setVisible(false);
|
|
}
|
|
|
|
m_page_context_menu->exec(view().map_point_to_global_position(content_position));
|
|
};
|
|
|
|
auto* open_link_action = new QAction("&Open", this);
|
|
open_link_action->setIcon(load_icon_from_uri("resource://icons/16x16/go-forward.png"sv));
|
|
QObject::connect(open_link_action, &QAction::triggered, this, [this]() {
|
|
open_link(m_link_context_menu_url);
|
|
});
|
|
|
|
auto* open_link_in_new_tab_action = new QAction("&Open in New &Tab", this);
|
|
open_link_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
|
|
QObject::connect(open_link_in_new_tab_action, &QAction::triggered, this, [this]() {
|
|
open_link_in_new_tab(m_link_context_menu_url);
|
|
});
|
|
|
|
m_link_context_menu_copy_url_action = new QAction("Copy &URL", this);
|
|
m_link_context_menu_copy_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
|
|
QObject::connect(m_link_context_menu_copy_url_action, &QAction::triggered, this, [this]() {
|
|
copy_link_url(m_link_context_menu_url);
|
|
});
|
|
|
|
m_link_context_menu = new QMenu("Link context menu", this);
|
|
m_link_context_menu->addAction(open_link_action);
|
|
m_link_context_menu->addAction(open_link_in_new_tab_action);
|
|
m_link_context_menu->addSeparator();
|
|
m_link_context_menu->addAction(m_link_context_menu_copy_url_action);
|
|
m_link_context_menu->addSeparator();
|
|
m_link_context_menu->addAction(&m_window->inspect_dom_node_action());
|
|
|
|
view().on_link_context_menu_request = [this](auto const& url, Gfx::IntPoint content_position) {
|
|
m_link_context_menu_url = url;
|
|
|
|
switch (WebView::url_type(url)) {
|
|
case WebView::URLType::Email:
|
|
m_link_context_menu_copy_url_action->setText("Copy &Email Address");
|
|
break;
|
|
case WebView::URLType::Telephone:
|
|
m_link_context_menu_copy_url_action->setText("Copy &Phone Number");
|
|
break;
|
|
case WebView::URLType::Other:
|
|
m_link_context_menu_copy_url_action->setText("Copy &URL");
|
|
break;
|
|
}
|
|
|
|
m_link_context_menu->exec(view().map_point_to_global_position(content_position));
|
|
};
|
|
|
|
auto* open_image_action = new QAction("&Open Image", this);
|
|
open_image_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
|
|
QObject::connect(open_image_action, &QAction::triggered, this, [this]() {
|
|
open_link(m_image_context_menu_url);
|
|
});
|
|
|
|
auto* open_image_in_new_tab_action = new QAction("&Open Image in New &Tab", this);
|
|
open_image_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
|
|
QObject::connect(open_image_in_new_tab_action, &QAction::triggered, this, [this]() {
|
|
open_link_in_new_tab(m_image_context_menu_url);
|
|
});
|
|
|
|
auto* copy_image_action = new QAction("&Copy Image", this);
|
|
copy_image_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
|
|
QObject::connect(copy_image_action, &QAction::triggered, this, [this]() {
|
|
auto* bitmap = m_image_context_menu_bitmap.bitmap();
|
|
if (bitmap == nullptr)
|
|
return;
|
|
|
|
auto data = Gfx::BMPWriter::encode(*bitmap);
|
|
if (data.is_error())
|
|
return;
|
|
|
|
auto image = QImage::fromData(data.value().data(), data.value().size(), "BMP");
|
|
if (image.isNull())
|
|
return;
|
|
|
|
auto* clipboard = QGuiApplication::clipboard();
|
|
clipboard->setImage(image);
|
|
});
|
|
|
|
auto* copy_image_url_action = new QAction("Copy Image &URL", this);
|
|
copy_image_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
|
|
QObject::connect(copy_image_url_action, &QAction::triggered, this, [this]() {
|
|
copy_link_url(m_image_context_menu_url);
|
|
});
|
|
|
|
m_image_context_menu = new QMenu("Image context menu", this);
|
|
m_image_context_menu->addAction(open_image_action);
|
|
m_image_context_menu->addAction(open_image_in_new_tab_action);
|
|
m_image_context_menu->addSeparator();
|
|
m_image_context_menu->addAction(copy_image_action);
|
|
m_image_context_menu->addAction(copy_image_url_action);
|
|
m_image_context_menu->addSeparator();
|
|
m_image_context_menu->addAction(&m_window->inspect_dom_node_action());
|
|
|
|
view().on_image_context_menu_request = [this](auto& image_url, Gfx::IntPoint content_position, Gfx::ShareableBitmap const& shareable_bitmap) {
|
|
m_image_context_menu_url = image_url;
|
|
m_image_context_menu_bitmap = shareable_bitmap;
|
|
|
|
m_image_context_menu->exec(view().map_point_to_global_position(content_position));
|
|
};
|
|
|
|
m_media_context_menu_play_icon = load_icon_from_uri("resource://icons/16x16/play.png"sv);
|
|
m_media_context_menu_pause_icon = load_icon_from_uri("resource://icons/16x16/pause.png"sv);
|
|
m_media_context_menu_mute_icon = load_icon_from_uri("resource://icons/16x16/audio-volume-muted.png"sv);
|
|
m_media_context_menu_unmute_icon = load_icon_from_uri("resource://icons/16x16/audio-volume-high.png"sv);
|
|
|
|
m_media_context_menu_play_pause_action = new QAction("&Play", this);
|
|
m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_play_icon);
|
|
QObject::connect(m_media_context_menu_play_pause_action, &QAction::triggered, this, [this]() {
|
|
view().toggle_media_play_state();
|
|
});
|
|
|
|
m_media_context_menu_mute_unmute_action = new QAction("&Mute", this);
|
|
m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_mute_icon);
|
|
QObject::connect(m_media_context_menu_mute_unmute_action, &QAction::triggered, this, [this]() {
|
|
view().toggle_media_mute_state();
|
|
});
|
|
|
|
m_media_context_menu_controls_action = new QAction("Show &Controls", this);
|
|
m_media_context_menu_controls_action->setCheckable(true);
|
|
QObject::connect(m_media_context_menu_controls_action, &QAction::triggered, this, [this]() {
|
|
view().toggle_media_controls_state();
|
|
});
|
|
|
|
m_media_context_menu_loop_action = new QAction("&Loop", this);
|
|
m_media_context_menu_loop_action->setCheckable(true);
|
|
QObject::connect(m_media_context_menu_loop_action, &QAction::triggered, this, [this]() {
|
|
view().toggle_media_loop_state();
|
|
});
|
|
|
|
auto* open_audio_action = new QAction("&Open Audio", this);
|
|
open_audio_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-sound.png"sv));
|
|
QObject::connect(open_audio_action, &QAction::triggered, this, [this]() {
|
|
open_link(m_media_context_menu_url);
|
|
});
|
|
|
|
auto* open_audio_in_new_tab_action = new QAction("Open Audio in New &Tab", this);
|
|
open_audio_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
|
|
QObject::connect(open_audio_in_new_tab_action, &QAction::triggered, this, [this]() {
|
|
open_link_in_new_tab(m_media_context_menu_url);
|
|
});
|
|
|
|
auto* copy_audio_url_action = new QAction("Copy Audio &URL", this);
|
|
copy_audio_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
|
|
QObject::connect(copy_audio_url_action, &QAction::triggered, this, [this]() {
|
|
copy_link_url(m_media_context_menu_url);
|
|
});
|
|
|
|
m_audio_context_menu = new QMenu("Audio context menu", this);
|
|
m_audio_context_menu->addAction(m_media_context_menu_play_pause_action);
|
|
m_audio_context_menu->addAction(m_media_context_menu_mute_unmute_action);
|
|
m_audio_context_menu->addAction(m_media_context_menu_controls_action);
|
|
m_audio_context_menu->addAction(m_media_context_menu_loop_action);
|
|
m_audio_context_menu->addSeparator();
|
|
m_audio_context_menu->addAction(open_audio_action);
|
|
m_audio_context_menu->addAction(open_audio_in_new_tab_action);
|
|
m_audio_context_menu->addSeparator();
|
|
m_audio_context_menu->addAction(copy_audio_url_action);
|
|
m_audio_context_menu->addSeparator();
|
|
m_audio_context_menu->addAction(&m_window->inspect_dom_node_action());
|
|
|
|
auto* open_video_action = new QAction("&Open Video", this);
|
|
open_video_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-video.png"sv));
|
|
QObject::connect(open_video_action, &QAction::triggered, this, [this]() {
|
|
open_link(m_media_context_menu_url);
|
|
});
|
|
|
|
auto* open_video_in_new_tab_action = new QAction("Open Video in New &Tab", this);
|
|
open_video_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
|
|
QObject::connect(open_video_in_new_tab_action, &QAction::triggered, this, [this]() {
|
|
open_link_in_new_tab(m_media_context_menu_url);
|
|
});
|
|
|
|
auto* copy_video_url_action = new QAction("Copy Video &URL", this);
|
|
copy_video_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
|
|
QObject::connect(copy_video_url_action, &QAction::triggered, this, [this]() {
|
|
copy_link_url(m_media_context_menu_url);
|
|
});
|
|
|
|
m_video_context_menu = new QMenu("Video context menu", this);
|
|
m_video_context_menu->addAction(m_media_context_menu_play_pause_action);
|
|
m_video_context_menu->addAction(m_media_context_menu_mute_unmute_action);
|
|
m_video_context_menu->addAction(m_media_context_menu_controls_action);
|
|
m_video_context_menu->addAction(m_media_context_menu_loop_action);
|
|
m_video_context_menu->addSeparator();
|
|
m_video_context_menu->addAction(open_video_action);
|
|
m_video_context_menu->addAction(open_video_in_new_tab_action);
|
|
m_video_context_menu->addSeparator();
|
|
m_video_context_menu->addAction(copy_video_url_action);
|
|
m_video_context_menu->addSeparator();
|
|
m_video_context_menu->addAction(&m_window->inspect_dom_node_action());
|
|
|
|
view().on_media_context_menu_request = [this](Gfx::IntPoint content_position, Web::Page::MediaContextMenu const& menu) {
|
|
m_media_context_menu_url = menu.media_url;
|
|
|
|
if (menu.is_playing) {
|
|
m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_pause_icon);
|
|
m_media_context_menu_play_pause_action->setText("&Pause");
|
|
} else {
|
|
m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_play_icon);
|
|
m_media_context_menu_play_pause_action->setText("&Play");
|
|
}
|
|
|
|
if (menu.is_muted) {
|
|
m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_unmute_icon);
|
|
m_media_context_menu_mute_unmute_action->setText("Un&mute");
|
|
} else {
|
|
m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_mute_icon);
|
|
m_media_context_menu_mute_unmute_action->setText("&Mute");
|
|
}
|
|
|
|
m_media_context_menu_controls_action->setChecked(menu.has_user_agent_controls);
|
|
m_media_context_menu_loop_action->setChecked(menu.is_looping);
|
|
|
|
auto screen_position = view().map_point_to_global_position(content_position);
|
|
if (menu.is_video)
|
|
m_video_context_menu->exec(screen_position);
|
|
else
|
|
m_audio_context_menu->exec(screen_position);
|
|
};
|
|
}
|
|
|
|
Tab::~Tab()
|
|
{
|
|
close_sub_widgets();
|
|
|
|
// Delete the InspectorWidget explicitly to ensure it is deleted before the WebContentView. Otherwise, Qt
|
|
// can destroy these objects in any order, which may cause use-after-free in InspectorWidget's destructor.
|
|
delete m_inspector_widget;
|
|
}
|
|
|
|
void Tab::select_dropdown_action()
|
|
{
|
|
QAction* action = qobject_cast<QAction*>(sender());
|
|
view().select_dropdown_closed(action->data().value<uint>());
|
|
}
|
|
|
|
void Tab::update_reset_zoom_button()
|
|
{
|
|
auto zoom_level = view().zoom_level();
|
|
if (zoom_level != 1.0f) {
|
|
auto zoom_level_text = MUST(String::formatted("{}%", round_to<int>(zoom_level * 100)));
|
|
m_reset_zoom_button->setText(qstring_from_ak_string(zoom_level_text));
|
|
m_reset_zoom_button_action->setVisible(true);
|
|
} else {
|
|
m_reset_zoom_button_action->setVisible(false);
|
|
}
|
|
}
|
|
|
|
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::back()
|
|
{
|
|
view().traverse_the_history_by_delta(-1);
|
|
}
|
|
|
|
void Tab::forward()
|
|
{
|
|
view().traverse_the_history_by_delta(1);
|
|
}
|
|
|
|
void Tab::reload()
|
|
{
|
|
view().reload();
|
|
}
|
|
|
|
void Tab::open_link(URL::URL const& url)
|
|
{
|
|
view().on_link_click(url, "", 0);
|
|
}
|
|
|
|
void Tab::open_link_in_new_tab(URL::URL const& url)
|
|
{
|
|
view().on_link_click(url, "_blank", 0);
|
|
}
|
|
|
|
void Tab::copy_link_url(URL::URL const& url)
|
|
{
|
|
auto* clipboard = QGuiApplication::clipboard();
|
|
clipboard->setText(qstring_from_ak_string(WebView::url_text_to_copy(url)));
|
|
}
|
|
|
|
void Tab::location_edit_return_pressed()
|
|
{
|
|
navigate(ak_url_from_qstring(m_location_edit->text()));
|
|
}
|
|
|
|
void Tab::open_file()
|
|
{
|
|
auto filename = QFileDialog::getOpenFileUrl(this, "Open file", QDir::homePath(), "All Files (*.*)");
|
|
if (filename.isValid()) {
|
|
navigate(ak_url_from_qurl(filename));
|
|
}
|
|
}
|
|
|
|
int Tab::tab_index()
|
|
{
|
|
return m_window->tab_index(this);
|
|
}
|
|
|
|
void Tab::debug_request(ByteString const& request, ByteString const& argument)
|
|
{
|
|
m_view->debug_request(request, argument);
|
|
}
|
|
|
|
void Tab::resizeEvent(QResizeEvent* event)
|
|
{
|
|
QWidget::resizeEvent(event);
|
|
if (m_hover_label->isVisible())
|
|
update_hover_label();
|
|
}
|
|
|
|
void Tab::update_hover_label()
|
|
{
|
|
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() - 8;
|
|
if (m_find_in_page->isVisible())
|
|
hover_label_height -= m_find_in_page->height() - 4;
|
|
|
|
m_hover_label->move(6, hover_label_height);
|
|
m_hover_label->raise();
|
|
}
|
|
|
|
void Tab::update_navigation_buttons_state()
|
|
{
|
|
m_window->go_back_action().setEnabled(m_can_navigate_back);
|
|
m_window->go_forward_action().setEnabled(m_can_navigate_forward);
|
|
}
|
|
|
|
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_window->go_back_action().setIcon(create_tvg_icon_with_theme_colors("back", palette()));
|
|
m_window->go_forward_action().setIcon(create_tvg_icon_with_theme_colors("forward", palette()));
|
|
m_window->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()));
|
|
}
|
|
|
|
void Tab::show_inspector_window(InspectorTarget inspector_target)
|
|
{
|
|
if (!m_inspector_widget)
|
|
m_inspector_widget = new InspectorWidget(this, view());
|
|
else
|
|
m_inspector_widget->inspect();
|
|
|
|
m_inspector_widget->show();
|
|
m_inspector_widget->activateWindow();
|
|
m_inspector_widget->raise();
|
|
|
|
if (inspector_target == InspectorTarget::HoveredElement)
|
|
m_inspector_widget->select_hovered_node();
|
|
else
|
|
m_inspector_widget->select_default_node();
|
|
}
|
|
|
|
void Tab::show_find_in_page()
|
|
{
|
|
m_find_in_page->setVisible(true);
|
|
m_find_in_page->setFocus();
|
|
}
|
|
|
|
void Tab::close_sub_widgets()
|
|
{
|
|
auto close_widget_window = [](auto* widget) {
|
|
if (widget)
|
|
widget->close();
|
|
};
|
|
|
|
close_widget_window(m_inspector_widget);
|
|
}
|
|
|
|
void Tab::set_line_box_borders(bool enabled)
|
|
{
|
|
debug_request("set-line-box-borders", enabled ? "on" : "off");
|
|
}
|
|
|
|
}
|