Tab.cpp 36 KB

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