Tab.cpp 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  1. /*
  2. * Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2022, Matthew Costa <ucosty@gmail.com>
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include "BrowserWindow.h"
  8. #include "Icon.h"
  9. #include "InspectorWidget.h"
  10. #include "Settings.h"
  11. #include "StringUtils.h"
  12. #include "TVGIconEngine.h"
  13. #include <AK/TemporaryChange.h>
  14. #include <LibGfx/ImageFormats/BMPWriter.h>
  15. #include <LibGfx/Painter.h>
  16. #include <LibWebView/SearchEngine.h>
  17. #include <LibWebView/SourceHighlighter.h>
  18. #include <LibWebView/URL.h>
  19. #include <QClipboard>
  20. #include <QColorDialog>
  21. #include <QCoreApplication>
  22. #include <QCursor>
  23. #include <QDesktopServices>
  24. #include <QFileDialog>
  25. #include <QFont>
  26. #include <QFontMetrics>
  27. #include <QGuiApplication>
  28. #include <QImage>
  29. #include <QInputDialog>
  30. #include <QMenu>
  31. #include <QMessageBox>
  32. #include <QMimeData>
  33. #include <QPainter>
  34. #include <QPoint>
  35. #include <QPushButton>
  36. #include <QResizeEvent>
  37. namespace Ladybird {
  38. static QIcon create_tvg_icon_with_theme_colors(QString name, QPalette const& palette)
  39. {
  40. auto path = QString(":/Icons/%1.tvg").arg(name);
  41. auto icon_engine = TVGIconEngine::from_file(path);
  42. VERIFY(icon_engine);
  43. auto icon_filter = [](QColor color) {
  44. return [color = Color::from_argb(color.rgba64().toArgb32())](Gfx::Color icon_color) {
  45. return color.with_alpha((icon_color.alpha() * color.alpha()) / 255);
  46. };
  47. };
  48. icon_engine->add_filter(QIcon::Mode::Normal, icon_filter(palette.color(QPalette::ColorGroup::Normal, QPalette::ColorRole::ButtonText)));
  49. icon_engine->add_filter(QIcon::Mode::Disabled, icon_filter(palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::ButtonText)));
  50. return QIcon(icon_engine);
  51. }
  52. Tab::Tab(BrowserWindow* window, WebContentOptions const& web_content_options, StringView webdriver_content_ipc_path, RefPtr<WebView::WebContentClient> parent_client, size_t page_index)
  53. : QWidget(window)
  54. , m_window(window)
  55. {
  56. m_layout = new QBoxLayout(QBoxLayout::Direction::TopToBottom, this);
  57. m_layout->setSpacing(0);
  58. m_layout->setContentsMargins(0, 0, 0, 0);
  59. m_view = new WebContentView(web_content_options, webdriver_content_ipc_path, parent_client, page_index);
  60. m_toolbar = new QToolBar(this);
  61. m_location_edit = new LocationEdit(this);
  62. m_hover_label = new QLabel(this);
  63. m_hover_label->hide();
  64. m_hover_label->setFrameShape(QFrame::Shape::Box);
  65. m_hover_label->setAutoFillBackground(true);
  66. auto* focus_location_editor_action = new QAction("Edit Location", this);
  67. focus_location_editor_action->setShortcut(QKeySequence("Ctrl+L"));
  68. addAction(focus_location_editor_action);
  69. m_layout->addWidget(m_toolbar);
  70. m_layout->addWidget(m_view);
  71. recreate_toolbar_icons();
  72. m_toolbar->addAction(&m_window->go_back_action());
  73. m_toolbar->addAction(&m_window->go_forward_action());
  74. m_toolbar->addAction(&m_window->reload_action());
  75. m_toolbar->addWidget(m_location_edit);
  76. m_toolbar->setIconSize({ 16, 16 });
  77. // This is a little awkward, but without this Qt shrinks the button to the size of the icon.
  78. // Note: toolButtonStyle="0" -> ToolButtonIconOnly.
  79. m_toolbar->setStyleSheet("QToolButton[toolButtonStyle=\"0\"]{width:24px;height:24px}");
  80. m_reset_zoom_button = new QToolButton(m_toolbar);
  81. m_reset_zoom_button->setToolButtonStyle(Qt::ToolButtonTextOnly);
  82. m_reset_zoom_button->setToolTip("Reset zoom level");
  83. m_reset_zoom_button_action = m_toolbar->addWidget(m_reset_zoom_button);
  84. m_reset_zoom_button_action->setVisible(false);
  85. QObject::connect(m_reset_zoom_button, &QAbstractButton::clicked, [this] {
  86. view().reset_zoom();
  87. update_reset_zoom_button();
  88. m_window->update_zoom_menu();
  89. });
  90. view().on_activate_tab = [this] {
  91. m_window->activate_tab(tab_index());
  92. };
  93. view().on_close = [this] {
  94. m_window->close_tab(tab_index());
  95. };
  96. view().on_link_hover = [this](auto const& url) {
  97. m_hover_label->setText(qstring_from_ak_string(url.to_byte_string()));
  98. update_hover_label();
  99. m_hover_label->show();
  100. };
  101. view().on_link_unhover = [this]() {
  102. m_hover_label->hide();
  103. };
  104. view().on_load_start = [this](const URL& url, bool is_redirect) {
  105. // If we are loading due to a redirect, we replace the current history entry
  106. // with the loaded URL
  107. if (is_redirect) {
  108. m_history.replace_current(url, m_title.toUtf8().data());
  109. }
  110. m_location_edit->setText(url.to_byte_string().characters());
  111. m_location_edit->setCursorPosition(0);
  112. // Don't add to history if back or forward is pressed
  113. if (!m_is_history_navigation) {
  114. m_history.push(url, m_title.toUtf8().data());
  115. }
  116. m_is_history_navigation = false;
  117. m_window->go_back_action().setEnabled(m_history.can_go_back());
  118. m_window->go_forward_action().setEnabled(m_history.can_go_forward());
  119. m_window->reload_action().setEnabled(!m_history.is_empty());
  120. if (m_inspector_widget)
  121. m_inspector_widget->reset();
  122. };
  123. view().on_load_finish = [this](auto&) {
  124. if (m_inspector_widget != nullptr && m_inspector_widget->isVisible())
  125. m_inspector_widget->inspect();
  126. };
  127. QObject::connect(m_location_edit, &QLineEdit::returnPressed, this, &Tab::location_edit_return_pressed);
  128. view().on_title_change = [this](auto const& title) {
  129. m_title = qstring_from_ak_string(title);
  130. m_history.update_title(title);
  131. emit title_changed(tab_index(), m_title);
  132. };
  133. view().on_favicon_change = [this](auto const& bitmap) {
  134. auto qimage = QImage(bitmap.scanline_u8(0), bitmap.width(), bitmap.height(), QImage::Format_ARGB32);
  135. if (qimage.isNull())
  136. return;
  137. auto qpixmap = QPixmap::fromImage(qimage);
  138. if (qpixmap.isNull())
  139. return;
  140. emit favicon_changed(tab_index(), QIcon(qpixmap));
  141. };
  142. view().on_request_alert = [this](auto const& message) {
  143. m_dialog = new QMessageBox(QMessageBox::Icon::Warning, "Ladybird", qstring_from_ak_string(message), QMessageBox::StandardButton::Ok, &view());
  144. m_dialog->exec();
  145. view().alert_closed();
  146. m_dialog = nullptr;
  147. };
  148. view().on_request_confirm = [this](auto const& message) {
  149. m_dialog = new QMessageBox(QMessageBox::Icon::Question, "Ladybird", qstring_from_ak_string(message), QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel, &view());
  150. auto result = m_dialog->exec();
  151. view().confirm_closed(result == QMessageBox::StandardButton::Ok || result == QDialog::Accepted);
  152. m_dialog = nullptr;
  153. };
  154. view().on_request_prompt = [this](auto const& message, auto const& default_) {
  155. m_dialog = new QInputDialog(&view());
  156. auto& dialog = static_cast<QInputDialog&>(*m_dialog);
  157. dialog.setWindowTitle("Ladybird");
  158. dialog.setLabelText(qstring_from_ak_string(message));
  159. dialog.setTextValue(qstring_from_ak_string(default_));
  160. if (dialog.exec() == QDialog::Accepted)
  161. view().prompt_closed(ak_string_from_qstring(dialog.textValue()));
  162. else
  163. view().prompt_closed({});
  164. m_dialog = nullptr;
  165. };
  166. view().on_request_set_prompt_text = [this](auto const& message) {
  167. if (m_dialog && is<QInputDialog>(*m_dialog))
  168. static_cast<QInputDialog&>(*m_dialog).setTextValue(qstring_from_ak_string(message));
  169. };
  170. view().on_request_accept_dialog = [this]() {
  171. if (m_dialog)
  172. m_dialog->accept();
  173. };
  174. view().on_request_dismiss_dialog = [this]() {
  175. if (m_dialog)
  176. m_dialog->reject();
  177. };
  178. view().on_request_color_picker = [this](Color current_color) {
  179. m_dialog = new QColorDialog(QColor(current_color.red(), current_color.green(), current_color.blue()), &view());
  180. auto& dialog = static_cast<QColorDialog&>(*m_dialog);
  181. dialog.setWindowTitle("Ladybird");
  182. dialog.setOption(QColorDialog::ShowAlphaChannel, false);
  183. QObject::connect(&dialog, &QColorDialog::currentColorChanged, this, [this](const QColor& color) {
  184. view().color_picker_update(Color(color.red(), color.green(), color.blue()), Web::HTML::ColorPickerUpdateState::Update);
  185. });
  186. if (dialog.exec() == QDialog::Accepted)
  187. view().color_picker_update(Color(dialog.selectedColor().red(), dialog.selectedColor().green(), dialog.selectedColor().blue()), Web::HTML::ColorPickerUpdateState::Closed);
  188. else
  189. view().color_picker_update({}, Web::HTML::ColorPickerUpdateState::Closed);
  190. m_dialog = nullptr;
  191. };
  192. m_select_dropdown = new QMenu("Select Dropdown", this);
  193. QObject::connect(m_select_dropdown, &QMenu::aboutToHide, this, [this]() {
  194. if (!m_select_dropdown->activeAction())
  195. view().select_dropdown_closed({});
  196. });
  197. view().on_request_select_dropdown = [this](Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items) {
  198. m_select_dropdown->clear();
  199. m_select_dropdown->setMinimumWidth(minimum_width / view().device_pixel_ratio());
  200. for (auto const& item : items) {
  201. select_dropdown_add_item(m_select_dropdown, item);
  202. }
  203. m_select_dropdown->exec(view().mapToGlobal(QPoint(content_position.x(), content_position.y()) / view().device_pixel_ratio()));
  204. };
  205. QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor);
  206. view().on_received_source = [this](auto const& url, auto const& source) {
  207. auto html = WebView::highlight_source(url, source);
  208. m_window->new_tab_from_content(html, Web::HTML::ActivateTab::Yes);
  209. };
  210. view().on_navigate_back = [this]() {
  211. back();
  212. };
  213. view().on_navigate_forward = [this]() {
  214. forward();
  215. };
  216. view().on_refresh = [this]() {
  217. reload();
  218. };
  219. view().on_restore_window = [this]() {
  220. m_window->showNormal();
  221. };
  222. view().on_reposition_window = [this](auto const& position) {
  223. m_window->move(position.x(), position.y());
  224. return Gfx::IntPoint { m_window->x(), m_window->y() };
  225. };
  226. view().on_resize_window = [this](auto const& size) {
  227. m_window->resize(size.width(), size.height());
  228. return Gfx::IntSize { m_window->width(), m_window->height() };
  229. };
  230. view().on_maximize_window = [this]() {
  231. m_window->showMaximized();
  232. return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
  233. };
  234. view().on_minimize_window = [this]() {
  235. m_window->showMinimized();
  236. return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
  237. };
  238. view().on_fullscreen_window = [this]() {
  239. m_window->showFullScreen();
  240. return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
  241. };
  242. view().on_insert_clipboard_entry = [](auto const& data, auto const&, auto const& mime_type) {
  243. QByteArray qdata { data.bytes_as_string_view().characters_without_null_termination(), static_cast<qsizetype>(data.bytes_as_string_view().length()) };
  244. auto* mime_data = new QMimeData();
  245. mime_data->setData(qstring_from_ak_string(mime_type), qdata);
  246. auto* clipboard = QGuiApplication::clipboard();
  247. clipboard->setMimeData(mime_data);
  248. };
  249. auto* search_selected_text_action = new QAction("&Search for <query>", this);
  250. search_selected_text_action->setIcon(load_icon_from_uri("resource://icons/16x16/find.png"sv));
  251. QObject::connect(search_selected_text_action, &QAction::triggered, this, [this]() {
  252. auto url = MUST(String::formatted(Settings::the()->search_engine().query_url, URL::percent_encode(*m_page_context_menu_search_text)));
  253. m_window->new_tab_from_url(AK::URL(url), Web::HTML::ActivateTab::Yes);
  254. });
  255. auto take_screenshot = [this](auto type) {
  256. auto& view = this->view();
  257. view.take_screenshot(type)
  258. ->when_resolved([this](auto const& path) {
  259. auto message = MUST(String::formatted("Screenshot saved to: {}", path));
  260. QMessageBox dialog(this);
  261. dialog.setWindowTitle("Ladybird");
  262. dialog.setIcon(QMessageBox::Information);
  263. dialog.setText(qstring_from_ak_string(message));
  264. dialog.addButton(QMessageBox::Ok);
  265. dialog.addButton(QMessageBox::Open)->setText("Open folder");
  266. if (dialog.exec() == QMessageBox::Open) {
  267. auto path_url = QUrl::fromLocalFile(qstring_from_ak_string(path.dirname()));
  268. QDesktopServices::openUrl(path_url);
  269. }
  270. })
  271. .when_rejected([this](auto const& error) {
  272. auto error_message = MUST(String::formatted("{}", error));
  273. QMessageBox::warning(this, "Ladybird", qstring_from_ak_string(error_message));
  274. });
  275. };
  276. auto* take_visible_screenshot_action = new QAction("Take &Visible Screenshot", this);
  277. take_visible_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
  278. QObject::connect(take_visible_screenshot_action, &QAction::triggered, this, [take_screenshot]() {
  279. take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible);
  280. });
  281. auto* take_full_screenshot_action = new QAction("Take &Full Screenshot", this);
  282. take_full_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
  283. QObject::connect(take_full_screenshot_action, &QAction::triggered, this, [take_screenshot]() {
  284. take_screenshot(WebView::ViewImplementation::ScreenshotType::Full);
  285. });
  286. m_page_context_menu = new QMenu("Context menu", this);
  287. m_page_context_menu->addAction(&m_window->go_back_action());
  288. m_page_context_menu->addAction(&m_window->go_forward_action());
  289. m_page_context_menu->addAction(&m_window->reload_action());
  290. m_page_context_menu->addSeparator();
  291. m_page_context_menu->addAction(&m_window->copy_selection_action());
  292. m_page_context_menu->addAction(&m_window->select_all_action());
  293. m_page_context_menu->addSeparator();
  294. m_page_context_menu->addAction(search_selected_text_action);
  295. m_page_context_menu->addSeparator();
  296. m_page_context_menu->addAction(take_visible_screenshot_action);
  297. m_page_context_menu->addAction(take_full_screenshot_action);
  298. m_page_context_menu->addSeparator();
  299. m_page_context_menu->addAction(&m_window->view_source_action());
  300. m_page_context_menu->addAction(&m_window->inspect_dom_node_action());
  301. view().on_context_menu_request = [this, search_selected_text_action](Gfx::IntPoint content_position) {
  302. auto selected_text = Settings::the()->enable_search()
  303. ? view().selected_text_with_whitespace_collapsed()
  304. : OptionalNone {};
  305. TemporaryChange change_url { m_page_context_menu_search_text, std::move(selected_text) };
  306. if (m_page_context_menu_search_text.has_value()) {
  307. auto action_text = WebView::format_search_query_for_display(Settings::the()->search_engine().query_url, *m_page_context_menu_search_text);
  308. search_selected_text_action->setText(qstring_from_ak_string(action_text));
  309. search_selected_text_action->setVisible(true);
  310. } else {
  311. search_selected_text_action->setVisible(false);
  312. }
  313. m_page_context_menu->exec(view().mapToGlobal(QPoint(content_position.x(), content_position.y()) / view().device_pixel_ratio()));
  314. };
  315. auto* open_link_action = new QAction("&Open", this);
  316. open_link_action->setIcon(load_icon_from_uri("resource://icons/16x16/go-forward.png"sv));
  317. QObject::connect(open_link_action, &QAction::triggered, this, [this]() {
  318. open_link(m_link_context_menu_url);
  319. });
  320. auto* open_link_in_new_tab_action = new QAction("&Open in New &Tab", this);
  321. open_link_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  322. QObject::connect(open_link_in_new_tab_action, &QAction::triggered, this, [this]() {
  323. open_link_in_new_tab(m_link_context_menu_url);
  324. });
  325. m_link_context_menu_copy_url_action = new QAction("Copy &URL", this);
  326. m_link_context_menu_copy_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  327. QObject::connect(m_link_context_menu_copy_url_action, &QAction::triggered, this, [this]() {
  328. copy_link_url(m_link_context_menu_url);
  329. });
  330. m_link_context_menu = new QMenu("Link context menu", this);
  331. m_link_context_menu->addAction(open_link_action);
  332. m_link_context_menu->addAction(open_link_in_new_tab_action);
  333. m_link_context_menu->addSeparator();
  334. m_link_context_menu->addAction(m_link_context_menu_copy_url_action);
  335. m_link_context_menu->addSeparator();
  336. m_link_context_menu->addAction(&m_window->inspect_dom_node_action());
  337. view().on_link_context_menu_request = [this](auto const& url, Gfx::IntPoint content_position) {
  338. m_link_context_menu_url = url;
  339. switch (WebView::url_type(url)) {
  340. case WebView::URLType::Email:
  341. m_link_context_menu_copy_url_action->setText("Copy &Email Address");
  342. break;
  343. case WebView::URLType::Telephone:
  344. m_link_context_menu_copy_url_action->setText("Copy &Phone Number");
  345. break;
  346. case WebView::URLType::Other:
  347. m_link_context_menu_copy_url_action->setText("Copy &URL");
  348. break;
  349. }
  350. m_link_context_menu->exec(view().mapToGlobal(QPoint(content_position.x(), content_position.y()) / view().device_pixel_ratio()));
  351. };
  352. auto* open_image_action = new QAction("&Open Image", this);
  353. open_image_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
  354. QObject::connect(open_image_action, &QAction::triggered, this, [this]() {
  355. open_link(m_image_context_menu_url);
  356. });
  357. auto* open_image_in_new_tab_action = new QAction("&Open Image in New &Tab", this);
  358. open_image_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  359. QObject::connect(open_image_in_new_tab_action, &QAction::triggered, this, [this]() {
  360. open_link_in_new_tab(m_image_context_menu_url);
  361. });
  362. auto* copy_image_action = new QAction("&Copy Image", this);
  363. copy_image_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  364. QObject::connect(copy_image_action, &QAction::triggered, this, [this]() {
  365. auto* bitmap = m_image_context_menu_bitmap.bitmap();
  366. if (bitmap == nullptr)
  367. return;
  368. auto data = Gfx::BMPWriter::encode(*bitmap);
  369. if (data.is_error())
  370. return;
  371. auto image = QImage::fromData(data.value().data(), data.value().size(), "BMP");
  372. if (image.isNull())
  373. return;
  374. auto* clipboard = QGuiApplication::clipboard();
  375. clipboard->setImage(image);
  376. });
  377. auto* copy_image_url_action = new QAction("Copy Image &URL", this);
  378. copy_image_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  379. QObject::connect(copy_image_url_action, &QAction::triggered, this, [this]() {
  380. copy_link_url(m_image_context_menu_url);
  381. });
  382. m_image_context_menu = new QMenu("Image context menu", this);
  383. m_image_context_menu->addAction(open_image_action);
  384. m_image_context_menu->addAction(open_image_in_new_tab_action);
  385. m_image_context_menu->addSeparator();
  386. m_image_context_menu->addAction(copy_image_action);
  387. m_image_context_menu->addAction(copy_image_url_action);
  388. m_image_context_menu->addSeparator();
  389. m_image_context_menu->addAction(&m_window->inspect_dom_node_action());
  390. view().on_image_context_menu_request = [this](auto& image_url, Gfx::IntPoint content_position, Gfx::ShareableBitmap const& shareable_bitmap) {
  391. m_image_context_menu_url = image_url;
  392. m_image_context_menu_bitmap = shareable_bitmap;
  393. m_image_context_menu->exec(view().mapToGlobal(QPoint(content_position.x(), content_position.y()) / view().device_pixel_ratio()));
  394. };
  395. m_media_context_menu_play_icon = load_icon_from_uri("resource://icons/16x16/play.png"sv);
  396. m_media_context_menu_pause_icon = load_icon_from_uri("resource://icons/16x16/pause.png"sv);
  397. m_media_context_menu_mute_icon = load_icon_from_uri("resource://icons/16x16/audio-volume-muted.png"sv);
  398. m_media_context_menu_unmute_icon = load_icon_from_uri("resource://icons/16x16/audio-volume-high.png"sv);
  399. m_media_context_menu_play_pause_action = new QAction("&Play", this);
  400. m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_play_icon);
  401. QObject::connect(m_media_context_menu_play_pause_action, &QAction::triggered, this, [this]() {
  402. view().toggle_media_play_state();
  403. });
  404. m_media_context_menu_mute_unmute_action = new QAction("&Mute", this);
  405. m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_mute_icon);
  406. QObject::connect(m_media_context_menu_mute_unmute_action, &QAction::triggered, this, [this]() {
  407. view().toggle_media_mute_state();
  408. });
  409. m_media_context_menu_controls_action = new QAction("Show &Controls", this);
  410. m_media_context_menu_controls_action->setCheckable(true);
  411. QObject::connect(m_media_context_menu_controls_action, &QAction::triggered, this, [this]() {
  412. view().toggle_media_controls_state();
  413. });
  414. m_media_context_menu_loop_action = new QAction("&Loop", this);
  415. m_media_context_menu_loop_action->setCheckable(true);
  416. QObject::connect(m_media_context_menu_loop_action, &QAction::triggered, this, [this]() {
  417. view().toggle_media_loop_state();
  418. });
  419. auto* open_audio_action = new QAction("&Open Audio", this);
  420. open_audio_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-sound.png"sv));
  421. QObject::connect(open_audio_action, &QAction::triggered, this, [this]() {
  422. open_link(m_media_context_menu_url);
  423. });
  424. auto* open_audio_in_new_tab_action = new QAction("Open Audio in New &Tab", this);
  425. open_audio_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  426. QObject::connect(open_audio_in_new_tab_action, &QAction::triggered, this, [this]() {
  427. open_link_in_new_tab(m_media_context_menu_url);
  428. });
  429. auto* copy_audio_url_action = new QAction("Copy Audio &URL", this);
  430. copy_audio_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  431. QObject::connect(copy_audio_url_action, &QAction::triggered, this, [this]() {
  432. copy_link_url(m_media_context_menu_url);
  433. });
  434. m_audio_context_menu = new QMenu("Audio context menu", this);
  435. m_audio_context_menu->addAction(m_media_context_menu_play_pause_action);
  436. m_audio_context_menu->addAction(m_media_context_menu_mute_unmute_action);
  437. m_audio_context_menu->addAction(m_media_context_menu_controls_action);
  438. m_audio_context_menu->addAction(m_media_context_menu_loop_action);
  439. m_audio_context_menu->addSeparator();
  440. m_audio_context_menu->addAction(open_audio_action);
  441. m_audio_context_menu->addAction(open_audio_in_new_tab_action);
  442. m_audio_context_menu->addSeparator();
  443. m_audio_context_menu->addAction(copy_audio_url_action);
  444. m_audio_context_menu->addSeparator();
  445. m_audio_context_menu->addAction(&m_window->inspect_dom_node_action());
  446. auto* open_video_action = new QAction("&Open Video", this);
  447. open_video_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-video.png"sv));
  448. QObject::connect(open_video_action, &QAction::triggered, this, [this]() {
  449. open_link(m_media_context_menu_url);
  450. });
  451. auto* open_video_in_new_tab_action = new QAction("Open Video in New &Tab", this);
  452. open_video_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  453. QObject::connect(open_video_in_new_tab_action, &QAction::triggered, this, [this]() {
  454. open_link_in_new_tab(m_media_context_menu_url);
  455. });
  456. auto* copy_video_url_action = new QAction("Copy Video &URL", this);
  457. copy_video_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  458. QObject::connect(copy_video_url_action, &QAction::triggered, this, [this]() {
  459. copy_link_url(m_media_context_menu_url);
  460. });
  461. m_video_context_menu = new QMenu("Video context menu", this);
  462. m_video_context_menu->addAction(m_media_context_menu_play_pause_action);
  463. m_video_context_menu->addAction(m_media_context_menu_mute_unmute_action);
  464. m_video_context_menu->addAction(m_media_context_menu_controls_action);
  465. m_video_context_menu->addAction(m_media_context_menu_loop_action);
  466. m_video_context_menu->addSeparator();
  467. m_video_context_menu->addAction(open_video_action);
  468. m_video_context_menu->addAction(open_video_in_new_tab_action);
  469. m_video_context_menu->addSeparator();
  470. m_video_context_menu->addAction(copy_video_url_action);
  471. m_video_context_menu->addSeparator();
  472. m_video_context_menu->addAction(&m_window->inspect_dom_node_action());
  473. view().on_media_context_menu_request = [this](Gfx::IntPoint content_position, Web::Page::MediaContextMenu const& menu) {
  474. m_media_context_menu_url = menu.media_url;
  475. if (menu.is_playing) {
  476. m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_pause_icon);
  477. m_media_context_menu_play_pause_action->setText("&Pause");
  478. } else {
  479. m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_play_icon);
  480. m_media_context_menu_play_pause_action->setText("&Play");
  481. }
  482. if (menu.is_muted) {
  483. m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_unmute_icon);
  484. m_media_context_menu_mute_unmute_action->setText("Un&mute");
  485. } else {
  486. m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_mute_icon);
  487. m_media_context_menu_mute_unmute_action->setText("&Mute");
  488. }
  489. m_media_context_menu_controls_action->setChecked(menu.has_user_agent_controls);
  490. m_media_context_menu_loop_action->setChecked(menu.is_looping);
  491. auto screen_position = view().mapToGlobal(QPoint(content_position.x(), content_position.y()) / view().device_pixel_ratio());
  492. if (menu.is_video)
  493. m_video_context_menu->exec(screen_position);
  494. else
  495. m_audio_context_menu->exec(screen_position);
  496. };
  497. }
  498. Tab::~Tab()
  499. {
  500. close_sub_widgets();
  501. // Delete the InspectorWidget explicitly to ensure it is deleted before the WebContentView. Otherwise, Qt
  502. // can destroy these objects in any order, which may cause use-after-free in InspectorWidget's destructor.
  503. delete m_inspector_widget;
  504. }
  505. void Tab::select_dropdown_add_item(QMenu* menu, Web::HTML::SelectItem const& item)
  506. {
  507. if (item.type == Web::HTML::SelectItem::Type::OptionGroup) {
  508. QAction* subtitle = new QAction(qstring_from_ak_string(item.label.value_or(""_string)), this);
  509. subtitle->setDisabled(true);
  510. menu->addAction(subtitle);
  511. for (auto const& item : *item.items) {
  512. select_dropdown_add_item(menu, item);
  513. }
  514. }
  515. if (item.type == Web::HTML::SelectItem::Type::Option) {
  516. QAction* action = new QAction(qstring_from_ak_string(item.label.value_or(""_string)), this);
  517. action->setCheckable(true);
  518. action->setChecked(item.selected);
  519. action->setData(QVariant(qstring_from_ak_string(item.value.value_or(""_string))));
  520. QObject::connect(action, &QAction::triggered, this, &Tab::select_dropdown_action);
  521. menu->addAction(action);
  522. }
  523. if (item.type == Web::HTML::SelectItem::Type::Separator) {
  524. menu->addSeparator();
  525. }
  526. }
  527. void Tab::select_dropdown_action()
  528. {
  529. QAction* action = qobject_cast<QAction*>(sender());
  530. auto value = action->data().value<QString>();
  531. view().select_dropdown_closed(ak_string_from_qstring(value));
  532. }
  533. void Tab::update_reset_zoom_button()
  534. {
  535. auto zoom_level = view().zoom_level();
  536. if (zoom_level != 1.0f) {
  537. auto zoom_level_text = MUST(String::formatted("{}%", round_to<int>(zoom_level * 100)));
  538. m_reset_zoom_button->setText(qstring_from_ak_string(zoom_level_text));
  539. m_reset_zoom_button_action->setVisible(true);
  540. } else {
  541. m_reset_zoom_button_action->setVisible(false);
  542. }
  543. }
  544. void Tab::focus_location_editor()
  545. {
  546. m_location_edit->setFocus();
  547. m_location_edit->selectAll();
  548. }
  549. void Tab::navigate(AK::URL const& url)
  550. {
  551. view().load(url);
  552. }
  553. void Tab::load_html(StringView html)
  554. {
  555. view().load_html(html);
  556. }
  557. void Tab::back()
  558. {
  559. if (!m_history.can_go_back())
  560. return;
  561. m_is_history_navigation = true;
  562. m_history.go_back();
  563. view().load(m_history.current().url.to_byte_string());
  564. }
  565. void Tab::forward()
  566. {
  567. if (!m_history.can_go_forward())
  568. return;
  569. m_is_history_navigation = true;
  570. m_history.go_forward();
  571. view().load(m_history.current().url.to_byte_string());
  572. }
  573. void Tab::reload()
  574. {
  575. if (m_history.is_empty())
  576. return;
  577. m_is_history_navigation = true;
  578. view().load(m_history.current().url.to_byte_string());
  579. }
  580. void Tab::open_link(URL const& url)
  581. {
  582. view().on_link_click(url, "", 0);
  583. }
  584. void Tab::open_link_in_new_tab(URL const& url)
  585. {
  586. view().on_link_click(url, "_blank", 0);
  587. }
  588. void Tab::copy_link_url(URL const& url)
  589. {
  590. auto* clipboard = QGuiApplication::clipboard();
  591. clipboard->setText(qstring_from_ak_string(WebView::url_text_to_copy(url)));
  592. }
  593. void Tab::location_edit_return_pressed()
  594. {
  595. navigate(ak_url_from_qurl(m_location_edit->text()));
  596. }
  597. void Tab::open_file()
  598. {
  599. auto filename = QFileDialog::getOpenFileName(this, "Open file", QDir::homePath(), "All Files (*.*)");
  600. if (!filename.isNull())
  601. navigate(ak_url_from_qstring(filename));
  602. }
  603. int Tab::tab_index()
  604. {
  605. return m_window->tab_index(this);
  606. }
  607. void Tab::debug_request(ByteString const& request, ByteString const& argument)
  608. {
  609. if (request == "dump-history")
  610. m_history.dump();
  611. else
  612. m_view->debug_request(request, argument);
  613. }
  614. void Tab::resizeEvent(QResizeEvent* event)
  615. {
  616. QWidget::resizeEvent(event);
  617. if (m_hover_label->isVisible())
  618. update_hover_label();
  619. }
  620. void Tab::update_hover_label()
  621. {
  622. m_hover_label->resize(QFontMetrics(m_hover_label->font()).boundingRect(m_hover_label->text()).adjusted(-4, -2, 4, 2).size());
  623. m_hover_label->move(6, height() - m_hover_label->height() - 8);
  624. m_hover_label->raise();
  625. }
  626. bool Tab::event(QEvent* event)
  627. {
  628. if (event->type() == QEvent::PaletteChange) {
  629. recreate_toolbar_icons();
  630. return QWidget::event(event);
  631. }
  632. return QWidget::event(event);
  633. }
  634. void Tab::recreate_toolbar_icons()
  635. {
  636. m_window->go_back_action().setIcon(create_tvg_icon_with_theme_colors("back", palette()));
  637. m_window->go_forward_action().setIcon(create_tvg_icon_with_theme_colors("forward", palette()));
  638. m_window->reload_action().setIcon(create_tvg_icon_with_theme_colors("reload", palette()));
  639. }
  640. void Tab::show_inspector_window(InspectorTarget inspector_target)
  641. {
  642. if (!m_inspector_widget)
  643. m_inspector_widget = new InspectorWidget(this, view());
  644. else
  645. m_inspector_widget->inspect();
  646. m_inspector_widget->show();
  647. m_inspector_widget->activateWindow();
  648. m_inspector_widget->raise();
  649. if (inspector_target == InspectorTarget::HoveredElement)
  650. m_inspector_widget->select_hovered_node();
  651. else
  652. m_inspector_widget->select_default_node();
  653. }
  654. void Tab::close_sub_widgets()
  655. {
  656. auto close_widget_window = [](auto* widget) {
  657. if (widget)
  658. widget->close();
  659. };
  660. close_widget_window(m_inspector_widget);
  661. }
  662. }