Tab.cpp 29 KB

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