Tab.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982
  1. /*
  2. * Copyright (c) 2022, Andreas Kling <andreas@ladybird.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/DeprecatedPainter.h>
  15. #include <LibGfx/ImageFormats/BMPWriter.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. QObject::connect(m_dialog, &QDialog::finished, this, [this]() {
  151. view().alert_closed();
  152. m_dialog = nullptr;
  153. });
  154. m_dialog->open();
  155. };
  156. view().on_request_confirm = [this](auto const& message) {
  157. m_dialog = new QMessageBox(QMessageBox::Icon::Question, "Ladybird", qstring_from_ak_string(message), QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel, &view());
  158. QObject::connect(m_dialog, &QDialog::finished, this, [this](auto result) {
  159. view().confirm_closed(result == QMessageBox::StandardButton::Ok || result == QDialog::Accepted);
  160. m_dialog = nullptr;
  161. });
  162. m_dialog->open();
  163. };
  164. view().on_request_prompt = [this](auto const& message, auto const& default_) {
  165. m_dialog = new QInputDialog(&view());
  166. auto& dialog = static_cast<QInputDialog&>(*m_dialog);
  167. dialog.setWindowTitle("Ladybird");
  168. dialog.setLabelText(qstring_from_ak_string(message));
  169. dialog.setTextValue(qstring_from_ak_string(default_));
  170. QObject::connect(m_dialog, &QDialog::finished, this, [this](auto result) {
  171. if (result == QDialog::Accepted) {
  172. auto& dialog = static_cast<QInputDialog&>(*m_dialog);
  173. view().prompt_closed(ak_string_from_qstring(dialog.textValue()));
  174. } else {
  175. view().prompt_closed({});
  176. }
  177. m_dialog = nullptr;
  178. });
  179. m_dialog->open();
  180. };
  181. view().on_request_set_prompt_text = [this](auto const& message) {
  182. if (m_dialog && is<QInputDialog>(*m_dialog))
  183. static_cast<QInputDialog&>(*m_dialog).setTextValue(qstring_from_ak_string(message));
  184. };
  185. view().on_request_accept_dialog = [this]() {
  186. if (m_dialog)
  187. m_dialog->accept();
  188. };
  189. view().on_request_dismiss_dialog = [this]() {
  190. if (m_dialog)
  191. m_dialog->reject();
  192. };
  193. view().on_request_color_picker = [this](Color current_color) {
  194. m_dialog = new QColorDialog(QColor(current_color.red(), current_color.green(), current_color.blue()), &view());
  195. auto& dialog = static_cast<QColorDialog&>(*m_dialog);
  196. dialog.setWindowTitle("Ladybird");
  197. dialog.setOption(QColorDialog::ShowAlphaChannel, false);
  198. QObject::connect(&dialog, &QColorDialog::currentColorChanged, this, [this](QColor const& color) {
  199. view().color_picker_update(Color(color.red(), color.green(), color.blue()), Web::HTML::ColorPickerUpdateState::Update);
  200. });
  201. QObject::connect(m_dialog, &QDialog::finished, this, [this](auto result) {
  202. if (result == QDialog::Accepted) {
  203. auto& dialog = static_cast<QColorDialog&>(*m_dialog);
  204. view().color_picker_update(Color(dialog.selectedColor().red(), dialog.selectedColor().green(), dialog.selectedColor().blue()), Web::HTML::ColorPickerUpdateState::Closed);
  205. } else {
  206. view().color_picker_update({}, Web::HTML::ColorPickerUpdateState::Closed);
  207. }
  208. m_dialog = nullptr;
  209. });
  210. m_dialog->open();
  211. };
  212. view().on_request_file_picker = [this](auto const& accepted_file_types, auto allow_multiple_files) {
  213. Vector<Web::HTML::SelectedFile> selected_files;
  214. auto create_selected_file = [&](auto const& qfile_path) {
  215. auto file_path = ak_byte_string_from_qstring(qfile_path);
  216. if (auto file = Web::HTML::SelectedFile::from_file_path(file_path); file.is_error())
  217. warnln("Unable to open file {}: {}", file_path, file.error());
  218. else
  219. selected_files.append(file.release_value());
  220. };
  221. QStringList accepted_file_filters;
  222. QMimeDatabase mime_database;
  223. for (auto const& filter : accepted_file_types.filters) {
  224. filter.visit(
  225. [&](Web::HTML::FileFilter::FileType type) {
  226. QString title;
  227. QString filter;
  228. switch (type) {
  229. case Web::HTML::FileFilter::FileType::Audio:
  230. title = "Audio files";
  231. filter = "audio/";
  232. break;
  233. case Web::HTML::FileFilter::FileType::Image:
  234. title = "Image files";
  235. filter = "image/";
  236. break;
  237. case Web::HTML::FileFilter::FileType::Video:
  238. title = "Video files";
  239. filter = "video/";
  240. break;
  241. }
  242. QStringList extensions;
  243. for (auto const& mime_type : mime_database.allMimeTypes()) {
  244. if (mime_type.name().startsWith(filter))
  245. extensions.append(mime_type.globPatterns());
  246. }
  247. accepted_file_filters.append(QString("%1 (%2)").arg(title, extensions.join(" ")));
  248. },
  249. [&](Web::HTML::FileFilter::MimeType const& filter) {
  250. if (auto mime_type = mime_database.mimeTypeForName(qstring_from_ak_string(filter.value)); mime_type.isValid())
  251. accepted_file_filters.append(mime_type.filterString());
  252. },
  253. [&](Web::HTML::FileFilter::Extension const& filter) {
  254. auto extension = MUST(String::formatted("*.{}", filter.value));
  255. accepted_file_filters.append(qstring_from_ak_string(extension));
  256. });
  257. }
  258. accepted_file_filters.size() > 1 ? accepted_file_filters.prepend("All files (*)") : accepted_file_filters.append("All files (*)");
  259. auto filters = accepted_file_filters.join(";;");
  260. if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) {
  261. auto paths = QFileDialog::getOpenFileNames(this, "Select files", QDir::homePath(), filters);
  262. selected_files.ensure_capacity(static_cast<size_t>(paths.size()));
  263. for (auto const& path : paths)
  264. create_selected_file(path);
  265. } else {
  266. auto path = QFileDialog::getOpenFileName(this, "Select file", QDir::homePath(), filters);
  267. create_selected_file(path);
  268. }
  269. view().file_picker_closed(std::move(selected_files));
  270. };
  271. view().on_find_in_page = [this](auto current_match_index, auto const& total_match_count) {
  272. m_find_in_page->update_result_label(current_match_index, total_match_count);
  273. };
  274. QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor);
  275. view().on_received_source = [this](auto const& url, auto const& base_url, auto const& source) {
  276. auto html = WebView::highlight_source(url, base_url, source, Syntax::Language::HTML, WebView::HighlightOutputMode::FullDocument);
  277. m_window->new_tab_from_content(html, Web::HTML::ActivateTab::Yes);
  278. };
  279. view().on_inspector_requested_style_sheet_source = [this](auto const& identifier) {
  280. view().request_style_sheet_source(identifier);
  281. };
  282. view().on_restore_window = [this]() {
  283. m_window->showNormal();
  284. };
  285. view().on_reposition_window = [this](auto const& position) {
  286. m_window->move(position.x(), position.y());
  287. view().did_update_window_rect();
  288. };
  289. view().on_resize_window = [this](auto const& size) {
  290. m_window->resize(size.width(), size.height());
  291. view().did_update_window_rect();
  292. };
  293. view().on_maximize_window = [this]() {
  294. m_window->showMaximized();
  295. view().did_update_window_rect();
  296. };
  297. view().on_minimize_window = [this]() {
  298. m_window->showMinimized();
  299. };
  300. view().on_fullscreen_window = [this]() {
  301. m_window->showFullScreen();
  302. view().did_update_window_rect();
  303. };
  304. view().on_insert_clipboard_entry = [](auto const& data, auto const&, auto const& mime_type) {
  305. QByteArray qdata { data.bytes_as_string_view().characters_without_null_termination(), static_cast<qsizetype>(data.bytes_as_string_view().length()) };
  306. auto* mime_data = new QMimeData();
  307. mime_data->setData(qstring_from_ak_string(mime_type), qdata);
  308. auto* clipboard = QGuiApplication::clipboard();
  309. clipboard->setMimeData(mime_data);
  310. };
  311. view().on_audio_play_state_changed = [this](auto play_state) {
  312. emit audio_play_state_changed(tab_index(), play_state);
  313. };
  314. view().on_navigation_buttons_state_changed = [this](auto back_enabled, auto forward_enabled) {
  315. m_can_navigate_back = back_enabled;
  316. m_can_navigate_forward = forward_enabled;
  317. emit navigation_buttons_state_changed(tab_index());
  318. };
  319. auto* reload_tab_action = new QAction("&Reload Tab", this);
  320. QObject::connect(reload_tab_action, &QAction::triggered, this, [this]() {
  321. reload();
  322. });
  323. auto* duplicate_tab_action = new QAction("&Duplicate Tab", this);
  324. QObject::connect(duplicate_tab_action, &QAction::triggered, this, [this]() {
  325. m_window->new_tab_from_url(view().url(), Web::HTML::ActivateTab::Yes);
  326. });
  327. auto* move_to_start_action = new QAction("Move to &Start", this);
  328. QObject::connect(move_to_start_action, &QAction::triggered, this, [this]() {
  329. m_window->move_tab(tab_index(), 0);
  330. });
  331. auto* move_to_end_action = new QAction("Move to &End", this);
  332. QObject::connect(move_to_end_action, &QAction::triggered, this, [this]() {
  333. m_window->move_tab(tab_index(), m_window->tab_count() - 1);
  334. });
  335. auto* close_tab_action = new QAction("&Close Tab", this);
  336. QObject::connect(close_tab_action, &QAction::triggered, this, [this]() {
  337. view().on_close();
  338. });
  339. auto* close_tabs_to_left_action = new QAction("C&lose Tabs to Left", this);
  340. QObject::connect(close_tabs_to_left_action, &QAction::triggered, this, [this]() {
  341. for (auto i = tab_index() - 1; i >= 0; i--) {
  342. m_window->close_tab(i);
  343. }
  344. });
  345. auto* close_tabs_to_right_action = new QAction("Close Tabs to R&ight", this);
  346. QObject::connect(close_tabs_to_right_action, &QAction::triggered, this, [this]() {
  347. for (auto i = m_window->tab_count() - 1; i > tab_index(); i--) {
  348. m_window->close_tab(i);
  349. }
  350. });
  351. auto* close_other_tabs_action = new QAction("Cl&ose Other Tabs", this);
  352. QObject::connect(close_other_tabs_action, &QAction::triggered, this, [this]() {
  353. for (auto i = m_window->tab_count() - 1; i >= 0; i--) {
  354. if (i == tab_index())
  355. continue;
  356. m_window->close_tab(i);
  357. }
  358. });
  359. m_context_menu = new QMenu("Context menu", this);
  360. m_context_menu->addAction(reload_tab_action);
  361. m_context_menu->addAction(duplicate_tab_action);
  362. m_context_menu->addSeparator();
  363. auto* move_tab_menu = m_context_menu->addMenu("Mo&ve Tab");
  364. move_tab_menu->addAction(move_to_start_action);
  365. move_tab_menu->addAction(move_to_end_action);
  366. m_context_menu->addSeparator();
  367. m_context_menu->addAction(close_tab_action);
  368. auto* close_multiple_tabs_menu = m_context_menu->addMenu("Close &Multiple Tabs");
  369. close_multiple_tabs_menu->addAction(close_tabs_to_left_action);
  370. close_multiple_tabs_menu->addAction(close_tabs_to_right_action);
  371. close_multiple_tabs_menu->addAction(close_other_tabs_action);
  372. auto* search_selected_text_action = new QAction("&Search for <query>", this);
  373. search_selected_text_action->setIcon(load_icon_from_uri("resource://icons/16x16/find.png"sv));
  374. QObject::connect(search_selected_text_action, &QAction::triggered, this, [this]() {
  375. auto url = MUST(String::formatted(Settings::the()->search_engine().query_url, URL::percent_encode(*m_page_context_menu_search_text)));
  376. m_window->new_tab_from_url(URL::URL(url), Web::HTML::ActivateTab::Yes);
  377. });
  378. auto take_screenshot = [this](auto type) {
  379. auto& view = this->view();
  380. view.take_screenshot(type)
  381. ->when_resolved([this](auto const& path) {
  382. auto message = MUST(String::formatted("Screenshot saved to: {}", path));
  383. QMessageBox dialog(this);
  384. dialog.setWindowTitle("Ladybird");
  385. dialog.setIcon(QMessageBox::Information);
  386. dialog.setText(qstring_from_ak_string(message));
  387. dialog.addButton(QMessageBox::Ok);
  388. dialog.addButton(QMessageBox::Open)->setText("Open folder");
  389. if (dialog.exec() == QMessageBox::Open) {
  390. auto path_url = QUrl::fromLocalFile(qstring_from_ak_string(path.dirname()));
  391. QDesktopServices::openUrl(path_url);
  392. }
  393. })
  394. .when_rejected([this](auto const& error) {
  395. if (error.is_errno() && error.code() == ECANCELED)
  396. return;
  397. auto error_message = MUST(String::formatted("{}", error));
  398. QMessageBox::warning(this, "Ladybird", qstring_from_ak_string(error_message));
  399. });
  400. };
  401. auto* take_visible_screenshot_action = new QAction("Take &Visible Screenshot", this);
  402. take_visible_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
  403. QObject::connect(take_visible_screenshot_action, &QAction::triggered, this, [take_screenshot]() {
  404. take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible);
  405. });
  406. auto* take_full_screenshot_action = new QAction("Take &Full Screenshot", this);
  407. take_full_screenshot_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
  408. QObject::connect(take_full_screenshot_action, &QAction::triggered, this, [take_screenshot]() {
  409. take_screenshot(WebView::ViewImplementation::ScreenshotType::Full);
  410. });
  411. m_page_context_menu = new QMenu("Context menu", this);
  412. m_page_context_menu->addAction(&m_window->go_back_action());
  413. m_page_context_menu->addAction(&m_window->go_forward_action());
  414. m_page_context_menu->addAction(&m_window->reload_action());
  415. m_page_context_menu->addSeparator();
  416. m_page_context_menu->addAction(&m_window->copy_selection_action());
  417. m_page_context_menu->addAction(&m_window->paste_action());
  418. m_page_context_menu->addAction(&m_window->select_all_action());
  419. m_page_context_menu->addSeparator();
  420. m_page_context_menu->addAction(search_selected_text_action);
  421. m_page_context_menu->addSeparator();
  422. m_page_context_menu->addAction(take_visible_screenshot_action);
  423. m_page_context_menu->addAction(take_full_screenshot_action);
  424. m_page_context_menu->addSeparator();
  425. m_page_context_menu->addAction(&m_window->view_source_action());
  426. m_page_context_menu->addAction(&m_window->inspect_dom_node_action());
  427. view().on_context_menu_request = [this, search_selected_text_action](Gfx::IntPoint content_position) {
  428. auto selected_text = Settings::the()->enable_search()
  429. ? view().selected_text_with_whitespace_collapsed()
  430. : OptionalNone {};
  431. TemporaryChange change_url { m_page_context_menu_search_text, std::move(selected_text) };
  432. if (m_page_context_menu_search_text.has_value()) {
  433. auto action_text = WebView::format_search_query_for_display(Settings::the()->search_engine().query_url, *m_page_context_menu_search_text);
  434. search_selected_text_action->setText(qstring_from_ak_string(action_text));
  435. search_selected_text_action->setVisible(true);
  436. } else {
  437. search_selected_text_action->setVisible(false);
  438. }
  439. m_page_context_menu->exec(view().map_point_to_global_position(content_position));
  440. };
  441. auto* open_link_in_new_tab_action = new QAction("Open Link in New &Tab", this);
  442. open_link_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  443. QObject::connect(open_link_in_new_tab_action, &QAction::triggered, this, [this]() {
  444. open_link_in_new_tab(m_link_context_menu_url);
  445. });
  446. m_link_context_menu_copy_url_action = new QAction("Copy &Link Address", this);
  447. m_link_context_menu_copy_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  448. QObject::connect(m_link_context_menu_copy_url_action, &QAction::triggered, this, [this]() {
  449. copy_link_url(m_link_context_menu_url);
  450. });
  451. m_link_context_menu = new QMenu("Link context menu", this);
  452. m_link_context_menu->addAction(open_link_in_new_tab_action);
  453. m_link_context_menu->addAction(m_link_context_menu_copy_url_action);
  454. m_link_context_menu->addSeparator();
  455. m_link_context_menu->addAction(&m_window->inspect_dom_node_action());
  456. view().on_link_context_menu_request = [this](auto const& url, Gfx::IntPoint content_position) {
  457. m_link_context_menu_url = url;
  458. switch (WebView::url_type(url)) {
  459. case WebView::URLType::Email:
  460. m_link_context_menu_copy_url_action->setText("Copy &Email Address");
  461. break;
  462. case WebView::URLType::Telephone:
  463. m_link_context_menu_copy_url_action->setText("Copy &Phone Number");
  464. break;
  465. case WebView::URLType::Other:
  466. m_link_context_menu_copy_url_action->setText("Copy &Link Address");
  467. break;
  468. }
  469. m_link_context_menu->exec(view().map_point_to_global_position(content_position));
  470. };
  471. auto* open_image_action = new QAction("&Open Image", this);
  472. open_image_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-image.png"sv));
  473. QObject::connect(open_image_action, &QAction::triggered, this, [this]() {
  474. open_link(m_image_context_menu_url);
  475. });
  476. auto* open_image_in_new_tab_action = new QAction("&Open Image in New &Tab", this);
  477. open_image_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  478. QObject::connect(open_image_in_new_tab_action, &QAction::triggered, this, [this]() {
  479. open_link_in_new_tab(m_image_context_menu_url);
  480. });
  481. auto* copy_image_action = new QAction("&Copy Image", this);
  482. copy_image_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  483. QObject::connect(copy_image_action, &QAction::triggered, this, [this]() {
  484. auto* bitmap = m_image_context_menu_bitmap.bitmap();
  485. if (bitmap == nullptr)
  486. return;
  487. auto data = Gfx::BMPWriter::encode(*bitmap);
  488. if (data.is_error())
  489. return;
  490. auto image = QImage::fromData(data.value().data(), data.value().size(), "BMP");
  491. if (image.isNull())
  492. return;
  493. auto* clipboard = QGuiApplication::clipboard();
  494. clipboard->setImage(image);
  495. });
  496. auto* copy_image_url_action = new QAction("Copy Image &URL", this);
  497. copy_image_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  498. QObject::connect(copy_image_url_action, &QAction::triggered, this, [this]() {
  499. copy_link_url(m_image_context_menu_url);
  500. });
  501. m_image_context_menu = new QMenu("Image context menu", this);
  502. m_image_context_menu->addAction(open_image_action);
  503. m_image_context_menu->addAction(open_image_in_new_tab_action);
  504. m_image_context_menu->addSeparator();
  505. m_image_context_menu->addAction(copy_image_action);
  506. m_image_context_menu->addAction(copy_image_url_action);
  507. m_image_context_menu->addSeparator();
  508. m_image_context_menu->addAction(&m_window->inspect_dom_node_action());
  509. view().on_image_context_menu_request = [this](auto& image_url, Gfx::IntPoint content_position, Gfx::ShareableBitmap const& shareable_bitmap) {
  510. m_image_context_menu_url = image_url;
  511. m_image_context_menu_bitmap = shareable_bitmap;
  512. m_image_context_menu->exec(view().map_point_to_global_position(content_position));
  513. };
  514. m_media_context_menu_play_icon = load_icon_from_uri("resource://icons/16x16/play.png"sv);
  515. m_media_context_menu_pause_icon = load_icon_from_uri("resource://icons/16x16/pause.png"sv);
  516. m_media_context_menu_mute_icon = load_icon_from_uri("resource://icons/16x16/audio-volume-muted.png"sv);
  517. m_media_context_menu_unmute_icon = load_icon_from_uri("resource://icons/16x16/audio-volume-high.png"sv);
  518. m_media_context_menu_play_pause_action = new QAction("&Play", this);
  519. m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_play_icon);
  520. QObject::connect(m_media_context_menu_play_pause_action, &QAction::triggered, this, [this]() {
  521. view().toggle_media_play_state();
  522. });
  523. m_media_context_menu_mute_unmute_action = new QAction("&Mute", this);
  524. m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_mute_icon);
  525. QObject::connect(m_media_context_menu_mute_unmute_action, &QAction::triggered, this, [this]() {
  526. view().toggle_media_mute_state();
  527. });
  528. m_media_context_menu_controls_action = new QAction("Show &Controls", this);
  529. m_media_context_menu_controls_action->setCheckable(true);
  530. QObject::connect(m_media_context_menu_controls_action, &QAction::triggered, this, [this]() {
  531. view().toggle_media_controls_state();
  532. });
  533. m_media_context_menu_loop_action = new QAction("&Loop", this);
  534. m_media_context_menu_loop_action->setCheckable(true);
  535. QObject::connect(m_media_context_menu_loop_action, &QAction::triggered, this, [this]() {
  536. view().toggle_media_loop_state();
  537. });
  538. auto* open_audio_action = new QAction("&Open Audio", this);
  539. open_audio_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-sound.png"sv));
  540. QObject::connect(open_audio_action, &QAction::triggered, this, [this]() {
  541. open_link(m_media_context_menu_url);
  542. });
  543. auto* open_audio_in_new_tab_action = new QAction("Open Audio in New &Tab", this);
  544. open_audio_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  545. QObject::connect(open_audio_in_new_tab_action, &QAction::triggered, this, [this]() {
  546. open_link_in_new_tab(m_media_context_menu_url);
  547. });
  548. auto* copy_audio_url_action = new QAction("Copy Audio &URL", this);
  549. copy_audio_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  550. QObject::connect(copy_audio_url_action, &QAction::triggered, this, [this]() {
  551. copy_link_url(m_media_context_menu_url);
  552. });
  553. m_audio_context_menu = new QMenu("Audio context menu", this);
  554. m_audio_context_menu->addAction(m_media_context_menu_play_pause_action);
  555. m_audio_context_menu->addAction(m_media_context_menu_mute_unmute_action);
  556. m_audio_context_menu->addAction(m_media_context_menu_controls_action);
  557. m_audio_context_menu->addAction(m_media_context_menu_loop_action);
  558. m_audio_context_menu->addSeparator();
  559. m_audio_context_menu->addAction(open_audio_action);
  560. m_audio_context_menu->addAction(open_audio_in_new_tab_action);
  561. m_audio_context_menu->addSeparator();
  562. m_audio_context_menu->addAction(copy_audio_url_action);
  563. m_audio_context_menu->addSeparator();
  564. m_audio_context_menu->addAction(&m_window->inspect_dom_node_action());
  565. auto* open_video_action = new QAction("&Open Video", this);
  566. open_video_action->setIcon(load_icon_from_uri("resource://icons/16x16/filetype-video.png"sv));
  567. QObject::connect(open_video_action, &QAction::triggered, this, [this]() {
  568. open_link(m_media_context_menu_url);
  569. });
  570. auto* open_video_in_new_tab_action = new QAction("Open Video in New &Tab", this);
  571. open_video_in_new_tab_action->setIcon(load_icon_from_uri("resource://icons/16x16/new-tab.png"sv));
  572. QObject::connect(open_video_in_new_tab_action, &QAction::triggered, this, [this]() {
  573. open_link_in_new_tab(m_media_context_menu_url);
  574. });
  575. auto* copy_video_url_action = new QAction("Copy Video &URL", this);
  576. copy_video_url_action->setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
  577. QObject::connect(copy_video_url_action, &QAction::triggered, this, [this]() {
  578. copy_link_url(m_media_context_menu_url);
  579. });
  580. m_video_context_menu = new QMenu("Video context menu", this);
  581. m_video_context_menu->addAction(m_media_context_menu_play_pause_action);
  582. m_video_context_menu->addAction(m_media_context_menu_mute_unmute_action);
  583. m_video_context_menu->addAction(m_media_context_menu_controls_action);
  584. m_video_context_menu->addAction(m_media_context_menu_loop_action);
  585. m_video_context_menu->addSeparator();
  586. m_video_context_menu->addAction(open_video_action);
  587. m_video_context_menu->addAction(open_video_in_new_tab_action);
  588. m_video_context_menu->addSeparator();
  589. m_video_context_menu->addAction(copy_video_url_action);
  590. m_video_context_menu->addSeparator();
  591. m_video_context_menu->addAction(&m_window->inspect_dom_node_action());
  592. view().on_media_context_menu_request = [this](Gfx::IntPoint content_position, Web::Page::MediaContextMenu const& menu) {
  593. m_media_context_menu_url = menu.media_url;
  594. if (menu.is_playing) {
  595. m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_pause_icon);
  596. m_media_context_menu_play_pause_action->setText("&Pause");
  597. } else {
  598. m_media_context_menu_play_pause_action->setIcon(m_media_context_menu_play_icon);
  599. m_media_context_menu_play_pause_action->setText("&Play");
  600. }
  601. if (menu.is_muted) {
  602. m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_unmute_icon);
  603. m_media_context_menu_mute_unmute_action->setText("Un&mute");
  604. } else {
  605. m_media_context_menu_mute_unmute_action->setIcon(m_media_context_menu_mute_icon);
  606. m_media_context_menu_mute_unmute_action->setText("&Mute");
  607. }
  608. m_media_context_menu_controls_action->setChecked(menu.has_user_agent_controls);
  609. m_media_context_menu_loop_action->setChecked(menu.is_looping);
  610. auto screen_position = view().map_point_to_global_position(content_position);
  611. if (menu.is_video)
  612. m_video_context_menu->exec(screen_position);
  613. else
  614. m_audio_context_menu->exec(screen_position);
  615. };
  616. }
  617. Tab::~Tab()
  618. {
  619. close_sub_widgets();
  620. // Delete the InspectorWidget explicitly to ensure it is deleted before the WebContentView. Otherwise, Qt
  621. // can destroy these objects in any order, which may cause use-after-free in InspectorWidget's destructor.
  622. delete m_inspector_widget;
  623. }
  624. void Tab::update_reset_zoom_button()
  625. {
  626. auto zoom_level = view().zoom_level();
  627. if (zoom_level != 1.0f) {
  628. auto zoom_level_text = MUST(String::formatted("{}%", round_to<int>(zoom_level * 100)));
  629. m_reset_zoom_button->setText(qstring_from_ak_string(zoom_level_text));
  630. m_reset_zoom_button_action->setVisible(true);
  631. } else {
  632. m_reset_zoom_button_action->setVisible(false);
  633. }
  634. }
  635. void Tab::focus_location_editor()
  636. {
  637. m_location_edit->setFocus();
  638. m_location_edit->selectAll();
  639. }
  640. void Tab::navigate(URL::URL const& url)
  641. {
  642. view().load(url);
  643. }
  644. void Tab::load_html(StringView html)
  645. {
  646. view().load_html(html);
  647. }
  648. void Tab::back()
  649. {
  650. view().traverse_the_history_by_delta(-1);
  651. }
  652. void Tab::forward()
  653. {
  654. view().traverse_the_history_by_delta(1);
  655. }
  656. void Tab::reload()
  657. {
  658. view().reload();
  659. }
  660. void Tab::open_link(URL::URL const& url)
  661. {
  662. view().on_link_click(url, "", 0);
  663. }
  664. void Tab::open_link_in_new_tab(URL::URL const& url)
  665. {
  666. view().on_link_click(url, "_blank", Web::UIEvents::Mod_Ctrl);
  667. }
  668. void Tab::copy_link_url(URL::URL const& url)
  669. {
  670. auto* clipboard = QGuiApplication::clipboard();
  671. clipboard->setText(qstring_from_ak_string(WebView::url_text_to_copy(url)));
  672. }
  673. void Tab::location_edit_return_pressed()
  674. {
  675. if (m_location_edit->text().isEmpty())
  676. return;
  677. navigate(m_location_edit->url());
  678. }
  679. void Tab::open_file()
  680. {
  681. auto filename = QFileDialog::getOpenFileUrl(this, "Open file", QDir::homePath(), "All Files (*.*)");
  682. if (filename.isValid()) {
  683. navigate(ak_url_from_qurl(filename));
  684. }
  685. }
  686. int Tab::tab_index()
  687. {
  688. return m_window->tab_index(this);
  689. }
  690. void Tab::debug_request(ByteString const& request, ByteString const& argument)
  691. {
  692. m_view->debug_request(request, argument);
  693. }
  694. void Tab::resizeEvent(QResizeEvent* event)
  695. {
  696. QWidget::resizeEvent(event);
  697. if (m_hover_label->isVisible())
  698. update_hover_label();
  699. }
  700. void Tab::update_hover_label()
  701. {
  702. m_hover_label->resize(QFontMetrics(m_hover_label->font()).boundingRect(m_hover_label->text()).adjusted(-4, -2, 4, 2).size());
  703. auto hover_label_height = height() - m_hover_label->height() - 8;
  704. if (m_find_in_page->isVisible())
  705. hover_label_height -= m_find_in_page->height() - 4;
  706. m_hover_label->move(6, hover_label_height);
  707. m_hover_label->raise();
  708. }
  709. void Tab::update_navigation_buttons_state()
  710. {
  711. if (m_window->current_tab() != this)
  712. return;
  713. m_window->go_back_action().setEnabled(m_can_navigate_back);
  714. m_window->go_forward_action().setEnabled(m_can_navigate_forward);
  715. }
  716. bool Tab::event(QEvent* event)
  717. {
  718. if (event->type() == QEvent::PaletteChange) {
  719. recreate_toolbar_icons();
  720. return QWidget::event(event);
  721. }
  722. return QWidget::event(event);
  723. }
  724. void Tab::recreate_toolbar_icons()
  725. {
  726. m_window->go_back_action().setIcon(create_tvg_icon_with_theme_colors("back", palette()));
  727. m_window->go_forward_action().setIcon(create_tvg_icon_with_theme_colors("forward", palette()));
  728. m_window->reload_action().setIcon(create_tvg_icon_with_theme_colors("reload", palette()));
  729. m_window->new_tab_action().setIcon(create_tvg_icon_with_theme_colors("new_tab", palette()));
  730. m_hamburger_button->setIcon(create_tvg_icon_with_theme_colors("hamburger", palette()));
  731. }
  732. void Tab::show_inspector_window(InspectorTarget inspector_target)
  733. {
  734. if (!m_inspector_widget)
  735. m_inspector_widget = new InspectorWidget(this, view());
  736. else
  737. m_inspector_widget->inspect();
  738. m_inspector_widget->show();
  739. m_inspector_widget->activateWindow();
  740. m_inspector_widget->raise();
  741. if (inspector_target == InspectorTarget::HoveredElement)
  742. m_inspector_widget->select_hovered_node();
  743. else
  744. m_inspector_widget->select_default_node();
  745. }
  746. void Tab::show_find_in_page()
  747. {
  748. m_find_in_page->setVisible(true);
  749. m_find_in_page->setFocus();
  750. }
  751. void Tab::find_previous()
  752. {
  753. m_find_in_page->find_previous();
  754. }
  755. void Tab::find_next()
  756. {
  757. m_find_in_page->find_next();
  758. }
  759. void Tab::close_sub_widgets()
  760. {
  761. auto close_widget_window = [](auto* widget) {
  762. if (widget)
  763. widget->close();
  764. };
  765. close_widget_window(m_inspector_widget);
  766. }
  767. void Tab::set_block_popups(bool enabled)
  768. {
  769. debug_request("block-pop-ups", enabled ? "on" : "off");
  770. }
  771. void Tab::set_line_box_borders(bool enabled)
  772. {
  773. debug_request("set-line-box-borders", enabled ? "on" : "off");
  774. }
  775. void Tab::set_same_origin_policy(bool enabled)
  776. {
  777. debug_request("same-origin-policy", enabled ? "on" : "off");
  778. }
  779. void Tab::set_scripting(bool enabled)
  780. {
  781. debug_request("scripting", enabled ? "on" : "off");
  782. }
  783. void Tab::set_user_agent_string(ByteString const& user_agent)
  784. {
  785. debug_request("spoof-user-agent", user_agent);
  786. // Clear the cache to ensure requests are re-done with the new user agent.
  787. debug_request("clear-cache");
  788. }
  789. void Tab::set_navigator_compatibility_mode(ByteString const& compatibility_mode)
  790. {
  791. debug_request("navigator-compatibility-mode", compatibility_mode);
  792. }
  793. void Tab::set_preferred_languages(Vector<String> const& preferred_languages)
  794. {
  795. m_view->set_preferred_languages(preferred_languages);
  796. }
  797. void Tab::set_enable_do_not_track(bool enable)
  798. {
  799. m_view->set_enable_do_not_track(enable);
  800. }
  801. void Tab::set_enable_autoplay(bool enable)
  802. {
  803. m_view->set_enable_autoplay(enable);
  804. }
  805. }