From b0b523e973eb29a5dbf50b1995cbfca63b228a6c Mon Sep 17 00:00:00 2001 From: Till Mayer Date: Wed, 20 Nov 2019 21:52:15 +0100 Subject: [PATCH] FileManager: Added properties dialog The user can rename files, change the permissions and view different properties of the file. --- Applications/FileManager/DirectoryView.h | 3 +- Applications/FileManager/Makefile | 1 + Applications/FileManager/PropertiesDialog.cpp | 258 ++++++++++++++++++ Applications/FileManager/PropertiesDialog.h | 71 +++++ Applications/FileManager/main.cpp | 28 +- Libraries/LibGUI/GDirectoryModel.cpp | 46 ++-- Libraries/LibGUI/GDirectoryModel.h | 15 + 7 files changed, 389 insertions(+), 33 deletions(-) create mode 100644 Applications/FileManager/PropertiesDialog.cpp create mode 100644 Applications/FileManager/PropertiesDialog.h diff --git a/Applications/FileManager/DirectoryView.h b/Applications/FileManager/DirectoryView.h index d513c5b2011..6d5dcde8cdd 100644 --- a/Applications/FileManager/DirectoryView.h +++ b/Applications/FileManager/DirectoryView.h @@ -55,9 +55,10 @@ public: callback(*m_item_view); } + GDirectoryModel& model() { return *m_model; } + private: explicit DirectoryView(GWidget* parent); - GDirectoryModel& model() { return *m_model; } const GDirectoryModel& model() const { return *m_model; } void handle_activation(const GModelIndex&); diff --git a/Applications/FileManager/Makefile b/Applications/FileManager/Makefile index 4c98970a585..b63aeabe61b 100644 --- a/Applications/FileManager/Makefile +++ b/Applications/FileManager/Makefile @@ -3,6 +3,7 @@ include ../../Makefile.common OBJS = \ DirectoryView.o \ FileUtils.o \ + PropertiesDialog.o \ main.o APP = FileManager diff --git a/Applications/FileManager/PropertiesDialog.cpp b/Applications/FileManager/PropertiesDialog.cpp new file mode 100644 index 00000000000..bde03b9e53b --- /dev/null +++ b/Applications/FileManager/PropertiesDialog.cpp @@ -0,0 +1,258 @@ +#include "PropertiesDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +PropertiesDialog::PropertiesDialog(GDirectoryModel& model, String path, bool disable_rename, CObject* parent) + : GDialog(parent) + , m_model(model) +{ + auto file_path = FileSystemPath(path); + ASSERT(file_path.is_valid()); + + auto main_widget = GWidget::construct(); + main_widget->set_layout(make(Orientation::Vertical)); + main_widget->layout()->set_margins({ 4, 4, 4, 4 }); + main_widget->set_fill_with_background_color(true); + + set_main_widget(main_widget); + set_rect({ 0, 0, 360, 420 }); + set_resizable(false); + + auto tab_widget = GTabWidget::construct(main_widget); + + auto general_tab = GWidget::construct(tab_widget.ptr()); + general_tab->set_layout(make(Orientation::Vertical)); + general_tab->layout()->set_margins({ 12, 8, 12, 8 }); + general_tab->layout()->set_spacing(10); + tab_widget->add_widget("General", general_tab); + + general_tab->layout()->add_spacer(); + + auto file_container = GWidget::construct(general_tab.ptr()); + file_container->set_layout(make(Orientation::Horizontal)); + file_container->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + file_container->layout()->set_spacing(20); + file_container->set_preferred_size(0, 34); + + m_icon = GLabel::construct(file_container); + m_icon->set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed); + m_icon->set_preferred_size(32, 32); + + m_name = file_path.basename(); + + m_name_box = GTextBox::construct(file_container); + m_name_box->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + m_name_box->set_preferred_size({ 0, 22 }); + m_name_box->set_text(m_name); + m_name_box->on_change = [&, disable_rename]() { + if (disable_rename) { + m_name_box->set_text(m_name); //FIXME: GTextBox does not support set_enabled yet... + } else { + m_name_dirty = m_name != m_name_box->text(); + m_apply_button->set_enabled(true); + } + }; + + set_icon(GraphicsBitmap::load_from_file("/res/icons/16x16/properties.png")); + make_divider(general_tab); + + struct stat st; + if (lstat(path.characters(), &st)) { + perror("stat"); + return; + } + + struct passwd* user_pw = getpwuid(st.st_uid); + struct passwd* group_pw = getpwuid(st.st_gid); + ASSERT(user_pw && group_pw); + + m_mode = st.st_mode; + + auto properties = Vector(); + properties.append({ "Type:", get_description(m_mode) }); + properties.append({ "Location:", path }); + + if (S_ISLNK(m_mode)) { + char link_destination[PATH_MAX]; + if (readlink(path.characters(), link_destination, sizeof(link_destination))) { + perror("readlink"); + return; + } + + properties.append({ "Link target:", link_destination }); + } + + properties.append({ "Size:", String::format("%zu bytes", st.st_size) }); + properties.append({ "Owner:", String::format("%s (%lu)", user_pw->pw_name, static_cast(user_pw->pw_uid)) }); + properties.append({ "Group:", String::format("%s (%lu)", group_pw->pw_name, static_cast(group_pw->pw_uid)) }); + properties.append({ "Created at:", GDirectoryModel::timestamp_string(st.st_ctime) }); + properties.append({ "Last modified:", GDirectoryModel::timestamp_string(st.st_mtime) }); + + make_property_value_pairs(properties, general_tab); + + make_divider(general_tab); + + make_permission_checkboxes(general_tab, { S_IRUSR, S_IWUSR, S_IXUSR }, "Owner:", m_mode); + make_permission_checkboxes(general_tab, { S_IRGRP, S_IWGRP, S_IXGRP }, "Group:", m_mode); + make_permission_checkboxes(general_tab, { S_IROTH, S_IWOTH, S_IXOTH }, "Others:", m_mode); + + general_tab->layout()->add_spacer(); + + auto button_widget = GWidget::construct(main_widget.ptr()); + button_widget->set_layout(make(Orientation::Horizontal)); + button_widget->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + button_widget->set_preferred_size(0, 24); + button_widget->layout()->set_spacing(5); + + button_widget->layout()->add_spacer(); + + make_button("OK", button_widget)->on_click = [&](auto&) {if(apply_changes()) close(); }; + make_button("Cancel", button_widget)->on_click = [&](auto&) { close(); }; + + m_apply_button = make_button("Apply", button_widget); + m_apply_button->on_click = [&](auto&) { apply_changes(); }; + m_apply_button->set_enabled(false); + + update(); +} + +PropertiesDialog::~PropertiesDialog() {} + +void PropertiesDialog::update() +{ + m_model.update(); + m_icon->set_icon(const_cast(m_model.icon_for_file(m_mode, m_name).bitmap_for_size(32))); + set_title(String::format("Properties of \"%s\"", m_name.characters())); +} + +void PropertiesDialog::permission_changed(mode_t mask, bool set) +{ + if (set) { + m_mode |= mask; + } else { + m_mode &= ~mask; + } + + m_permissions_dirty = true; + m_apply_button->set_enabled(true); +} + +String PropertiesDialog::make_full_path(String name) +{ + return String::format("%s/%s", m_model.path().characters(), name.characters()); +} + +bool PropertiesDialog::apply_changes() +{ + if (m_name_dirty) { + String new_name = m_name_box->text(); + String new_file = make_full_path(new_name).characters(); + + if (GFilePicker::file_exists(new_file)) { + GMessageBox::show(String::format("A file \"%s\" already exists!", new_name.characters()), "Error", GMessageBox::Type::Error); + return false; + } + + if (rename(make_full_path(m_name).characters(), new_file.characters())) { + GMessageBox::show(String::format("Could not rename file: %s!", strerror(errno)), "Error", GMessageBox::Type::Error); + return false; + } + + m_name = new_name; + m_name_dirty = false; + update(); + } + + if (m_permissions_dirty) { + if (chmod(make_full_path(m_name).characters(), m_mode)) { + GMessageBox::show(String::format("Could not update permissions: %s!", strerror(errno)), "Error", GMessageBox::Type::Error); + return false; + } + + m_permissions_dirty = false; + } + + update(); + m_apply_button->set_enabled(false); + return true; +} + +void PropertiesDialog::make_permission_checkboxes(NonnullRefPtr& parent, PermissionMasks masks, String label_string, mode_t mode) +{ + auto widget = GWidget::construct(parent.ptr()); + widget->set_layout(make(Orientation::Horizontal)); + widget->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + widget->set_preferred_size(0, 16); + widget->layout()->set_spacing(10); + + auto label = GLabel::construct(label_string, widget); + label->set_text_alignment(TextAlignment::CenterLeft); + + auto box_read = GCheckBox::construct("Read", widget); + box_read->set_checked(mode & masks.read); + box_read->on_checked = [&, masks](bool checked) { permission_changed(masks.read, checked); }; + + auto box_write = GCheckBox::construct("Write", widget); + box_write->set_checked(mode & masks.write); + box_write->on_checked = [&, masks](bool checked) { permission_changed(masks.write, checked); }; + + auto box_execute = GCheckBox::construct("Execute", widget); + box_execute->set_checked(mode & masks.execute); + box_execute->on_checked = [&, masks](bool checked) { permission_changed(masks.execute, checked); }; +} + +void PropertiesDialog::make_property_value_pairs(const Vector& pairs, NonnullRefPtr& parent) +{ + int max_width = 0; + Vector> property_labels; + + property_labels.ensure_capacity(pairs.size()); + for (auto pair : pairs) { + auto label_container = GWidget::construct(parent.ptr()); + label_container->set_layout(make(Orientation::Horizontal)); + label_container->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + label_container->set_preferred_size(0, 14); + label_container->layout()->set_spacing(12); + + auto label_property = GLabel::construct(pair.property, label_container); + label_property->set_text_alignment(TextAlignment::CenterLeft); + label_property->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); + + GLabel::construct(pair.value, label_container)->set_text_alignment(TextAlignment::CenterLeft); + + max_width = max(max_width, label_property->font().width(pair.property)); + property_labels.append(label_property); + } + + for (auto label : property_labels) + label->set_preferred_size({ max_width, 0 }); +} + +NonnullRefPtr PropertiesDialog::make_button(String text, NonnullRefPtr& parent) +{ + auto button = GButton::construct(text, parent.ptr()); + button->set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed); + button->set_preferred_size(70, 22); + return button; +} + +void PropertiesDialog::make_divider(NonnullRefPtr& parent) +{ + parent->layout()->add_spacer(); + + auto divider = GFrame::construct(parent.ptr()); + divider->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + divider->set_preferred_size({ 0, 2 }); + divider->set_frame_shape(FrameShape::HorizontalLine); + divider->set_frame_shadow(FrameShadow::Sunken); + divider->set_frame_thickness(2); + + parent->layout()->add_spacer(); +} diff --git a/Applications/FileManager/PropertiesDialog.h b/Applications/FileManager/PropertiesDialog.h new file mode 100644 index 00000000000..d4f3e6d709a --- /dev/null +++ b/Applications/FileManager/PropertiesDialog.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class PropertiesDialog final : public GDialog { + C_OBJECT(PropertiesDialog) +public: + virtual ~PropertiesDialog() override; + +private: + explicit PropertiesDialog(GDirectoryModel&, String, bool disable_rename, CObject* parent = nullptr); + + struct PropertyValuePair { + String property; + String value; + }; + + struct PermissionMasks { + mode_t read; + mode_t write; + mode_t execute; + }; + + static const String get_description(const mode_t mode) + { + if (S_ISREG(mode)) + return "File"; + if (S_ISDIR(mode)) + return "Directory"; + if (S_ISLNK(mode)) + return "Symbolic link"; + if (S_ISCHR(mode)) + return "Character device"; + if (S_ISBLK(mode)) + return "Block device"; + if (S_ISFIFO(mode)) + return "FIFO (named pipe)"; + if (S_ISSOCK(mode)) + return "Socket"; + if (mode & S_IXUSR) + return "Executable"; + + return "Unknown"; + } + + NonnullRefPtr make_button(String, NonnullRefPtr&); + void make_divider(NonnullRefPtr&); + void make_property_value_pairs(const Vector& pairs, NonnullRefPtr& parent); + void make_permission_checkboxes(NonnullRefPtr& parent, PermissionMasks, String label_string, mode_t mode); + void permission_changed(mode_t mask, bool set); + bool apply_changes(); + void update(); + String make_full_path(String name); + + GDirectoryModel& m_model; + RefPtr m_apply_button; + RefPtr m_name_box; + RefPtr m_icon; + String m_name; + String m_path; + mode_t m_mode; + int m_row; + bool m_permissions_dirty { false }; + bool m_name_dirty { false }; +}; diff --git a/Applications/FileManager/main.cpp b/Applications/FileManager/main.cpp index 6c76c5e60bb..2fb5a9e12a4 100644 --- a/Applications/FileManager/main.cpp +++ b/Applications/FileManager/main.cpp @@ -1,5 +1,6 @@ #include "DirectoryView.h" #include "FileUtils.h" +#include "PropertiesDialog.h" #include #include #include @@ -45,13 +46,13 @@ int main(int argc, char** argv) auto window = GWindow::construct(); window->set_title("File Manager"); - + auto left = config->read_num_entry("Window", "Left", 150); auto top = config->read_num_entry("Window", "Top", 75); auto width = config->read_num_entry("Window", "Width", 640); auto heigth = config->read_num_entry("Window", "Heigth", 480); - window->set_rect( {left, top, width, heigth} ); - + window->set_rect({ left, top, width, heigth }); + auto widget = GWidget::construct(); widget->set_layout(make(Orientation::Vertical)); widget->layout()->set_spacing(0); @@ -154,7 +155,7 @@ int main(int argc, char** argv) view_type_action_group->set_exclusive(true); view_type_action_group->add_action(*view_as_table_action); view_type_action_group->add_action(*view_as_icons_action); - + auto selected_file_paths = [&] { Vector paths; auto& view = directory_view->current_view(); @@ -211,9 +212,22 @@ int main(int argc, char** argv) }; auto properties_action - = GAction::create("Properties...", { Mod_Alt, Key_Return }, GraphicsBitmap::load_from_file("/res/icons/16x16/properties.png"), [](auto&) {}); + = GAction::create("Properties...", { Mod_Alt, Key_Return }, GraphicsBitmap::load_from_file("/res/icons/16x16/properties.png"), [&](auto&) { + auto& model = directory_view->model(); + auto selected = selected_file_paths(); - enum class ConfirmBeforeDelete { No, Yes }; + RefPtr properties; + if (selected.is_empty()) { + properties = PropertiesDialog::construct(model, directory_view->path(), true, window); + } else { + properties = PropertiesDialog::construct(model, selected.first(), false, window); + } + + properties->exec(); + }); + + enum class ConfirmBeforeDelete { No, + Yes }; auto do_delete = [&](ConfirmBeforeDelete confirm) { auto paths = selected_file_paths(); @@ -423,6 +437,6 @@ int main(int argc, char** argv) return GWindow::CloseRequestDecision::Close; }; - + return app.exec(); } diff --git a/Libraries/LibGUI/GDirectoryModel.cpp b/Libraries/LibGUI/GDirectoryModel.cpp index 20c80c4c0f8..cbb08390e47 100644 --- a/Libraries/LibGUI/GDirectoryModel.cpp +++ b/Libraries/LibGUI/GDirectoryModel.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include static HashMap> s_thumbnail_cache; @@ -154,20 +153,28 @@ bool GDirectoryModel::fetch_thumbnail_for(const Entry& entry) return false; } +GIcon GDirectoryModel::icon_for_file(const mode_t mode, const String name) const +{ + if (S_ISDIR(mode)) + return m_directory_icon; + if (S_ISLNK(mode)) + return m_symlink_icon; + if (S_ISSOCK(mode)) + return m_socket_icon; + if (mode & S_IXUSR) + return m_executable_icon; + if (name.to_lowercase().ends_with(".wav")) + return m_filetype_sound_icon; + if (name.to_lowercase().ends_with(".html")) + return m_filetype_html_icon; + if (name.to_lowercase().ends_with(".png")) { + return m_filetype_image_icon; + } + return m_file_icon; +} + GIcon GDirectoryModel::icon_for(const Entry& entry) const { - if (S_ISDIR(entry.mode)) - return m_directory_icon; - if (S_ISLNK(entry.mode)) - return m_symlink_icon; - if (S_ISSOCK(entry.mode)) - return m_socket_icon; - if (entry.mode & S_IXUSR) - return m_executable_icon; - if (entry.name.to_lowercase().ends_with(".wav")) - return m_filetype_sound_icon; - if (entry.name.to_lowercase().ends_with(".html")) - return m_filetype_html_icon; if (entry.name.to_lowercase().ends_with(".png")) { if (!entry.thumbnail) { if (!const_cast(this)->fetch_thumbnail_for(entry)) @@ -175,19 +182,8 @@ GIcon GDirectoryModel::icon_for(const Entry& entry) const } return GIcon(m_filetype_image_icon.bitmap_for_size(16), *entry.thumbnail); } - return m_file_icon; -} -static String timestamp_string(time_t timestamp) -{ - auto* tm = localtime(×tamp); - return String::format("%4u-%02u-%02u %02u:%02u:%02u", - tm->tm_year + 1900, - tm->tm_mon + 1, - tm->tm_mday, - tm->tm_hour, - tm->tm_min, - tm->tm_sec); + return icon_for_file(entry.mode, entry.name); } static String permission_string(mode_t mode) diff --git a/Libraries/LibGUI/GDirectoryModel.h b/Libraries/LibGUI/GDirectoryModel.h index 3a94e5ff2e9..01930838b87 100644 --- a/Libraries/LibGUI/GDirectoryModel.h +++ b/Libraries/LibGUI/GDirectoryModel.h @@ -4,6 +4,7 @@ #include #include #include +#include class GDirectoryModel final : public GModel , public Weakable { @@ -58,6 +59,20 @@ public: return m_files[index - m_directories.size()]; } + GIcon icon_for_file(const mode_t mode, const String name) const; + + static String timestamp_string(time_t timestamp) + { + auto* tm = localtime(×tamp); + return String::format("%4u-%02u-%02u %02u:%02u:%02u", + tm->tm_year + 1900, + tm->tm_mon + 1, + tm->tm_mday, + tm->tm_hour, + tm->tm_min, + tm->tm_sec); + } + private: GDirectoryModel();