Tab.cpp 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  1. /*
  2. * Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2022, Matthew Costa <ucosty@gmail.com>
  4. * Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
  5. *
  6. * SPDX-License-Identifier: BSD-2-Clause
  7. */
  8. #include "BrowserWindow.h"
  9. #include "Icon.h"
  10. #include "InspectorWidget.h"
  11. #include "Settings.h"
  12. #include "StringUtils.h"
  13. #include <AK/TemporaryChange.h>
  14. #include <LibGfx/ImageFormats/BMPWriter.h>
  15. #include <LibGfx/Painter.h>
  16. #include <LibWeb/HTML/SelectedFile.h>
  17. #include <LibWebView/SearchEngine.h>
  18. #include <LibWebView/SourceHighlighter.h>
  19. #include <LibWebView/URL.h>
  20. #include <QClipboard>
  21. #include <QColorDialog>
  22. #include <QCoreApplication>
  23. #include <QCursor>
  24. #include <QDesktopServices>
  25. #include <QFileDialog>
  26. #include <QFont>
  27. #include <QFontMetrics>
  28. #include <QGuiApplication>
  29. #include <QImage>
  30. #include <QInputDialog>
  31. #include <QMenu>
  32. #include <QMessageBox>
  33. #include <QMimeData>
  34. #include <QMimeDatabase>
  35. #include <QMimeType>
  36. #include <QPainter>
  37. #include <QPoint>
  38. #include <QPushButton>
  39. #include <QResizeEvent>
  40. namespace Ladybird {
  41. static QIcon default_favicon()
  42. {
  43. static QIcon icon = load_icon_from_uri("resource://icons/48x48/app-browser.png"sv);
  44. return icon;
  45. }
  46. Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client, size_t page_index)
  47. : QWidget(window)
  48. , m_window(window)
  49. {
  50. m_layout = new QBoxLayout(QBoxLayout::Direction::TopToBottom, this);
  51. m_layout->setSpacing(0);
  52. m_layout->setContentsMargins(0, 0, 0, 0);
  53. m_view = new WebContentView(this, parent_client, page_index);
  54. m_find_in_page = new FindInPageWidget(this, m_view);
  55. m_find_in_page->setVisible(false);
  56. m_toolbar = new QToolBar(this);
  57. m_location_edit = new LocationEdit(this);
  58. m_hover_label = new QLabel(this);
  59. m_hover_label->hide();
  60. m_hover_label->setFrameShape(QFrame::Shape::Box);
  61. m_hover_label->setAutoFillBackground(true);
  62. auto* focus_location_editor_action = new QAction("Edit Location", this);
  63. focus_location_editor_action->setShortcut(QKeySequence("Ctrl+L"));
  64. addAction(focus_location_editor_action);
  65. m_layout->addWidget(m_toolbar);
  66. m_layout->addWidget(m_view);
  67. m_layout->addWidget(m_find_in_page);
  68. m_hamburger_button = new QToolButton(m_toolbar);
  69. m_hamburger_button->setText("Show &Menu");
  70. m_hamburger_button->setToolTip("Show Menu");
  71. m_hamburger_button->setIcon(create_tvg_icon_with_theme_colors("hamburger", palette()));
  72. m_hamburger_button->setPopupMode(QToolButton::InstantPopup);
  73. m_hamburger_button->setMenu(&m_window->hamburger_menu());
  74. m_hamburger_button->setStyleSheet(":menu-indicator {image: none}");
  75. recreate_toolbar_icons();
  76. m_favicon = default_favicon();
  77. m_toolbar->addAction(&m_window->go_back_action());
  78. m_toolbar->addAction(&m_window->go_forward_action());
  79. m_toolbar->addAction(&m_window->reload_action());
  80. m_toolbar->addWidget(m_location_edit);
  81. m_toolbar->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
  82. m_hamburger_button_action = m_toolbar->addWidget(m_hamburger_button);
  83. m_toolbar->setIconSize({ 16, 16 });
  84. // This is a little awkward, but without this Qt shrinks the button to the size of the icon.
  85. // Note: toolButtonStyle="0" -> ToolButtonIconOnly.
  86. m_toolbar->setStyleSheet("QToolButton[toolButtonStyle=\"0\"]{width:24px;height:24px}");
  87. m_hamburger_button_action->setVisible(!Settings::the()->show_menubar());
  88. QObject::connect(Settings::the(), &Settings::show_menubar_changed, this, [this](bool show_menubar) {
  89. m_hamburger_button_action->setVisible(!show_menubar);
  90. });
  91. m_reset_zoom_button = new QToolButton(m_toolbar);
  92. m_reset_zoom_button->setToolButtonStyle(Qt::ToolButtonTextOnly);
  93. m_reset_zoom_button->setToolTip("Reset zoom level");
  94. m_reset_zoom_button_action = m_toolbar->addWidget(m_reset_zoom_button);
  95. m_reset_zoom_button_action->setVisible(false);
  96. QObject::connect(m_reset_zoom_button, &QAbstractButton::clicked, [this] {
  97. view().reset_zoom();
  98. update_reset_zoom_button();
  99. m_window->update_zoom_menu();
  100. });
  101. view().on_activate_tab = [this] {
  102. m_window->activate_tab(tab_index());
  103. };
  104. view().on_close = [this] {
  105. m_window->close_tab(tab_index());
  106. };
  107. view().on_link_hover = [this](auto const& url) {
  108. m_hover_label->setText(qstring_from_ak_string(url.to_byte_string()));
  109. update_hover_label();
  110. m_hover_label->show();
  111. };
  112. view().on_link_unhover = [this]() {
  113. m_hover_label->hide();
  114. };
  115. view().on_load_start = [this](const URL::URL& url, bool) {
  116. if (m_inspector_widget)
  117. m_inspector_widget->reset();
  118. auto url_serialized = qstring_from_ak_string(url.serialize());
  119. m_title = url_serialized;
  120. emit title_changed(tab_index(), url_serialized);
  121. m_favicon = default_favicon();
  122. emit favicon_changed(tab_index(), m_favicon);
  123. m_location_edit->set_url(url);
  124. m_location_edit->setCursorPosition(0);
  125. };
  126. view().on_load_finish = [this](auto&) {
  127. if (m_inspector_widget != nullptr && m_inspector_widget->isVisible())
  128. m_inspector_widget->inspect();
  129. };
  130. view().on_url_change = [this](auto const& url) {
  131. m_location_edit->set_url(url);
  132. };
  133. QObject::connect(m_location_edit, &QLineEdit::returnPressed, this, &Tab::location_edit_return_pressed);
  134. view().on_title_change = [this](auto const& title) {
  135. m_title = qstring_from_ak_string(title);
  136. emit title_changed(tab_index(), m_title);
  137. };
  138. view().on_favicon_change = [this](auto const& bitmap) {
  139. auto qimage = QImage(bitmap.scanline_u8(0), bitmap.width(), bitmap.height(), QImage::Format_ARGB32);
  140. if (qimage.isNull())
  141. return;
  142. auto qpixmap = QPixmap::fromImage(qimage);
  143. if (qpixmap.isNull())
  144. return;
  145. m_favicon = qpixmap;
  146. emit favicon_changed(tab_index(), m_favicon);
  147. };
  148. view().on_request_alert = [this](auto const& message) {
  149. m_dialog = new QMessageBox(QMessageBox::Icon::Warning, "Ladybird", qstring_from_ak_string(message), QMessageBox::StandardButton::Ok, &view());
  150. m_dialog->exec();
  151. view().alert_closed();
  152. m_dialog = nullptr;
  153. };
  154. view().on_request_confirm = [this](auto const& message) {
  155. m_dialog = new QMessageBox(QMessageBox::Icon::Question, "Ladybird", qstring_from_ak_string(message), QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel, &view());
  156. auto result = m_dialog->exec();
  157. view().confirm_closed(result == QMessageBox::StandardButton::Ok || result == QDialog::Accepted);
  158. m_dialog = nullptr;
  159. };
  160. view().on_request_prompt = [this](auto const& message, auto const& default_) {
  161. m_dialog = new QInputDialog(&view());
  162. auto& dialog = static_cast<QInputDialog&>(*m_dialog);
  163. dialog.setWindowTitle("Ladybird");
  164. dialog.setLabelText(qstring_from_ak_string(message));
  165. dialog.setTextValue(qstring_from_ak_string(default_));
  166. if (dialog.exec() == QDialog::Accepted)
  167. view().prompt_closed(ak_string_from_qstring(dialog.textValue()));
  168. else
  169. view().prompt_closed({});
  170. m_dialog = nullptr;
  171. };
  172. view().on_request_set_prompt_text = [this](auto const& message) {
  173. if (m_dialog && is<QInputDialog>(*m_dialog))
  174. static_cast<QInputDialog&>(*m_dialog).setTextValue(qstring_from_ak_string(message));
  175. };
  176. view().on_request_accept_dialog = [this]() {
  177. if (m_dialog)
  178. m_dialog->accept();
  179. };
  180. view().on_request_dismiss_dialog = [this]() {
  181. if (m_dialog)
  182. m_dialog->reject();
  183. };
  184. view().on_request_color_picker = [this](Color current_color) {
  185. m_dialog = new QColorDialog(QColor(current_color.red(), current_color.green(), current_color.blue()), &view());
  186. auto& dialog = static_cast<QColorDialog&>(*m_dialog);
  187. dialog.setWindowTitle("Ladybird");
  188. dialog.setOption(QColorDialog::ShowAlphaChannel, false);
  189. QObject::connect(&dialog, &QColorDialog::currentColorChanged, this, [this](QColor const& color) {
  190. view().color_picker_update(Color(color.red(), color.green(), color.blue()), Web::HTML::ColorPickerUpdateState::Update);
  191. });
  192. if (dialog.exec() == QDialog::Accepted)
  193. view().color_picker_update(Color(dialog.selectedColor().red(), dialog.selectedColor().green(), dialog.selectedColor().blue()), Web::HTML::ColorPickerUpdateState::Closed);
  194. else
  195. view().color_picker_update({}, Web::HTML::ColorPickerUpdateState::Closed);
  196. m_dialog = nullptr;
  197. };
  198. view().on_request_file_picker = [this](auto const& accepted_file_types, auto allow_multiple_files) {
  199. Vector<Web::HTML::SelectedFile> selected_files;
  200. auto create_selected_file = [&](auto const& qfile_path) {
  201. auto file_path = ak_byte_string_from_qstring(qfile_path);
  202. if (auto file = Web::HTML::SelectedFile::from_file_path(file_path); file.is_error())
  203. warnln("Unable to open file {}: {}", file_path, file.error());
  204. else
  205. selected_files.append(file.release_value());
  206. };
  207. QStringList accepted_file_filters;
  208. QMimeDatabase mime_database;
  209. for (auto const& filter : accepted_file_types.filters) {
  210. filter.visit(
  211. [&](Web::HTML::FileFilter::FileType type) {
  212. QString title;
  213. QString filter;
  214. switch (type) {
  215. case Web::HTML::FileFilter::FileType::Audio:
  216. title = "Audio files";
  217. filter = "audio/";
  218. break;
  219. case Web::HTML::FileFilter::FileType::Image:
  220. title = "Image files";
  221. filter = "image/";
  222. break;
  223. case Web::HTML::FileFilter::FileType::Video:
  224. title = "Video files";
  225. filter = "video/";
  226. break;
  227. }
  228. QStringList extensions;
  229. for (auto const& mime_type : mime_database.allMimeTypes()) {
  230. if (mime_type.name().startsWith(filter))
  231. extensions.append(mime_type.globPatterns());
  232. }
  233. accepted_file_filters.append(QString("%1 (%2)").arg(title, extensions.join(" ")));
  234. },
  235. [&](Web::HTML::FileFilter::MimeType const& filter) {
  236. if (auto mime_type = mime_database.mimeTypeForName(qstring_from_ak_string(filter.value)); mime_type.isValid())
  237. accepted_file_filters.append(mime_type.filterString());
  238. },
  239. [&](Web::HTML::FileFilter::Extension const& filter) {
  240. auto extension = MUST(String::formatted("*.{}", filter.value));
  241. accepted_file_filters.append(qstring_from_ak_string(extension));
  242. });
  243. }
  244. accepted_file_filters.size() > 1 ? accepted_file_filters.prepend("All files (*)") : accepted_file_filters.append("All files (*)");
  245. auto filters = accepted_file_filters.join(";;");
  246. if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) {
  247. auto paths = QFileDialog::getOpenFileNames(this, "Select files", QDir::homePath(), filters);
  248. selected_files.ensure_capacity(static_cast<size_t>(paths.size()));
  249. for (auto const& path : paths)
  250. create_selected_file(path);
  251. } else {
  252. auto path = QFileDialog::getOpenFileName(this, "Select file", QDir::homePath(), filters);
  253. create_selected_file(path);
  254. }
  255. view().file_picker_closed(std::move(selected_files));
  256. };
  257. view().on_find_in_page = [this](auto current_match_index, auto const& total_match_count) {
  258. m_find_in_page->update_result_label(current_match_index, total_match_count);
  259. };
  260. m_select_dropdown = new QMenu("Select Dropdown", this);
  261. QObject::connect(m_select_dropdown, &QMenu::aboutToHide, this, [this]() {
  262. if (!m_select_dropdown->activeAction())
  263. view().select_dropdown_closed({});
  264. });
  265. view().on_request_select_dropdown = [this](Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items) {
  266. m_select_dropdown->clear();
  267. m_select_dropdown->setMinimumWidth(minimum_width / view().device_pixel_ratio());
  268. auto add_menu_item = [this](Web::HTML::SelectItemOption const& item_option, bool in_option_group) {
  269. QAction* action = new QAction(qstring_from_ak_string(in_option_group ? MUST(String::formatted(" {}", item_option.label)) : item_option.label), this);
  270. action->setCheckable(true);
  271. action->setChecked(item_option.selected);
  272. action->setDisabled(item_option.disabled);
  273. action->setData(QVariant(static_cast<uint>(item_option.id)));
  274. QObject::connect(action, &QAction::triggered, this, &Tab::select_dropdown_action);
  275. m_select_dropdown->addAction(action);
  276. };
  277. for (auto const& item : items) {
  278. if (item.has<Web::HTML::SelectItemOptionGroup>()) {
  279. auto const& item_option_group = item.get<Web::HTML::SelectItemOptionGroup>();
  280. QAction* subtitle = new QAction(qstring_from_ak_string(item_option_group.label), this);
  281. subtitle->setDisabled(true);
  282. m_select_dropdown->addAction(subtitle);
  283. for (auto const& item_option : item_option_group.items)
  284. add_menu_item(item_option, true);
  285. }
  286. if (item.has<Web::HTML::SelectItemOption>())
  287. add_menu_item(item.get<Web::HTML::SelectItemOption>(), false);
  288. if (item.has<Web::HTML::SelectItemSeparator>())
  289. m_select_dropdown->addSeparator();
  290. }
  291. m_select_dropdown->exec(view().map_point_to_global_position(content_position));
  292. };
  293. QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor);
  294. view().on_received_source = [this](auto const& url, auto const& source) {
  295. auto html = WebView::highlight_source(url, source);
  296. m_window->new_tab_from_content(html, Web::HTML::ActivateTab::Yes);
  297. };
  298. view().on_navigate_back = [this]() {
  299. back();
  300. };
  301. view().on_navigate_forward = [this]() {
  302. forward();
  303. };
  304. view().on_refresh = [this]() {
  305. reload();
  306. };
  307. view().on_restore_window = [this]() {
  308. m_window->showNormal();
  309. };
  310. view().on_reposition_window = [this](auto const& position) {
  311. m_window->move(position.x(), position.y());
  312. return Gfx::IntPoint { m_window->x(), m_window->y() };
  313. };
  314. view().on_resize_window = [this](auto const& size) {
  315. m_window->resize(size.width(), size.height());
  316. return Gfx::IntSize { m_window->width(), m_window->height() };
  317. };
  318. view().on_maximize_window = [this]() {
  319. m_window->showMaximized();
  320. return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
  321. };
  322. view().on_minimize_window = [this]() {
  323. m_window->showMinimized();
  324. return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
  325. };
  326. view().on_fullscreen_window = [this]() {
  327. m_window->showFullScreen();
  328. return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() };
  329. };
  330. view().on_insert_clipboard_entry = [](auto const& data, auto const&, auto const& mime_type) {
  331. QByteArray qdata { data.bytes_as_string_view().characters_without_null_termination(), static_cast<qsizetype>(data.bytes_as_string_view().length()) };
  332. auto* mime_data = new QMimeData();
  333. mime_data->setData(qstring_from_ak_string(mime_type), qdata);
  334. auto* clipboard = QGuiApplication::clipboard();
  335. clipboard->setMimeData(mime_data);
  336. };
  337. view().on_audio_play_state_changed = [this](auto play_state) {
  338. emit audio_play_state_changed(tab_index(), play_state);
  339. };
  340. view().on_navigation_buttons_state_changed = [this](auto back_enabled, auto forward_enabled) {
  341. m_can_navigate_back = back_enabled;
  342. m_can_navigate_forward = forward_enabled;
  343. emit navigation_buttons_state_changed(tab_index());
  344. };
  345. auto* reload_tab_action = new QAction("&Reload Tab", this);
  346. QObject::connect(reload_tab_action, &QAction::triggered, this, [this]() {
  347. reload();
  348. });
  349. auto* duplicate_tab_action = new QAction("&Duplicate Tab", this);
  350. QObject::connect(duplicate_tab_action, &QAction::triggered, this, [this]() {
  351. m_window->new_tab_from_url(view().url(), Web::HTML::ActivateTab::Yes);
  352. });
  353. auto* move_to_start_action = new QAction("Move to &Start", this);
  354. QObject::connect(move_to_start_action, &QAction::triggered, this, [this]() {
  355. m_window->move_tab(tab_index(), 0);
  356. });
  357. auto* move_to_end_action = new QAction("Move to &End", this);
  358. QObject::connect(move_to_end_action, &QAction::triggered, this, [this]() {
  359. m_window->move_tab(tab_index(), m_window->tab_count() - 1);
  360. });
  361. auto* close_tab_action = new QAction("&Close Tab", this);
  362. QObject::connect(close_tab_action, &QAction::triggered, this, [this]() {
  363. view().on_close();
  364. });
  365. auto* close_tabs_to_left_action = new QAction("C&lose Tabs to Left", this);
  366. QObject::connect(close_tabs_to_left_action, &QAction::triggered, this, [this]() {
  367. for (auto i = tab_index() - 1; i >= 0; i--) {
  368. m_window->close_tab(i);
  369. }
  370. });
  371. auto* close_tabs_to_right_action = new QAction("Close Tabs to R&ight", this);
  372. QObject::connect(close_tabs_to_right_action, &QAction::triggered, this, [this]() {
  373. for (auto i = m_window->tab_count() - 1; i > tab_index(); i--) {
  374. m_window->close_tab(i);
  375. }
  376. });
  377. auto* close_other_tabs_action = new QAction("Cl&ose Other Tabs", this);
  378. QObject::connect(close_other_tabs_action, &QAction::triggered, this, [this]() {
  379. for (auto i = m_window->tab_count() - 1; i >= 0; i--) {
  380. if (i == tab_index())
  381. continue;
  382. m_window->close_tab(i);
  383. }
  384. });
  385. m_context_menu = new QMenu("Context menu", this);
  386. m_context_menu->addAction(reload_tab_action);
  387. m_context_menu->addAction(duplicate_tab_action);
  388. m_context_menu->addSeparator();
  389. auto* move_tab_menu = m_context_menu->addMenu("Mo&ve Tab");
  390. move_tab_menu->addAction(move_to_start_action);
  391. move_tab_menu->addAction(move_to_end_action);
  392. m_context_menu->addSeparator();
  393. m_context_menu->addAction(close_tab_action);
  394. auto* close_multiple_tabs_menu = m_context_menu->addMenu("Close &Multiple Tabs");
  395. close_multiple_tabs_menu->addAction(close_tabs_to_left_action);
  396. close_multiple_tabs_menu->addAction(close_tabs_to_right_action);
  397. close_multiple_tabs_menu->addAction(close_other_tabs_action);
  398. auto* search_selected_text_action = new QAction("&Search for <query>", this);
  399. search_selected_text_action->setIcon(load_icon_from_uri("resource://icons/16x16/find.png"sv));
  400. QObject::connect(search_selected_text_action, &QAction::triggered, this, [this]() {
  401. auto url = MUST(String::formatted(Settings::the()->search_engine().query_url, URL::percent_encode(*m_page_context_menu_search_text)));
  402. m_window->new_tab_from_url(URL::URL(url), Web::HTML::ActivateTab::Yes);
  403. });
  404. auto take_screenshot = [this](auto type) {
  405. auto& view = this->view();
  406. view.take_screenshot(type)
  407. ->when_resolved([this](auto const& path) {
  408. auto message = MUST(String::formatted("Screenshot saved to: {}", path));
  409. QMessageBox dialog(this);
  410. dialog.setWindowTitle("Ladybird");
  411. dialog.setIcon(QMessageBox::Information);
  412. dialog.setText(qstring_from_ak_string(message));
  413. dialog.addButton(QMessageBox::Ok);
  414. dialog.addButton(QMessageBox::Open)->setText("Open folder");
  415. if (dialog.exec() == QMessageBox::Open) {
  416. auto path_url = QUrl::fromLocalFile(qstring_from_ak_string(path.dirname()));
  417. QDesktopServices::openUrl(path_url);
  418. }
  419. })
  420. .when_rejected([this](auto const& error) {
  421. auto error_message = MUST(String::formatted("{}", error));
  422. QMessageBox::warning(this, "Ladybird", qstring_from_ak_string(error_message));
  423. });
  424. };
  425. auto* take_visible_screenshot_action = new QAction("Take &Visible Screenshot", this);
  426. take_visible_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
  427. QObject::connect(take_visible_screenshot_action, &QAction::triggered, this, [take_screenshot]() {
  428. take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible);
  429. });
  430. auto* take_full_screenshot_action = new QAction("Take &Full Screenshot", this);
  431. take_full_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
  432. QObject::connect(take_full_screenshot_action, &QAction::triggered, this, [take_screenshot]() {
  433. take_screenshot(WebView::ViewImplementation::ScreenshotType::Full);
  434. });
  435. m_page_context_menu = new QMenu("Context menu", this);
  436. m_page_context_menu->addAction(&m_window->go_back_action());
  437. m_page_context_menu->addAction(&m_window->go_forward_action());
  438. m_page_context_menu->addAction(&m_window->reload_action());
  439. m_page_context_menu->addSeparator();
  440. m_page_context_menu->addAction(&m_window->copy_selection_action());
  441. m_page_context_menu->addAction(&m_window->paste_action());
  442. m_page_context_menu->addAction(&m_window->select_all_action());
  443. m_page_context_menu->addSeparator();
  444. m_page_context_menu->addAction(search_selected_text_action);
  445. m_page_context_menu->addSeparator();
  446. m_page_context_menu->addAction(take_visible_screenshot_action);
  447. m_page_context_menu->addAction(take_full_screenshot_action);
  448. m_page_context_menu->addSeparator();
  449. m_page_context_menu->addAction(&m_window->view_source_action());
  450. m_page_context_menu->addAction(&m_window->inspect_dom_node_action());
  451. view().on_context_menu_request = [this, search_selected_text_action](Gfx::IntPoint content_position) {
  452. auto selected_text = Settings::the()->enable_search()
  453. ? view().selected_text_with_whitespace_collapsed()
  454. : OptionalNone {};
  455. TemporaryChange change_url { m_page_context_menu_search_text, std::move(selected_text) };
  456. if (m_page_context_menu_search_text.has_value()) {
  457. auto action_text = WebView::format_search_query_for_display(Settings::the()->search_engine().query_url, *m_page_context_menu_search_text);
  458. search_selected_text_action->setText(qstring_from_ak_string(action_text));
  459. search_selected_text_action->setVisible(true);
  460. } else {
  461. search_selected_text_action->setVisible(false);
  462. }
  463. m_page_context_menu->exec(view().map_point_to_global_position(content_position));
  464. };
  465. auto* open_link_action = new QAction("&Open", this);
  466. open_link_action->setIcon(load_icon_from_uri("resource://icons/16x16/go-forward.png"sv));
  467. QObject::connect(open_link_action, &QAction::triggered, this, [this]() {
  468. open_link(m_link_context_menu_url);
  469. });
  470. auto* open_link_in_new_tab_action = new QAction("&Open in New &Tab", this);
  471. open_link_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  472. QObject::connect(open_link_in_new_tab_action, &QAction::triggered, this, [this]() {
  473. open_link_in_new_tab(m_link_context_menu_url);
  474. });
  475. m_link_context_menu_copy_url_action = new QAction("Copy &URL", this);
  476. m_link_context_menu_copy_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  477. QObject::connect(m_link_context_menu_copy_url_action, &QAction::triggered, this, [this]() {
  478. copy_link_url(m_link_context_menu_url);
  479. });
  480. m_link_context_menu = new QMenu("Link context menu", this);
  481. m_link_context_menu->addAction(open_link_action);
  482. m_link_context_menu->addAction(open_link_in_new_tab_action);
  483. m_link_context_menu->addSeparator();
  484. m_link_context_menu->addAction(m_link_context_menu_copy_url_action);
  485. m_link_context_menu->addSeparator();
  486. m_link_context_menu->addAction(&m_window->inspect_dom_node_action());
  487. view().on_link_context_menu_request = [this](auto const& url, Gfx::IntPoint content_position) {
  488. m_link_context_menu_url = url;
  489. switch (WebView::url_type(url)) {
  490. case WebView::URLType::Email:
  491. m_link_context_menu_copy_url_action->setText("Copy &Email Address");
  492. break;
  493. case WebView::URLType::Telephone:
  494. m_link_context_menu_copy_url_action->setText("Copy &Phone Number");
  495. break;
  496. case WebView::URLType::Other:
  497. m_link_context_menu_copy_url_action->setText("Copy &URL");
  498. break;
  499. }
  500. m_link_context_menu->exec(view().map_point_to_global_position(content_position));
  501. };
  502. auto* open_image_action = new QAction("&Open Image", this);
  503. open_image_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
  504. QObject::connect(open_image_action, &QAction::triggered, this, [this]() {
  505. open_link(m_image_context_menu_url);
  506. });
  507. auto* open_image_in_new_tab_action = new QAction("&Open Image in New &Tab", this);
  508. open_image_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  509. QObject::connect(open_image_in_new_tab_action, &QAction::triggered, this, [this]() {
  510. open_link_in_new_tab(m_image_context_menu_url);
  511. });
  512. auto* copy_image_action = new QAction("&Copy Image", this);
  513. copy_image_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  514. QObject::connect(copy_image_action, &QAction::triggered, this, [this]() {
  515. auto* bitmap = m_image_context_menu_bitmap.bitmap();
  516. if (bitmap == nullptr)
  517. return;
  518. auto data = Gfx::BMPWriter::encode(*bitmap);
  519. if (data.is_error())
  520. return;
  521. auto image = QImage::fromData(data.value().data(), data.value().size(), "BMP");
  522. if (image.isNull())
  523. return;
  524. auto* clipboard = QGuiApplication::clipboard();
  525. clipboard->setImage(image);
  526. });
  527. auto* copy_image_url_action = new QAction("Copy Image &URL", this);
  528. copy_image_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  529. QObject::connect(copy_image_url_action, &QAction::triggered, this, [this]() {
  530. copy_link_url(m_image_context_menu_url);
  531. });
  532. m_image_context_menu = new QMenu("Image context menu", this);
  533. m_image_context_menu->addAction(open_image_action);
  534. m_image_context_menu->addAction(open_image_in_new_tab_action);
  535. m_image_context_menu->addSeparator();
  536. m_image_context_menu->addAction(copy_image_action);
  537. m_image_context_menu->addAction(copy_image_url_action);
  538. m_image_context_menu->addSeparator();
  539. m_image_context_menu->addAction(&m_window->inspect_dom_node_action());
  540. view().on_image_context_menu_request = [this](auto& image_url, Gfx::IntPoint content_position, Gfx::ShareableBitmap const& shareable_bitmap) {
  541. m_image_context_menu_url = image_url;
  542. m_image_context_menu_bitmap = shareable_bitmap;
  543. m_image_context_menu->exec(view().map_point_to_global_position(content_position));
  544. };
  545. m_media_context_menu_play_icon = load_icon_from_uri("resource://icons/16x16/play.png"sv);
  546. m_media_context_menu_pause_icon = load_icon_from_uri("resource://icons/16x16/pause.png"sv);
  547. m_media_context_menu_mute_icon = load_icon_from_uri("resource://icons/16x16/audio-volume-muted.png"sv);
  548. m_media_context_menu_unmute_icon = load_icon_from_uri("resource://icons/16x16/audio-volume-high.png"sv);
  549. m_media_context_menu_play_pause_action = new QAction("&Play", this);
  550. m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_play_icon);
  551. QObject::connect(m_media_context_menu_play_pause_action, &QAction::triggered, this, [this]() {
  552. view().toggle_media_play_state();
  553. });
  554. m_media_context_menu_mute_unmute_action = new QAction("&Mute", this);
  555. m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_mute_icon);
  556. QObject::connect(m_media_context_menu_mute_unmute_action, &QAction::triggered, this, [this]() {
  557. view().toggle_media_mute_state();
  558. });
  559. m_media_context_menu_controls_action = new QAction("Show &Controls", this);
  560. m_media_context_menu_controls_action->setCheckable(true);
  561. QObject::connect(m_media_context_menu_controls_action, &QAction::triggered, this, [this]() {
  562. view().toggle_media_controls_state();
  563. });
  564. m_media_context_menu_loop_action = new QAction("&Loop", this);
  565. m_media_context_menu_loop_action->setCheckable(true);
  566. QObject::connect(m_media_context_menu_loop_action, &QAction::triggered, this, [this]() {
  567. view().toggle_media_loop_state();
  568. });
  569. auto* open_audio_action = new QAction("&Open Audio", this);
  570. open_audio_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-sound.png"sv));
  571. QObject::connect(open_audio_action, &QAction::triggered, this, [this]() {
  572. open_link(m_media_context_menu_url);
  573. });
  574. auto* open_audio_in_new_tab_action = new QAction("Open Audio in New &Tab", this);
  575. open_audio_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  576. QObject::connect(open_audio_in_new_tab_action, &QAction::triggered, this, [this]() {
  577. open_link_in_new_tab(m_media_context_menu_url);
  578. });
  579. auto* copy_audio_url_action = new QAction("Copy Audio &URL", this);
  580. copy_audio_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  581. QObject::connect(copy_audio_url_action, &QAction::triggered, this, [this]() {
  582. copy_link_url(m_media_context_menu_url);
  583. });
  584. m_audio_context_menu = new QMenu("Audio context menu", this);
  585. m_audio_context_menu->addAction(m_media_context_menu_play_pause_action);
  586. m_audio_context_menu->addAction(m_media_context_menu_mute_unmute_action);
  587. m_audio_context_menu->addAction(m_media_context_menu_controls_action);
  588. m_audio_context_menu->addAction(m_media_context_menu_loop_action);
  589. m_audio_context_menu->addSeparator();
  590. m_audio_context_menu->addAction(open_audio_action);
  591. m_audio_context_menu->addAction(open_audio_in_new_tab_action);
  592. m_audio_context_menu->addSeparator();
  593. m_audio_context_menu->addAction(copy_audio_url_action);
  594. m_audio_context_menu->addSeparator();
  595. m_audio_context_menu->addAction(&m_window->inspect_dom_node_action());
  596. auto* open_video_action = new QAction("&Open Video", this);
  597. open_video_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-video.png"sv));
  598. QObject::connect(open_video_action, &QAction::triggered, this, [this]() {
  599. open_link(m_media_context_menu_url);
  600. });
  601. auto* open_video_in_new_tab_action = new QAction("Open Video in New &Tab", this);
  602. open_video_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  603. QObject::connect(open_video_in_new_tab_action, &QAction::triggered, this, [this]() {
  604. open_link_in_new_tab(m_media_context_menu_url);
  605. });
  606. auto* copy_video_url_action = new QAction("Copy Video &URL", this);
  607. copy_video_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  608. QObject::connect(copy_video_url_action, &QAction::triggered, this, [this]() {
  609. copy_link_url(m_media_context_menu_url);
  610. });
  611. m_video_context_menu = new QMenu("Video context menu", this);
  612. m_video_context_menu->addAction(m_media_context_menu_play_pause_action);
  613. m_video_context_menu->addAction(m_media_context_menu_mute_unmute_action);
  614. m_video_context_menu->addAction(m_media_context_menu_controls_action);
  615. m_video_context_menu->addAction(m_media_context_menu_loop_action);
  616. m_video_context_menu->addSeparator();
  617. m_video_context_menu->addAction(open_video_action);
  618. m_video_context_menu->addAction(open_video_in_new_tab_action);
  619. m_video_context_menu->addSeparator();
  620. m_video_context_menu->addAction(copy_video_url_action);
  621. m_video_context_menu->addSeparator();
  622. m_video_context_menu->addAction(&m_window->inspect_dom_node_action());
  623. view().on_media_context_menu_request = [this](Gfx::IntPoint content_position, Web::Page::MediaContextMenu const& menu) {
  624. m_media_context_menu_url = menu.media_url;
  625. if (menu.is_playing) {
  626. m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_pause_icon);
  627. m_media_context_menu_play_pause_action->setText("&Pause");
  628. } else {
  629. m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_play_icon);
  630. m_media_context_menu_play_pause_action->setText("&Play");
  631. }
  632. if (menu.is_muted) {
  633. m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_unmute_icon);
  634. m_media_context_menu_mute_unmute_action->setText("Un&mute");
  635. } else {
  636. m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_mute_icon);
  637. m_media_context_menu_mute_unmute_action->setText("&Mute");
  638. }
  639. m_media_context_menu_controls_action->setChecked(menu.has_user_agent_controls);
  640. m_media_context_menu_loop_action->setChecked(menu.is_looping);
  641. auto screen_position = view().map_point_to_global_position(content_position);
  642. if (menu.is_video)
  643. m_video_context_menu->exec(screen_position);
  644. else
  645. m_audio_context_menu->exec(screen_position);
  646. };
  647. }
  648. Tab::~Tab()
  649. {
  650. close_sub_widgets();
  651. // Delete the InspectorWidget explicitly to ensure it is deleted before the WebContentView. Otherwise, Qt
  652. // can destroy these objects in any order, which may cause use-after-free in InspectorWidget's destructor.
  653. delete m_inspector_widget;
  654. }
  655. void Tab::select_dropdown_action()
  656. {
  657. QAction* action = qobject_cast<QAction*>(sender());
  658. view().select_dropdown_closed(action->data().value<uint>());
  659. }
  660. void Tab::update_reset_zoom_button()
  661. {
  662. auto zoom_level = view().zoom_level();
  663. if (zoom_level != 1.0f) {
  664. auto zoom_level_text = MUST(String::formatted("{}%", round_to<int>(zoom_level * 100)));
  665. m_reset_zoom_button->setText(qstring_from_ak_string(zoom_level_text));
  666. m_reset_zoom_button_action->setVisible(true);
  667. } else {
  668. m_reset_zoom_button_action->setVisible(false);
  669. }
  670. }
  671. void Tab::focus_location_editor()
  672. {
  673. m_location_edit->setFocus();
  674. m_location_edit->selectAll();
  675. }
  676. void Tab::navigate(URL::URL const& url)
  677. {
  678. view().load(url);
  679. }
  680. void Tab::load_html(StringView html)
  681. {
  682. view().load_html(html);
  683. }
  684. void Tab::back()
  685. {
  686. view().traverse_the_history_by_delta(-1);
  687. }
  688. void Tab::forward()
  689. {
  690. view().traverse_the_history_by_delta(1);
  691. }
  692. void Tab::reload()
  693. {
  694. view().reload();
  695. }
  696. void Tab::open_link(URL::URL const& url)
  697. {
  698. view().on_link_click(url, "", 0);
  699. }
  700. void Tab::open_link_in_new_tab(URL::URL const& url)
  701. {
  702. view().on_link_click(url, "_blank", Web::UIEvents::Mod_Ctrl);
  703. }
  704. void Tab::copy_link_url(URL::URL const& url)
  705. {
  706. auto* clipboard = QGuiApplication::clipboard();
  707. clipboard->setText(qstring_from_ak_string(WebView::url_text_to_copy(url)));
  708. }
  709. void Tab::location_edit_return_pressed()
  710. {
  711. navigate(m_location_edit->url());
  712. }
  713. void Tab::open_file()
  714. {
  715. auto filename = QFileDialog::getOpenFileUrl(this, "Open file", QDir::homePath(), "All Files (*.*)");
  716. if (filename.isValid()) {
  717. navigate(ak_url_from_qurl(filename));
  718. }
  719. }
  720. int Tab::tab_index()
  721. {
  722. return m_window->tab_index(this);
  723. }
  724. void Tab::debug_request(ByteString const& request, ByteString const& argument)
  725. {
  726. m_view->debug_request(request, argument);
  727. }
  728. void Tab::resizeEvent(QResizeEvent* event)
  729. {
  730. QWidget::resizeEvent(event);
  731. if (m_hover_label->isVisible())
  732. update_hover_label();
  733. }
  734. void Tab::update_hover_label()
  735. {
  736. m_hover_label->resize(QFontMetrics(m_hover_label->font()).boundingRect(m_hover_label->text()).adjusted(-4, -2, 4, 2).size());
  737. auto hover_label_height = height() - m_hover_label->height() - 8;
  738. if (m_find_in_page->isVisible())
  739. hover_label_height -= m_find_in_page->height() - 4;
  740. m_hover_label->move(6, hover_label_height);
  741. m_hover_label->raise();
  742. }
  743. void Tab::update_navigation_buttons_state()
  744. {
  745. if (m_window->current_tab() != this)
  746. return;
  747. m_window->go_back_action().setEnabled(m_can_navigate_back);
  748. m_window->go_forward_action().setEnabled(m_can_navigate_forward);
  749. }
  750. bool Tab::event(QEvent* event)
  751. {
  752. if (event->type() == QEvent::PaletteChange) {
  753. recreate_toolbar_icons();
  754. return QWidget::event(event);
  755. }
  756. return QWidget::event(event);
  757. }
  758. void Tab::recreate_toolbar_icons()
  759. {
  760. m_window->go_back_action().setIcon(create_tvg_icon_with_theme_colors("back", palette()));
  761. m_window->go_forward_action().setIcon(create_tvg_icon_with_theme_colors("forward", palette()));
  762. m_window->reload_action().setIcon(create_tvg_icon_with_theme_colors("reload", palette()));
  763. m_window->new_tab_action().setIcon(create_tvg_icon_with_theme_colors("new_tab", palette()));
  764. m_hamburger_button->setIcon(create_tvg_icon_with_theme_colors("hamburger", palette()));
  765. }
  766. void Tab::show_inspector_window(InspectorTarget inspector_target)
  767. {
  768. if (!m_inspector_widget)
  769. m_inspector_widget = new InspectorWidget(this, view());
  770. else
  771. m_inspector_widget->inspect();
  772. m_inspector_widget->show();
  773. m_inspector_widget->activateWindow();
  774. m_inspector_widget->raise();
  775. if (inspector_target == InspectorTarget::HoveredElement)
  776. m_inspector_widget->select_hovered_node();
  777. else
  778. m_inspector_widget->select_default_node();
  779. }
  780. void Tab::show_find_in_page()
  781. {
  782. m_find_in_page->setVisible(true);
  783. m_find_in_page->setFocus();
  784. }
  785. void Tab::find_previous()
  786. {
  787. m_find_in_page->find_previous();
  788. }
  789. void Tab::find_next()
  790. {
  791. m_find_in_page->find_next();
  792. }
  793. void Tab::close_sub_widgets()
  794. {
  795. auto close_widget_window = [](auto* widget) {
  796. if (widget)
  797. widget->close();
  798. };
  799. close_widget_window(m_inspector_widget);
  800. }
  801. void Tab::set_block_popups(bool enabled)
  802. {
  803. debug_request("block-pop-ups", enabled ? "on" : "off");
  804. }
  805. void Tab::set_line_box_borders(bool enabled)
  806. {
  807. debug_request("set-line-box-borders", enabled ? "on" : "off");
  808. }
  809. void Tab::set_same_origin_policy(bool enabled)
  810. {
  811. debug_request("same-origin-policy", enabled ? "on" : "off");
  812. }
  813. void Tab::set_scripting(bool enabled)
  814. {
  815. debug_request("scripting", enabled ? "on" : "off");
  816. }
  817. void Tab::set_user_agent_string(ByteString const& user_agent)
  818. {
  819. debug_request("spoof-user-agent", user_agent);
  820. // Clear the cache to ensure requests are re-done with the new user agent.
  821. debug_request("clear-cache");
  822. }
  823. void Tab::set_navigator_compatibility_mode(ByteString const& compatibility_mode)
  824. {
  825. debug_request("navigator-compatibility-mode", compatibility_mode);
  826. }
  827. void Tab::set_preferred_languages(Vector<String> const& preferred_languages)
  828. {
  829. m_view->set_preferred_languages(preferred_languages);
  830. }
  831. void Tab::set_enable_do_not_track(bool enable)
  832. {
  833. m_view->set_enable_do_not_track(enable);
  834. }
  835. }