PropertiesWindow.cpp 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2022-2023, the SerenityOS developers.
  4. * Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
  5. *
  6. * SPDX-License-Identifier: BSD-2-Clause
  7. */
  8. #include "PropertiesWindow.h"
  9. #include <AK/GenericShorthands.h>
  10. #include <AK/LexicalPath.h>
  11. #include <AK/NumberFormat.h>
  12. #include <Applications/FileManager/DirectoryView.h>
  13. #include <Applications/FileManager/PropertiesWindowArchiveTabGML.h>
  14. #include <Applications/FileManager/PropertiesWindowAudioTabGML.h>
  15. #include <Applications/FileManager/PropertiesWindowFontTabGML.h>
  16. #include <Applications/FileManager/PropertiesWindowGeneralTabGML.h>
  17. #include <Applications/FileManager/PropertiesWindowImageTabGML.h>
  18. #include <Applications/FileManager/PropertiesWindowPDFTabGML.h>
  19. #include <LibArchive/Zip.h>
  20. #include <LibAudio/Loader.h>
  21. #include <LibCore/Directory.h>
  22. #include <LibCore/System.h>
  23. #include <LibDesktop/Launcher.h>
  24. #include <LibFileSystem/FileSystem.h>
  25. #include <LibGUI/BoxLayout.h>
  26. #include <LibGUI/CheckBox.h>
  27. #include <LibGUI/FileIconProvider.h>
  28. #include <LibGUI/FilePicker.h>
  29. #include <LibGUI/IconView.h>
  30. #include <LibGUI/LinkLabel.h>
  31. #include <LibGUI/MessageBox.h>
  32. #include <LibGUI/SeparatorWidget.h>
  33. #include <LibGUI/TabWidget.h>
  34. #include <LibGfx/Font/BitmapFont.h>
  35. #include <LibGfx/Font/FontDatabase.h>
  36. #include <LibGfx/Font/FontStyleMapping.h>
  37. #include <LibGfx/Font/OpenType/Font.h>
  38. #include <LibGfx/Font/Typeface.h>
  39. #include <LibGfx/Font/WOFF/Font.h>
  40. #include <LibGfx/ICC/Profile.h>
  41. #include <LibGfx/ICC/Tags.h>
  42. #include <LibPDF/Document.h>
  43. #include <grp.h>
  44. #include <pwd.h>
  45. #include <stdio.h>
  46. #include <string.h>
  47. #include <unistd.h>
  48. ErrorOr<NonnullRefPtr<PropertiesWindow>> PropertiesWindow::try_create(DeprecatedString const& path, bool disable_rename, Window* parent)
  49. {
  50. auto window = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) PropertiesWindow(path, parent)));
  51. window->set_icon(TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"sv)));
  52. TRY(window->create_widgets(disable_rename));
  53. return window;
  54. }
  55. PropertiesWindow::PropertiesWindow(DeprecatedString const& path, Window* parent_window)
  56. : Window(parent_window)
  57. {
  58. auto lexical_path = LexicalPath(path);
  59. m_name = lexical_path.basename();
  60. m_path = lexical_path.string();
  61. m_parent_path = lexical_path.dirname();
  62. set_rect({ 0, 0, 360, 420 });
  63. set_resizable(false);
  64. }
  65. ErrorOr<void> PropertiesWindow::create_widgets(bool disable_rename)
  66. {
  67. auto main_widget = set_main_widget<GUI::Widget>();
  68. main_widget->set_layout<GUI::VerticalBoxLayout>(4, 6);
  69. main_widget->set_fill_with_background_color(true);
  70. auto& tab_widget = main_widget->add<GUI::TabWidget>();
  71. TRY(create_general_tab(tab_widget, disable_rename));
  72. TRY(create_file_type_specific_tabs(tab_widget));
  73. auto& button_widget = main_widget->add<GUI::Widget>();
  74. button_widget.set_layout<GUI::HorizontalBoxLayout>(GUI::Margins {}, 5);
  75. button_widget.set_fixed_height(22);
  76. button_widget.add_spacer();
  77. auto& ok_button = make_button("OK"_string, button_widget);
  78. ok_button.on_click = [this](auto) {
  79. if (apply_changes())
  80. close();
  81. };
  82. auto& cancel_button = make_button("Cancel"_string, button_widget);
  83. cancel_button.on_click = [this](auto) {
  84. close();
  85. };
  86. m_apply_button = make_button("Apply"_string, button_widget);
  87. m_apply_button->on_click = [this](auto) { apply_changes(); };
  88. m_apply_button->set_enabled(false);
  89. if (S_ISDIR(m_old_mode)) {
  90. m_directory_statistics_calculator = make_ref_counted<DirectoryStatisticsCalculator>(m_path);
  91. m_directory_statistics_calculator->on_update = [this, origin_event_loop = &Core::EventLoop::current()](off_t total_size_in_bytes, size_t file_count, size_t directory_count) {
  92. origin_event_loop->deferred_invoke([=, weak_this = make_weak_ptr<PropertiesWindow>()] {
  93. if (auto strong_this = weak_this.strong_ref())
  94. strong_this->m_size_label->set_text(String::formatted("{}\n{} files, {} subdirectories", human_readable_size_long(total_size_in_bytes, UseThousandsSeparator::Yes), file_count, directory_count).release_value_but_fixme_should_propagate_errors());
  95. });
  96. };
  97. m_directory_statistics_calculator->start();
  98. }
  99. m_on_escape = GUI::Action::create("Close properties", { Key_Escape }, [this](GUI::Action&) {
  100. if (!m_apply_button->is_enabled())
  101. close();
  102. });
  103. update();
  104. return {};
  105. }
  106. ErrorOr<void> PropertiesWindow::create_general_tab(GUI::TabWidget& tab_widget, bool disable_rename)
  107. {
  108. auto& general_tab = tab_widget.add_tab<GUI::Widget>("General"_string);
  109. TRY(general_tab.load_from_gml(properties_window_general_tab_gml));
  110. m_icon = general_tab.find_descendant_of_type_named<GUI::ImageWidget>("icon");
  111. m_name_box = general_tab.find_descendant_of_type_named<GUI::TextBox>("name");
  112. m_name_box->set_text(m_name);
  113. m_name_box->set_mode(disable_rename ? GUI::TextBox::Mode::DisplayOnly : GUI::TextBox::Mode::Editable);
  114. m_name_box->on_change = [&]() {
  115. m_name_dirty = m_name != m_name_box->text();
  116. m_apply_button->set_enabled(m_name_dirty || m_permissions_dirty);
  117. };
  118. auto* location = general_tab.find_descendant_of_type_named<GUI::LinkLabel>("location");
  119. location->set_text(TRY(String::from_deprecated_string(m_path)));
  120. location->on_click = [this] {
  121. Desktop::Launcher::open(URL::create_with_file_scheme(m_parent_path, m_name));
  122. };
  123. auto st = TRY(Core::System::lstat(m_path));
  124. DeprecatedString owner_name;
  125. DeprecatedString group_name;
  126. if (auto* pw = getpwuid(st.st_uid)) {
  127. owner_name = pw->pw_name;
  128. } else {
  129. owner_name = "n/a";
  130. }
  131. if (auto* gr = getgrgid(st.st_gid)) {
  132. group_name = gr->gr_name;
  133. } else {
  134. group_name = "n/a";
  135. }
  136. m_mode = st.st_mode;
  137. m_old_mode = st.st_mode;
  138. auto* type = general_tab.find_descendant_of_type_named<GUI::Label>("type");
  139. type->set_text(TRY(String::from_utf8(get_description(m_mode))));
  140. if (S_ISLNK(m_mode)) {
  141. auto link_destination_or_error = FileSystem::read_link(m_path);
  142. if (link_destination_or_error.is_error()) {
  143. perror("readlink");
  144. } else {
  145. auto link_destination = link_destination_or_error.release_value();
  146. auto* link_location = general_tab.find_descendant_of_type_named<GUI::LinkLabel>("link_location");
  147. link_location->set_text(link_destination);
  148. link_location->on_click = [link_destination] {
  149. auto link_directory = LexicalPath(link_destination.to_deprecated_string());
  150. Desktop::Launcher::open(URL::create_with_file_scheme(link_directory.dirname(), link_directory.basename()));
  151. };
  152. }
  153. } else {
  154. auto* link_location_widget = general_tab.find_descendant_of_type_named<GUI::Widget>("link_location_widget");
  155. general_tab.remove_child(*link_location_widget);
  156. }
  157. m_size_label = general_tab.find_descendant_of_type_named<GUI::Label>("size");
  158. m_size_label->set_text(S_ISDIR(st.st_mode)
  159. ? "Calculating..."_string
  160. : TRY(String::from_deprecated_string(human_readable_size_long(st.st_size, UseThousandsSeparator::Yes))));
  161. auto* owner = general_tab.find_descendant_of_type_named<GUI::Label>("owner");
  162. owner->set_text(String::formatted("{} ({})", owner_name, st.st_uid).release_value_but_fixme_should_propagate_errors());
  163. auto* group = general_tab.find_descendant_of_type_named<GUI::Label>("group");
  164. group->set_text(String::formatted("{} ({})", group_name, st.st_gid).release_value_but_fixme_should_propagate_errors());
  165. auto* created_at = general_tab.find_descendant_of_type_named<GUI::Label>("created_at");
  166. created_at->set_text(String::from_deprecated_string(GUI::FileSystemModel::timestamp_string(st.st_ctime)).release_value_but_fixme_should_propagate_errors());
  167. auto* last_modified = general_tab.find_descendant_of_type_named<GUI::Label>("last_modified");
  168. last_modified->set_text(String::from_deprecated_string(GUI::FileSystemModel::timestamp_string(st.st_mtime)).release_value_but_fixme_should_propagate_errors());
  169. auto* owner_read = general_tab.find_descendant_of_type_named<GUI::CheckBox>("owner_read");
  170. auto* owner_write = general_tab.find_descendant_of_type_named<GUI::CheckBox>("owner_write");
  171. auto* owner_execute = general_tab.find_descendant_of_type_named<GUI::CheckBox>("owner_execute");
  172. TRY(setup_permission_checkboxes(*owner_read, *owner_write, *owner_execute, { S_IRUSR, S_IWUSR, S_IXUSR }, m_mode));
  173. auto* group_read = general_tab.find_descendant_of_type_named<GUI::CheckBox>("group_read");
  174. auto* group_write = general_tab.find_descendant_of_type_named<GUI::CheckBox>("group_write");
  175. auto* group_execute = general_tab.find_descendant_of_type_named<GUI::CheckBox>("group_execute");
  176. TRY(setup_permission_checkboxes(*group_read, *group_write, *group_execute, { S_IRGRP, S_IWGRP, S_IXGRP }, m_mode));
  177. auto* others_read = general_tab.find_descendant_of_type_named<GUI::CheckBox>("others_read");
  178. auto* others_write = general_tab.find_descendant_of_type_named<GUI::CheckBox>("others_write");
  179. auto* others_execute = general_tab.find_descendant_of_type_named<GUI::CheckBox>("others_execute");
  180. TRY(setup_permission_checkboxes(*others_read, *others_write, *others_execute, { S_IROTH, S_IWOTH, S_IXOTH }, m_mode));
  181. return {};
  182. }
  183. ErrorOr<void> PropertiesWindow::create_file_type_specific_tabs(GUI::TabWidget& tab_widget)
  184. {
  185. auto mapped_file_or_error = Core::MappedFile::map(m_path);
  186. if (mapped_file_or_error.is_error()) {
  187. warnln("{}: {}", m_path, mapped_file_or_error.release_error());
  188. return {};
  189. }
  190. auto mapped_file = mapped_file_or_error.release_value();
  191. auto file_name_guess = Core::guess_mime_type_based_on_filename(m_path);
  192. auto mime_type = Core::guess_mime_type_based_on_sniffed_bytes(mapped_file->bytes()).value_or(file_name_guess);
  193. // FIXME: Support other archive types
  194. if (mime_type == "application/zip"sv)
  195. return create_archive_tab(tab_widget, move(mapped_file));
  196. if (mime_type.starts_with("audio/"sv))
  197. return create_audio_tab(tab_widget, move(mapped_file));
  198. if (mime_type.starts_with("font/"sv) || m_path.ends_with(".font"sv))
  199. return create_font_tab(tab_widget, move(mapped_file), mime_type);
  200. if (mime_type.starts_with("image/"sv))
  201. return create_image_tab(tab_widget, move(mapped_file), mime_type);
  202. if (mime_type == "application/pdf"sv)
  203. return create_pdf_tab(tab_widget, move(mapped_file));
  204. return {};
  205. }
  206. ErrorOr<void> PropertiesWindow::create_archive_tab(GUI::TabWidget& tab_widget, NonnullOwnPtr<Core::MappedFile> mapped_file)
  207. {
  208. auto maybe_zip = Archive::Zip::try_create(mapped_file->bytes());
  209. if (!maybe_zip.has_value()) {
  210. warnln("Failed to read zip file '{}' ", m_path);
  211. return {};
  212. }
  213. auto zip = maybe_zip.release_value();
  214. auto& tab = tab_widget.add_tab<GUI::Widget>("Archive"_string);
  215. TRY(tab.load_from_gml(properties_window_archive_tab_gml));
  216. auto statistics = TRY(zip.calculate_statistics());
  217. tab.find_descendant_of_type_named<GUI::Label>("archive_file_count")->set_text(TRY(String::number(statistics.file_count())));
  218. tab.find_descendant_of_type_named<GUI::Label>("archive_format")->set_text("ZIP"_string);
  219. tab.find_descendant_of_type_named<GUI::Label>("archive_directory_count")->set_text(TRY(String::number(statistics.directory_count())));
  220. tab.find_descendant_of_type_named<GUI::Label>("archive_uncompressed_size")->set_text(TRY(String::from_deprecated_string(AK::human_readable_size(statistics.total_uncompressed_bytes()))));
  221. return {};
  222. }
  223. ErrorOr<void> PropertiesWindow::create_audio_tab(GUI::TabWidget& tab_widget, NonnullOwnPtr<Core::MappedFile> mapped_file)
  224. {
  225. auto loader_or_error = Audio::Loader::create(mapped_file->bytes());
  226. if (loader_or_error.is_error()) {
  227. warnln("Failed to open '{}': {}", m_path, loader_or_error.release_error());
  228. return {};
  229. }
  230. auto loader = loader_or_error.release_value();
  231. auto& tab = tab_widget.add_tab<GUI::Widget>("Audio"_string);
  232. TRY(tab.load_from_gml(properties_window_audio_tab_gml));
  233. tab.find_descendant_of_type_named<GUI::Label>("audio_type")->set_text(TRY(String::from_deprecated_string(loader->format_name())));
  234. auto duration_seconds = loader->total_samples() / loader->sample_rate();
  235. tab.find_descendant_of_type_named<GUI::Label>("audio_duration")->set_text(TRY(String::from_deprecated_string(human_readable_digital_time(duration_seconds))));
  236. tab.find_descendant_of_type_named<GUI::Label>("audio_sample_rate")->set_text(TRY(String::formatted("{} Hz", loader->sample_rate())));
  237. tab.find_descendant_of_type_named<GUI::Label>("audio_format")->set_text(TRY(String::formatted("{}-bit", loader->bits_per_sample())));
  238. auto channel_count = loader->num_channels();
  239. String channels_string;
  240. if (channel_count == 1 || channel_count == 2) {
  241. channels_string = TRY(String::formatted("{} ({})", channel_count, channel_count == 1 ? "Mono"sv : "Stereo"sv));
  242. } else {
  243. channels_string = TRY(String::number(channel_count));
  244. }
  245. tab.find_descendant_of_type_named<GUI::Label>("audio_channels")->set_text(channels_string);
  246. tab.find_descendant_of_type_named<GUI::Label>("audio_title")->set_text(loader->metadata().title.value_or({}));
  247. tab.find_descendant_of_type_named<GUI::Label>("audio_artists")->set_text(TRY(loader->metadata().all_artists()).value_or({}));
  248. tab.find_descendant_of_type_named<GUI::Label>("audio_album")->set_text(loader->metadata().album.value_or({}));
  249. tab.find_descendant_of_type_named<GUI::Label>("audio_track_number")
  250. ->set_text(TRY(loader->metadata().track_number.map([](auto number) { return String::number(number); })).value_or({}));
  251. tab.find_descendant_of_type_named<GUI::Label>("audio_genre")->set_text(loader->metadata().genre.value_or({}));
  252. tab.find_descendant_of_type_named<GUI::Label>("audio_comment")->set_text(loader->metadata().comment.value_or({}));
  253. return {};
  254. }
  255. struct FontInfo {
  256. enum class Format {
  257. BitmapFont,
  258. OpenType,
  259. TrueType,
  260. WOFF,
  261. WOFF2,
  262. };
  263. Format format;
  264. NonnullRefPtr<Gfx::Typeface> typeface;
  265. };
  266. static ErrorOr<FontInfo> load_font(StringView path, StringView mime_type, NonnullOwnPtr<Core::MappedFile> mapped_file)
  267. {
  268. if (path.ends_with(".font"sv)) {
  269. auto font = TRY(Gfx::BitmapFont::try_load_from_mapped_file(move(mapped_file)));
  270. auto typeface = TRY(try_make_ref_counted<Gfx::Typeface>(font->family(), font->variant()));
  271. typeface->add_bitmap_font(move(font));
  272. return FontInfo { FontInfo::Format::BitmapFont, move(typeface) };
  273. }
  274. if (mime_type == "font/otf" || mime_type == "font/ttf") {
  275. auto font = TRY(OpenType::Font::try_load_from_externally_owned_memory(mapped_file->bytes()));
  276. auto typeface = TRY(try_make_ref_counted<Gfx::Typeface>(font->family(), font->variant()));
  277. typeface->set_vector_font(move(font));
  278. return FontInfo {
  279. mime_type == "font/otf" ? FontInfo::Format::OpenType : FontInfo::Format::TrueType,
  280. move(typeface)
  281. };
  282. }
  283. if (mime_type == "font/woff" || mime_type == "font/woff2") {
  284. auto font = TRY(WOFF::Font::try_load_from_externally_owned_memory(mapped_file->bytes()));
  285. auto typeface = TRY(try_make_ref_counted<Gfx::Typeface>(font->family(), font->variant()));
  286. typeface->set_vector_font(move(font));
  287. return FontInfo {
  288. mime_type == "font/woff" ? FontInfo::Format::WOFF : FontInfo::Format::WOFF2,
  289. move(typeface)
  290. };
  291. }
  292. return Error::from_string_view("Unrecognized font format."sv);
  293. }
  294. ErrorOr<void> PropertiesWindow::create_font_tab(GUI::TabWidget& tab_widget, NonnullOwnPtr<Core::MappedFile> mapped_file, StringView mime_type)
  295. {
  296. auto font_info_or_error = load_font(m_path, mime_type, move(mapped_file));
  297. if (font_info_or_error.is_error()) {
  298. warnln("Failed to open '{}': {}", m_path, font_info_or_error.release_error());
  299. return {};
  300. }
  301. auto font_info = font_info_or_error.release_value();
  302. auto& typeface = font_info.typeface;
  303. auto& tab = tab_widget.add_tab<GUI::Widget>("Font"_string);
  304. TRY(tab.load_from_gml(properties_window_font_tab_gml));
  305. String format_name;
  306. switch (font_info.format) {
  307. case FontInfo::Format::BitmapFont:
  308. format_name = "Bitmap Font"_string;
  309. break;
  310. case FontInfo::Format::OpenType:
  311. format_name = "OpenType"_string;
  312. break;
  313. case FontInfo::Format::TrueType:
  314. format_name = "TrueType"_string;
  315. break;
  316. case FontInfo::Format::WOFF:
  317. format_name = "WOFF"_string;
  318. break;
  319. case FontInfo::Format::WOFF2:
  320. format_name = "WOFF2"_string;
  321. break;
  322. }
  323. tab.find_descendant_of_type_named<GUI::Label>("font_family")->set_text(typeface->family().to_string());
  324. tab.find_descendant_of_type_named<GUI::Label>("font_fixed_width")->set_text(typeface->is_fixed_width() ? "Yes"_string : "No"_string);
  325. tab.find_descendant_of_type_named<GUI::Label>("font_format")->set_text(format_name);
  326. tab.find_descendant_of_type_named<GUI::Label>("font_width")->set_text(TRY(String::from_utf8(Gfx::width_to_name(static_cast<Gfx::FontWidth>(typeface->width())))));
  327. auto nearest_weight_class_name = [](unsigned weight) {
  328. if (weight > 925)
  329. return Gfx::weight_to_name(Gfx::FontWeight::ExtraBlack);
  330. unsigned weight_class = clamp(round_to<unsigned>(weight / 100.0) * 100, Gfx::FontWeight::Thin, Gfx::FontWeight::Black);
  331. return Gfx::weight_to_name(weight_class);
  332. };
  333. auto weight = typeface->weight();
  334. tab.find_descendant_of_type_named<GUI::Label>("font_weight")->set_text(TRY(String::formatted("{} ({})", weight, nearest_weight_class_name(weight))));
  335. tab.find_descendant_of_type_named<GUI::Label>("font_slope")->set_text(TRY(String::from_utf8(Gfx::slope_to_name(typeface->slope()))));
  336. return {};
  337. }
  338. ErrorOr<void> PropertiesWindow::create_image_tab(GUI::TabWidget& tab_widget, NonnullOwnPtr<Core::MappedFile> mapped_file, StringView mime_type)
  339. {
  340. auto image_decoder = Gfx::ImageDecoder::try_create_for_raw_bytes(mapped_file->bytes(), mime_type);
  341. if (!image_decoder)
  342. return {};
  343. auto& tab = tab_widget.add_tab<GUI::Widget>("Image"_string);
  344. TRY(tab.load_from_gml(properties_window_image_tab_gml));
  345. tab.find_descendant_of_type_named<GUI::Label>("image_type")->set_text(TRY(String::from_utf8(mime_type)));
  346. tab.find_descendant_of_type_named<GUI::Label>("image_size")->set_text(TRY(String::formatted("{} x {}", image_decoder->width(), image_decoder->height())));
  347. String animation_text;
  348. if (image_decoder->is_animated()) {
  349. auto loops = image_decoder->loop_count();
  350. auto frames = image_decoder->frame_count();
  351. StringBuilder builder;
  352. if (loops == 0) {
  353. TRY(builder.try_append("Loop indefinitely"sv));
  354. } else if (loops == 1) {
  355. TRY(builder.try_append("Once"sv));
  356. } else {
  357. TRY(builder.try_appendff("Loop {} times"sv, loops));
  358. }
  359. TRY(builder.try_appendff(" ({} frames)"sv, frames));
  360. animation_text = TRY(builder.to_string());
  361. } else {
  362. animation_text = "None"_string;
  363. }
  364. tab.find_descendant_of_type_named<GUI::Label>("image_animation")->set_text(move(animation_text));
  365. auto hide_icc_group = [&tab](String profile_text) {
  366. tab.find_descendant_of_type_named<GUI::Label>("image_has_icc_profile")->set_text(profile_text);
  367. tab.find_descendant_of_type_named<GUI::Widget>("image_icc_group")->set_visible(false);
  368. };
  369. if (auto embedded_icc_bytes = TRY(image_decoder->icc_data()); embedded_icc_bytes.has_value()) {
  370. auto icc_profile_or_error = Gfx::ICC::Profile::try_load_from_externally_owned_memory(embedded_icc_bytes.value());
  371. if (icc_profile_or_error.is_error()) {
  372. hide_icc_group("Present but invalid"_string);
  373. } else {
  374. auto icc_profile = icc_profile_or_error.release_value();
  375. tab.find_descendant_of_type_named<GUI::Label>("image_has_icc_profile")->set_text("See below"_string);
  376. tab.find_descendant_of_type_named<GUI::Label>("image_icc_profile")->set_text(icc_profile->tag_string_data(Gfx::ICC::profileDescriptionTag).value_or({}));
  377. tab.find_descendant_of_type_named<GUI::Label>("image_icc_copyright")->set_text(icc_profile->tag_string_data(Gfx::ICC::copyrightTag).value_or({}));
  378. tab.find_descendant_of_type_named<GUI::Label>("image_icc_color_space")->set_text(TRY(String::from_utf8(data_color_space_name(icc_profile->data_color_space()))));
  379. tab.find_descendant_of_type_named<GUI::Label>("image_icc_device_class")->set_text(TRY(String::from_utf8((device_class_name(icc_profile->device_class())))));
  380. }
  381. } else {
  382. hide_icc_group("None"_string);
  383. }
  384. return {};
  385. }
  386. ErrorOr<void> PropertiesWindow::create_pdf_tab(GUI::TabWidget& tab_widget, NonnullOwnPtr<Core::MappedFile> mapped_file)
  387. {
  388. auto maybe_document = PDF::Document::create(mapped_file->bytes());
  389. if (maybe_document.is_error()) {
  390. warnln("Failed to open '{}': {}", m_path, maybe_document.error().message());
  391. return {};
  392. }
  393. auto document = maybe_document.release_value();
  394. if (auto handler = document->security_handler(); handler && !handler->has_user_password()) {
  395. // FIXME: Show a password dialog, once we've switched to lazy-loading
  396. auto& tab = tab_widget.add_tab<GUI::Label>("PDF"_string);
  397. tab.set_text("PDF is password-protected."_string);
  398. return {};
  399. }
  400. if (auto maybe_error = document->initialize(); maybe_error.is_error()) {
  401. warnln("PDF '{}' seems to be invalid: {}", m_path, maybe_error.error().message());
  402. return {};
  403. }
  404. auto& tab = tab_widget.add_tab<GUI::Widget>("PDF"_string);
  405. TRY(tab.load_from_gml(properties_window_pdf_tab_gml));
  406. tab.find_descendant_of_type_named<GUI::Label>("pdf_version")->set_text(TRY(String::formatted("{}.{}", document->version().major, document->version().minor)));
  407. tab.find_descendant_of_type_named<GUI::Label>("pdf_page_count")->set_text(TRY(String::number(document->get_page_count())));
  408. auto maybe_info_dict = document->info_dict();
  409. if (maybe_info_dict.is_error()) {
  410. warnln("Failed to read InfoDict from '{}': {}", m_path, maybe_info_dict.error().message());
  411. } else if (maybe_info_dict.value().has_value()) {
  412. auto get_info_string = [](PDF::PDFErrorOr<Optional<DeprecatedString>> input) -> ErrorOr<String> {
  413. if (input.is_error())
  414. return String {};
  415. if (!input.value().has_value())
  416. return String {};
  417. return String::from_deprecated_string(input.value().value());
  418. };
  419. auto info_dict = maybe_info_dict.release_value().release_value();
  420. tab.find_descendant_of_type_named<GUI::Label>("pdf_title")->set_text(TRY(get_info_string(info_dict.title())));
  421. tab.find_descendant_of_type_named<GUI::Label>("pdf_author")->set_text(TRY(get_info_string(info_dict.author())));
  422. tab.find_descendant_of_type_named<GUI::Label>("pdf_subject")->set_text(TRY(get_info_string(info_dict.subject())));
  423. tab.find_descendant_of_type_named<GUI::Label>("pdf_keywords")->set_text(TRY(get_info_string(info_dict.keywords())));
  424. tab.find_descendant_of_type_named<GUI::Label>("pdf_creator")->set_text(TRY(get_info_string(info_dict.creator())));
  425. tab.find_descendant_of_type_named<GUI::Label>("pdf_producer")->set_text(TRY(get_info_string(info_dict.producer())));
  426. tab.find_descendant_of_type_named<GUI::Label>("pdf_creation_date")->set_text(TRY(get_info_string(info_dict.creation_date())));
  427. tab.find_descendant_of_type_named<GUI::Label>("pdf_modification_date")->set_text(TRY(get_info_string(info_dict.modification_date())));
  428. }
  429. return {};
  430. }
  431. void PropertiesWindow::update()
  432. {
  433. m_icon->set_bitmap(GUI::FileIconProvider::icon_for_path(make_full_path(m_name), m_mode).bitmap_for_size(32));
  434. set_title(DeprecatedString::formatted("{} - Properties", m_name));
  435. }
  436. void PropertiesWindow::permission_changed(mode_t mask, bool set)
  437. {
  438. if (set) {
  439. m_mode |= mask;
  440. } else {
  441. m_mode &= ~mask;
  442. }
  443. m_permissions_dirty = m_mode != m_old_mode;
  444. m_apply_button->set_enabled(m_name_dirty || m_permissions_dirty);
  445. }
  446. DeprecatedString PropertiesWindow::make_full_path(DeprecatedString const& name)
  447. {
  448. return DeprecatedString::formatted("{}/{}", m_parent_path, name);
  449. }
  450. bool PropertiesWindow::apply_changes()
  451. {
  452. if (m_name_dirty) {
  453. DeprecatedString new_name = m_name_box->text();
  454. DeprecatedString new_file = make_full_path(new_name).characters();
  455. if (FileSystem::exists(new_file)) {
  456. GUI::MessageBox::show(this, DeprecatedString::formatted("A file \"{}\" already exists!", new_name), "Error"sv, GUI::MessageBox::Type::Error);
  457. return false;
  458. }
  459. if (rename(make_full_path(m_name).characters(), new_file.characters())) {
  460. GUI::MessageBox::show(this, DeprecatedString::formatted("Could not rename file: {}!", strerror(errno)), "Error"sv, GUI::MessageBox::Type::Error);
  461. return false;
  462. }
  463. m_name = new_name;
  464. m_name_dirty = false;
  465. update();
  466. }
  467. if (m_permissions_dirty) {
  468. if (chmod(make_full_path(m_name).characters(), m_mode)) {
  469. GUI::MessageBox::show(this, DeprecatedString::formatted("Could not update permissions: {}!", strerror(errno)), "Error"sv, GUI::MessageBox::Type::Error);
  470. return false;
  471. }
  472. m_old_mode = m_mode;
  473. m_permissions_dirty = false;
  474. }
  475. auto directory_view = parent()->find_descendant_of_type_named<FileManager::DirectoryView>("directory_view");
  476. directory_view->refresh();
  477. update();
  478. m_apply_button->set_enabled(false);
  479. return true;
  480. }
  481. ErrorOr<void> PropertiesWindow::setup_permission_checkboxes(GUI::CheckBox& box_read, GUI::CheckBox& box_write, GUI::CheckBox& box_execute, PermissionMasks masks, mode_t mode)
  482. {
  483. auto st = TRY(Core::System::lstat(m_path));
  484. auto can_edit_checkboxes = st.st_uid == getuid();
  485. box_read.set_checked(mode & masks.read);
  486. box_read.on_checked = [&, masks](bool checked) { permission_changed(masks.read, checked); };
  487. box_read.set_enabled(can_edit_checkboxes);
  488. box_write.set_checked(mode & masks.write);
  489. box_write.on_checked = [&, masks](bool checked) { permission_changed(masks.write, checked); };
  490. box_write.set_enabled(can_edit_checkboxes);
  491. box_execute.set_checked(mode & masks.execute);
  492. box_execute.on_checked = [&, masks](bool checked) { permission_changed(masks.execute, checked); };
  493. box_execute.set_enabled(can_edit_checkboxes);
  494. return {};
  495. }
  496. GUI::Button& PropertiesWindow::make_button(String text, GUI::Widget& parent)
  497. {
  498. auto& button = parent.add<GUI::Button>(text);
  499. button.set_fixed_size(70, 22);
  500. return button;
  501. }
  502. void PropertiesWindow::close()
  503. {
  504. GUI::Window::close();
  505. if (m_directory_statistics_calculator)
  506. m_directory_statistics_calculator->stop();
  507. }
  508. PropertiesWindow::DirectoryStatisticsCalculator::DirectoryStatisticsCalculator(DeprecatedString path)
  509. {
  510. m_work_queue.enqueue(path);
  511. }
  512. void PropertiesWindow::DirectoryStatisticsCalculator::start()
  513. {
  514. using namespace AK::TimeLiterals;
  515. VERIFY(!m_background_action);
  516. m_background_action = Threading::BackgroundAction<int>::construct(
  517. [this, strong_this = NonnullRefPtr(*this)](auto& task) -> ErrorOr<int> {
  518. auto timer = Core::ElapsedTimer();
  519. while (!m_work_queue.is_empty()) {
  520. auto base_directory = m_work_queue.dequeue();
  521. auto result = Core::Directory::for_each_entry(base_directory, Core::DirIterator::SkipParentAndBaseDir, [&](auto const& entry, auto const& directory) -> ErrorOr<IterationDecision> {
  522. if (task.is_canceled())
  523. return Error::from_errno(ECANCELED);
  524. struct stat st = {};
  525. if (fstatat(directory.fd(), entry.name.characters(), &st, AT_SYMLINK_NOFOLLOW) < 0) {
  526. perror("fstatat");
  527. return IterationDecision::Continue;
  528. }
  529. if (S_ISDIR(st.st_mode)) {
  530. auto full_path = LexicalPath::join(directory.path().string(), entry.name).string();
  531. m_directory_count++;
  532. m_work_queue.enqueue(full_path);
  533. } else if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {
  534. m_file_count++;
  535. m_total_size_in_bytes += st.st_size;
  536. }
  537. // Show the first update, then show any subsequent updates every 100ms.
  538. if (!task.is_canceled() && on_update && (!timer.is_valid() || timer.elapsed_time() > 100_ms)) {
  539. timer.start();
  540. on_update(m_total_size_in_bytes, m_file_count, m_directory_count);
  541. }
  542. return IterationDecision::Continue;
  543. });
  544. if (result.is_error() && result.error().code() == ECANCELED)
  545. return Error::from_errno(ECANCELED);
  546. }
  547. return 0;
  548. },
  549. [this](auto) -> ErrorOr<void> {
  550. if (on_update)
  551. on_update(m_total_size_in_bytes, m_file_count, m_directory_count);
  552. return {};
  553. },
  554. [](auto) {
  555. // Ignore the error.
  556. });
  557. }
  558. void PropertiesWindow::DirectoryStatisticsCalculator::stop()
  559. {
  560. VERIFY(m_background_action);
  561. m_background_action->cancel();
  562. }