PropertiesWindow.cpp 30 KB

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