Tab.cpp 30 KB

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