diff options
author | Sergey Bugaev <bugaevc@gmail.com> | 2020-01-10 18:58:00 +0300 |
---|---|---|
committer | Andreas Kling <awesomekling@gmail.com> | 2020-01-10 17:45:59 +0100 |
commit | fdeb91e000f57407334c7cfdd11ccd7c97dd51c6 (patch) | |
tree | 1e68a3045aebeb9e3bd04ea2eac5298651540b6d /Libraries | |
parent | 0f18a16e2c855a99e5c261793a8d36565b410a72 (diff) | |
download | serenity-fdeb91e000f57407334c7cfdd11ccd7c97dd51c6.zip |
LibGUI+FileManager: Merge GDirectoryModel into GFileSystemModel
We used to have two different models for displaying file system contents:
the FileManager-grade table-like directory model, which exposed rich data
(such as file icons with integrated image previews) about contents of a
single directory, and the tree-like GFileSystemModel, which only exposed
a tree of file names with very basic info about them.
This commit unifies the two. The new GFileSystemModel can be used both as a
tree-like and as a table-like model, or in fact in both ways simultaneously.
It exposes rich data about a file system subtree rooted at the given root.
The users of the two previous models are all ported to use this new model.
Diffstat (limited to 'Libraries')
-rw-r--r-- | Libraries/LibGUI/GDirectoryModel.cpp | 373 | ||||
-rw-r--r-- | Libraries/LibGUI/GDirectoryModel.h | 106 | ||||
-rw-r--r-- | Libraries/LibGUI/GFilePicker.cpp | 42 | ||||
-rw-r--r-- | Libraries/LibGUI/GFilePicker.h | 4 | ||||
-rw-r--r-- | Libraries/LibGUI/GFileSystemModel.cpp | 538 | ||||
-rw-r--r-- | Libraries/LibGUI/GFileSystemModel.h | 109 | ||||
-rw-r--r-- | Libraries/LibGUI/Makefile | 1 |
7 files changed, 531 insertions, 642 deletions
diff --git a/Libraries/LibGUI/GDirectoryModel.cpp b/Libraries/LibGUI/GDirectoryModel.cpp deleted file mode 100644 index e4a6cda880..0000000000 --- a/Libraries/LibGUI/GDirectoryModel.cpp +++ /dev/null @@ -1,373 +0,0 @@ -#include "GDirectoryModel.h" -#include <AK/FileSystemPath.h> -#include <AK/StringBuilder.h> -#include <LibCore/CDirIterator.h> -#include <LibDraw/GraphicsBitmap.h> -#include <LibGUI/GPainter.h> -#include <LibThread/BackgroundAction.h> -#include <dirent.h> -#include <grp.h> -#include <pwd.h> -#include <stdio.h> -#include <unistd.h> - -static HashMap<String, RefPtr<GraphicsBitmap>> s_thumbnail_cache; - -static RefPtr<GraphicsBitmap> render_thumbnail(const StringView& path) -{ - auto png_bitmap = GraphicsBitmap::load_from_file(path); - if (!png_bitmap) - return nullptr; - auto thumbnail = GraphicsBitmap::create(png_bitmap->format(), { 32, 32 }); - Painter painter(*thumbnail); - painter.draw_scaled_bitmap(thumbnail->rect(), *png_bitmap, png_bitmap->rect()); - return thumbnail; -} - -GDirectoryModel::GDirectoryModel() -{ - m_directory_icon = GIcon::default_icon("filetype-folder"); - m_file_icon = GIcon::default_icon("filetype-unknown"); - m_symlink_icon = GIcon::default_icon("filetype-symlink"); - m_socket_icon = GIcon::default_icon("filetype-socket"); - m_executable_icon = GIcon::default_icon("filetype-executable"); - m_filetype_image_icon = GIcon::default_icon("filetype-image"); - m_filetype_sound_icon = GIcon::default_icon("filetype-sound"); - m_filetype_html_icon = GIcon::default_icon("filetype-html"); - - setpwent(); - while (auto* passwd = getpwent()) - m_user_names.set(passwd->pw_uid, passwd->pw_name); - endpwent(); - - setgrent(); - while (auto* group = getgrent()) - m_group_names.set(group->gr_gid, group->gr_name); - endgrent(); -} - -GDirectoryModel::~GDirectoryModel() -{ -} - -int GDirectoryModel::row_count(const GModelIndex&) const -{ - return m_directories.size() + m_files.size(); -} - -int GDirectoryModel::column_count(const GModelIndex&) const -{ - return Column::__Count; -} - -String GDirectoryModel::column_name(int column) const -{ - switch (column) { - case Column::Icon: - return ""; - case Column::Name: - return "Name"; - case Column::Size: - return "Size"; - case Column::Owner: - return "Owner"; - case Column::Group: - return "Group"; - case Column::Permissions: - return "Mode"; - case Column::ModificationTime: - return "Modified"; - case Column::Inode: - return "Inode"; - } - ASSERT_NOT_REACHED(); -} - -GModel::ColumnMetadata GDirectoryModel::column_metadata(int column) const -{ - switch (column) { - case Column::Icon: - return { 16, TextAlignment::Center, nullptr, GModel::ColumnMetadata::Sortable::False }; - case Column::Name: - return { 120, TextAlignment::CenterLeft }; - case Column::Size: - return { 80, TextAlignment::CenterRight }; - case Column::Owner: - return { 50, TextAlignment::CenterLeft }; - case Column::Group: - return { 50, TextAlignment::CenterLeft }; - case Column::ModificationTime: - return { 110, TextAlignment::CenterLeft }; - case Column::Permissions: - return { 65, TextAlignment::CenterLeft }; - case Column::Inode: - return { 60, TextAlignment::CenterRight }; - } - ASSERT_NOT_REACHED(); -} - -bool GDirectoryModel::fetch_thumbnail_for(const Entry& entry) -{ - // See if we already have the thumbnail - // we're looking for in the cache. - auto path = entry.full_path(*this); - auto it = s_thumbnail_cache.find(path); - if (it != s_thumbnail_cache.end()) { - if (!(*it).value) - return false; - entry.thumbnail = (*it).value; - return true; - } - - // Otherwise, arrange to render the thumbnail - // in background and make it available later. - - s_thumbnail_cache.set(path, nullptr); - m_thumbnail_progress_total++; - - auto directory_model = make_weak_ptr(); - - LibThread::BackgroundAction<RefPtr<GraphicsBitmap>>::create( - [path] { - return render_thumbnail(path); - }, - - [this, path, directory_model](auto thumbnail) { - s_thumbnail_cache.set(path, move(thumbnail)); - - // class was destroyed, no need to update progress or call any event handlers. - if (directory_model.is_null()) - return; - - m_thumbnail_progress++; - if (on_thumbnail_progress) - on_thumbnail_progress(m_thumbnail_progress, m_thumbnail_progress_total); - if (m_thumbnail_progress == m_thumbnail_progress_total) { - m_thumbnail_progress = 0; - m_thumbnail_progress_total = 0; - } - - did_update(); - }); - - 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 (entry.name.to_lowercase().ends_with(".png")) { - if (!entry.thumbnail) { - if (!const_cast<GDirectoryModel*>(this)->fetch_thumbnail_for(entry)) - return m_filetype_image_icon; - } - return GIcon(m_filetype_image_icon.bitmap_for_size(16), *entry.thumbnail); - } - - return icon_for_file(entry.mode, entry.name); -} - -static String permission_string(mode_t mode) -{ - StringBuilder builder; - if (S_ISDIR(mode)) - builder.append("d"); - else if (S_ISLNK(mode)) - builder.append("l"); - else if (S_ISBLK(mode)) - builder.append("b"); - else if (S_ISCHR(mode)) - builder.append("c"); - else if (S_ISFIFO(mode)) - builder.append("f"); - else if (S_ISSOCK(mode)) - builder.append("s"); - else if (S_ISREG(mode)) - builder.append("-"); - else - builder.append("?"); - - builder.appendf("%c%c%c%c%c%c%c%c", - mode & S_IRUSR ? 'r' : '-', - mode & S_IWUSR ? 'w' : '-', - mode & S_ISUID ? 's' : (mode & S_IXUSR ? 'x' : '-'), - mode & S_IRGRP ? 'r' : '-', - mode & S_IWGRP ? 'w' : '-', - mode & S_ISGID ? 's' : (mode & S_IXGRP ? 'x' : '-'), - mode & S_IROTH ? 'r' : '-', - mode & S_IWOTH ? 'w' : '-'); - - if (mode & S_ISVTX) - builder.append("t"); - else - builder.appendf("%c", mode & S_IXOTH ? 'x' : '-'); - return builder.to_string(); -} - -String GDirectoryModel::name_for_uid(uid_t uid) const -{ - auto it = m_user_names.find(uid); - if (it == m_user_names.end()) - return String::number(uid); - return (*it).value; -} - -String GDirectoryModel::name_for_gid(uid_t gid) const -{ - auto it = m_user_names.find(gid); - if (it == m_user_names.end()) - return String::number(gid); - return (*it).value; -} - -GVariant GDirectoryModel::data(const GModelIndex& index, Role role) const -{ - ASSERT(is_valid(index)); - auto& entry = this->entry(index.row()); - if (role == Role::Custom) { - ASSERT(index.column() == Column::Name); - return entry.full_path(*this); - } - if (role == Role::DragData) { - if (index.column() == Column::Name) { - StringBuilder builder; - builder.append("file://"); - builder.append(entry.full_path(*this)); - return builder.to_string(); - } - return {}; - } - if (role == Role::Sort) { - switch (index.column()) { - case Column::Icon: - return entry.is_directory() ? 0 : 1; - case Column::Name: - return entry.name; - case Column::Size: - return (int)entry.size; - case Column::Owner: - return name_for_uid(entry.uid); - case Column::Group: - return name_for_gid(entry.gid); - case Column::Permissions: - return permission_string(entry.mode); - case Column::ModificationTime: - return entry.mtime; - case Column::Inode: - return (int)entry.inode; - } - ASSERT_NOT_REACHED(); - } - if (role == Role::Display) { - switch (index.column()) { - case Column::Icon: - return icon_for(entry); - case Column::Name: - return entry.name; - case Column::Size: - return (int)entry.size; - case Column::Owner: - return name_for_uid(entry.uid); - case Column::Group: - return name_for_gid(entry.gid); - case Column::Permissions: - return permission_string(entry.mode); - case Column::ModificationTime: - return timestamp_string(entry.mtime); - case Column::Inode: - return (int)entry.inode; - } - } - if (role == Role::Icon) { - return icon_for(entry); - } - return {}; -} - -void GDirectoryModel::update() -{ - CDirIterator di(m_path, CDirIterator::SkipDots); - if (di.has_error()) { - fprintf(stderr, "CDirIterator: %s\n", di.error_string()); - exit(1); - } - - m_directories.clear(); - m_files.clear(); - - m_bytes_in_files = 0; - while (di.has_next()) { - String name = di.next_path(); - Entry entry; - entry.name = name; - - struct stat st; - int rc = lstat(String::format("%s/%s", m_path.characters(), name.characters()).characters(), &st); - if (rc < 0) { - perror("lstat"); - continue; - } - entry.size = st.st_size; - entry.mode = st.st_mode; - entry.uid = st.st_uid; - entry.gid = st.st_gid; - entry.inode = st.st_ino; - entry.mtime = st.st_mtime; - auto& entries = S_ISDIR(st.st_mode) ? m_directories : m_files; - entries.append(move(entry)); - - m_bytes_in_files += st.st_size; - } - - did_update(); -} - -void GDirectoryModel::open(const StringView& a_path) -{ - auto path = canonicalized_path(a_path); - if (m_path == path) - return; - DIR* dirp = opendir(path.characters()); - if (!dirp) - return; - closedir(dirp); - if (m_notifier) { - close(m_notifier->fd()); - m_notifier = nullptr; - } - m_path = path; - int watch_fd = watch_file(path.characters(), path.length()); - if (watch_fd < 0) { - perror("watch_file"); - } else { - m_notifier = CNotifier::construct(watch_fd, CNotifier::Event::Read); - m_notifier->on_ready_to_read = [this] { - update(); - char buffer[32]; - int rc = read(m_notifier->fd(), buffer, sizeof(buffer)); - ASSERT(rc >= 0); - }; - } - if (on_path_change) - on_path_change(); - update(); -} diff --git a/Libraries/LibGUI/GDirectoryModel.h b/Libraries/LibGUI/GDirectoryModel.h deleted file mode 100644 index 01930838b8..0000000000 --- a/Libraries/LibGUI/GDirectoryModel.h +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once - -#include <AK/HashMap.h> -#include <LibCore/CNotifier.h> -#include <LibGUI/GModel.h> -#include <sys/stat.h> -#include <time.h> - -class GDirectoryModel final : public GModel - , public Weakable<GDirectoryModel> { -public: - static NonnullRefPtr<GDirectoryModel> create() { return adopt(*new GDirectoryModel); } - virtual ~GDirectoryModel() override; - - enum Column { - Icon = 0, - Name, - Size, - Owner, - Group, - Permissions, - ModificationTime, - Inode, - __Count, - }; - - virtual int row_count(const GModelIndex& = GModelIndex()) const override; - virtual int column_count(const GModelIndex& = GModelIndex()) const override; - virtual String column_name(int column) const override; - virtual ColumnMetadata column_metadata(int column) const override; - virtual GVariant data(const GModelIndex&, Role = Role::Display) const override; - virtual void update() override; - - String path() const { return m_path; } - void open(const StringView& path); - size_t bytes_in_files() const { return m_bytes_in_files; } - - Function<void(int done, int total)> on_thumbnail_progress; - Function<void()> on_path_change; - - struct Entry { - String name; - size_t size { 0 }; - mode_t mode { 0 }; - uid_t uid { 0 }; - uid_t gid { 0 }; - ino_t inode { 0 }; - time_t mtime { 0 }; - mutable RefPtr<GraphicsBitmap> thumbnail; - bool is_directory() const { return S_ISDIR(mode); } - bool is_executable() const { return mode & S_IXUSR; } - String full_path(const GDirectoryModel& model) const { return String::format("%s/%s", model.path().characters(), name.characters()); } - }; - - const Entry& entry(int index) const - { - if (index < m_directories.size()) - return m_directories[index]; - 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(); - - String name_for_uid(uid_t) const; - String name_for_gid(gid_t) const; - - bool fetch_thumbnail_for(const Entry& entry); - GIcon icon_for(const Entry& entry) const; - - String m_path; - Vector<Entry> m_files; - Vector<Entry> m_directories; - size_t m_bytes_in_files; - - GIcon m_directory_icon; - GIcon m_file_icon; - GIcon m_symlink_icon; - GIcon m_socket_icon; - GIcon m_executable_icon; - GIcon m_filetype_image_icon; - GIcon m_filetype_sound_icon; - GIcon m_filetype_html_icon; - - HashMap<uid_t, String> m_user_names; - HashMap<gid_t, String> m_group_names; - - RefPtr<CNotifier> m_notifier; - - unsigned m_thumbnail_progress { 0 }; - unsigned m_thumbnail_progress_total { 0 }; -}; diff --git a/Libraries/LibGUI/GFilePicker.cpp b/Libraries/LibGUI/GFilePicker.cpp index 48d03d96c3..41e348436d 100644 --- a/Libraries/LibGUI/GFilePicker.cpp +++ b/Libraries/LibGUI/GFilePicker.cpp @@ -4,8 +4,8 @@ #include <LibGUI/GAction.h> #include <LibGUI/GBoxLayout.h> #include <LibGUI/GButton.h> -#include <LibGUI/GDirectoryModel.h> #include <LibGUI/GFilePicker.h> +#include <LibGUI/GFileSystemModel.h> #include <LibGUI/GInputBox.h> #include <LibGUI/GLabel.h> #include <LibGUI/GMessageBox.h> @@ -48,7 +48,7 @@ Optional<String> GFilePicker::get_save_filepath(const String& title, const Strin GFilePicker::GFilePicker(Mode mode, const StringView& file_name, const StringView& path, CObject* parent) : GDialog(parent) - , m_model(GDirectoryModel::create()) + , m_model(GFileSystemModel::create()) , m_mode(mode) { set_title(m_mode == Mode::Open ? "Open File" : "Save File"); @@ -80,25 +80,25 @@ GFilePicker::GFilePicker(Mode mode, const StringView& file_name, const StringVie m_view = GTableView::construct(vertical_container); m_view->set_model(GSortingProxyModel::create(*m_model)); - m_view->set_column_hidden(GDirectoryModel::Column::Owner, true); - m_view->set_column_hidden(GDirectoryModel::Column::Group, true); - m_view->set_column_hidden(GDirectoryModel::Column::Permissions, true); - m_view->set_column_hidden(GDirectoryModel::Column::Inode, true); - m_model->open(path); + m_view->set_column_hidden(GFileSystemModel::Column::Owner, true); + m_view->set_column_hidden(GFileSystemModel::Column::Group, true); + m_view->set_column_hidden(GFileSystemModel::Column::Permissions, true); + m_view->set_column_hidden(GFileSystemModel::Column::Inode, true); + m_model->set_root_path(path); location_textbox->on_return_pressed = [&] { - m_model->open(location_textbox->text()); + m_model->set_root_path(location_textbox->text()); clear_preview(); }; auto open_parent_directory_action = GAction::create("Open parent directory", { Mod_Alt, Key_Up }, GraphicsBitmap::load_from_file("/res/icons/16x16/open-parent-directory.png"), [this](const GAction&) { - m_model->open(String::format("%s/..", m_model->path().characters())); + m_model->set_root_path(String::format("%s/..", m_model->root_path().characters())); clear_preview(); }); toolbar->add_action(*open_parent_directory_action); auto go_home_action = GCommonActions::make_go_home_action([this](auto&) { - m_model->open(get_current_user_home_path()); + m_model->set_root_path(get_current_user_home_path()); }); toolbar->add_action(go_home_action); toolbar->add_separator(); @@ -107,7 +107,7 @@ GFilePicker::GFilePicker(Mode mode, const StringView& file_name, const StringVie auto input_box = GInputBox::construct("Enter name:", "New directory", this); if (input_box->exec() == GInputBox::ExecOK && !input_box->text_value().is_empty()) { auto new_dir_path = FileSystemPath(String::format("%s/%s", - m_model->path().characters(), + m_model->root_path().characters(), input_box->text_value().characters())) .string(); int rc = mkdir(new_dir_path.characters(), 0777); @@ -147,13 +147,13 @@ GFilePicker::GFilePicker(Mode mode, const StringView& file_name, const StringVie m_view->on_selection = [this](auto& index) { auto& filter_model = (GSortingProxyModel&)*m_view->model(); auto local_index = filter_model.map_to_target(index); - const GDirectoryModel::Entry& entry = m_model->entry(local_index.row()); - FileSystemPath path(String::format("%s/%s", m_model->path().characters(), entry.name.characters())); + const GFileSystemModel::Node& node = m_model->node(local_index); + FileSystemPath path { node.full_path(m_model) }; clear_preview(); - if (!entry.is_directory()) - m_filename_textbox->set_text(entry.name); + if (!node.is_directory()) + m_filename_textbox->set_text(node.name); set_preview(path); }; @@ -183,12 +183,12 @@ GFilePicker::GFilePicker(Mode mode, const StringView& file_name, const StringVie m_view->on_activation = [this](auto& index) { auto& filter_model = (GSortingProxyModel&)*m_view->model(); auto local_index = filter_model.map_to_target(index); - const GDirectoryModel::Entry& entry = m_model->entry(local_index.row()); - FileSystemPath path(String::format("%s/%s", m_model->path().characters(), entry.name.characters())); + const GFileSystemModel::Node& node = m_model->node(local_index); + auto path = node.full_path(m_model); - if (entry.is_directory()) { - m_model->open(path.string()); - // NOTE: 'entry' is invalid from here on + if (node.is_directory()) { + m_model->set_root_path(path); + // NOTE: 'node' is invalid from here on } else { on_file_return(); } @@ -247,7 +247,7 @@ void GFilePicker::clear_preview() void GFilePicker::on_file_return() { - FileSystemPath path(String::format("%s/%s", m_model->path().characters(), m_filename_textbox->text().characters())); + FileSystemPath path(String::format("%s/%s", m_model->root_path().characters(), m_filename_textbox->text().characters())); if (GFilePicker::file_exists(path.string()) && m_mode == Mode::Save) { auto result = GMessageBox::show("File already exists, overwrite?", "Existing File", GMessageBox::Type::Warning, GMessageBox::InputType::OKCancel); diff --git a/Libraries/LibGUI/GFilePicker.h b/Libraries/LibGUI/GFilePicker.h index d4f2c35aa5..169f59fd8e 100644 --- a/Libraries/LibGUI/GFilePicker.h +++ b/Libraries/LibGUI/GFilePicker.h @@ -4,7 +4,7 @@ #include <LibGUI/GDialog.h> #include <LibGUI/GTableView.h> -class GDirectoryModel; +class GFileSystemModel; class GLabel; class GTextBox; @@ -44,7 +44,7 @@ private: } RefPtr<GTableView> m_view; - NonnullRefPtr<GDirectoryModel> m_model; + NonnullRefPtr<GFileSystemModel> m_model; FileSystemPath m_selected_file; RefPtr<GTextBox> m_filename_textbox; diff --git a/Libraries/LibGUI/GFileSystemModel.cpp b/Libraries/LibGUI/GFileSystemModel.cpp index 637fac93ef..198ca8b098 100644 --- a/Libraries/LibGUI/GFileSystemModel.cpp +++ b/Libraries/LibGUI/GFileSystemModel.cpp @@ -1,126 +1,141 @@ #include <AK/FileSystemPath.h> #include <AK/StringBuilder.h> #include <LibCore/CDirIterator.h> +#include <LibDraw/GraphicsBitmap.h> #include <LibGUI/GFileSystemModel.h> +#include <LibGUI/GPainter.h> +#include <LibThread/BackgroundAction.h> #include <dirent.h> +#include <grp.h> +#include <pwd.h> #include <stdio.h> #include <sys/stat.h> #include <unistd.h> -struct GFileSystemModel::Node { - String name; - Node* parent { nullptr }; - Vector<Node*> children; - enum Type { - Unknown, - Directory, - File - }; - Type type { Unknown }; - - bool has_traversed { false }; - - GModelIndex index(const GFileSystemModel& model) const - { - if (!parent) - return model.create_index(0, 0, const_cast<Node*>(this)); - for (int row = 0; row < parent->children.size(); ++row) { - if (parent->children[row] == this) - return model.create_index(row, 0, const_cast<Node*>(this)); - } - ASSERT_NOT_REACHED(); +GModelIndex GFileSystemModel::Node::index(const GFileSystemModel& model, int column) const +{ + if (!parent) + return {}; + for (int row = 0; row < parent->children.size(); ++row) { + if (&parent->children[row] == this) + return model.create_index(row, column, const_cast<Node*>(this)); } + ASSERT_NOT_REACHED(); +} - void cleanup() - { - for (auto& child: children) { - child->cleanup(); - delete child; - } - children.clear(); +bool GFileSystemModel::Node::fetch_data_using_lstat(const String& full_path) +{ + struct stat st; + int rc = lstat(full_path.characters(), &st); + if (rc < 0) { + perror("lstat"); + return false; } - void traverse_if_needed(const GFileSystemModel& model) - { - if (type != Node::Directory || has_traversed) - return; - has_traversed = true; + size = st.st_size; + mode = st.st_mode; + uid = st.st_uid; + gid = st.st_gid; + inode = st.st_ino; + mtime = st.st_mtime; - auto full_path = this->full_path(model); - CDirIterator di(full_path, CDirIterator::SkipDots); - if (di.has_error()) { - fprintf(stderr, "CDirIterator: %s\n", di.error_string()); - return; - } + return true; +} - while (di.has_next()) { - String name = di.next_path(); - struct stat st; - int rc = lstat(String::format("%s/%s", full_path.characters(), name.characters()).characters(), &st); - if (rc < 0) { - perror("lstat"); - continue; - } - if (model.m_mode == DirectoriesOnly && !S_ISDIR(st.st_mode)) - continue; - auto* child = new Node; - child->name = name; - child->type = S_ISDIR(st.st_mode) ? Node::Type::Directory : Node::Type::File; - child->parent = this; - children.append(child); - } +void GFileSystemModel::Node::traverse_if_needed(const GFileSystemModel& model) +{ + if (!is_directory() || has_traversed) + return; + has_traversed = true; + total_size = 0; + + auto full_path = this->full_path(model); + CDirIterator di(full_path, CDirIterator::SkipDots); + if (di.has_error()) { + fprintf(stderr, "CDirIterator: %s\n", di.error_string()); + return; } - void reify_if_needed(const GFileSystemModel& model) - { - traverse_if_needed(model); - if (type != Node::Type::Unknown) - return; - struct stat st; - auto full_path = this->full_path(model); - int rc = lstat(full_path.characters(), &st); - dbgprintf("lstat(%s) = %d\n", full_path.characters(), rc); - if (rc < 0) { - perror("lstat"); - return; - } - type = S_ISDIR(st.st_mode) ? Node::Type::Directory : Node::Type::File; + while (di.has_next()) { + String name = di.next_path(); + String child_path = String::format("%s/%s", full_path.characters(), name.characters()); + NonnullOwnPtr<Node> child = make<Node>(); + bool ok = child->fetch_data_using_lstat(child_path); + if (!ok) + continue; + if (model.m_mode == DirectoriesOnly && !S_ISDIR(child->mode)) + continue; + child->name = name; + child->parent = this; + total_size += child->size; + children.append(move(child)); } - String full_path(const GFileSystemModel& model) const - { - Vector<String, 32> lineage; - for (auto* ancestor = parent; ancestor; ancestor = ancestor->parent) { - lineage.append(ancestor->name); - } - StringBuilder builder; - builder.append(model.root_path()); - for (int i = lineage.size() - 1; i >= 0; --i) { - builder.append('/'); - builder.append(lineage[i]); - } + if (m_watch_fd >= 0) + return; + + m_watch_fd = watch_file(full_path.characters(), full_path.length()); + if (m_watch_fd < 0) { + perror("watch_file"); + return; + } + fcntl(m_watch_fd, F_SETFD, FD_CLOEXEC); + dbg() << "Watching " << full_path << " for changes, m_watch_fd = " << m_watch_fd; + m_notifier = CNotifier::construct(m_watch_fd, CNotifier::Event::Read); + m_notifier->on_ready_to_read = [this, &model] { + char buffer[32]; + int rc = read(m_notifier->fd(), buffer, sizeof(buffer)); + ASSERT(rc >= 0); + + has_traversed = false; + mode = 0; + children.clear(); + reify_if_needed(model); + const_cast<GFileSystemModel&>(model).did_update(); + }; +} + +void GFileSystemModel::Node::reify_if_needed(const GFileSystemModel& model) +{ + traverse_if_needed(model); + if (mode != 0) + return; + fetch_data_using_lstat(full_path(model)); +} + +String GFileSystemModel::Node::full_path(const GFileSystemModel& model) const +{ + Vector<String, 32> lineage; + for (auto* ancestor = parent; ancestor; ancestor = ancestor->parent) { + lineage.append(ancestor->name); + } + StringBuilder builder; + builder.append(model.root_path()); + for (int i = lineage.size() - 1; i >= 0; --i) { builder.append('/'); - builder.append(name); - return canonicalized_path(builder.to_string()); + builder.append(lineage[i]); } -}; + builder.append('/'); + builder.append(name); + return canonicalized_path(builder.to_string()); +} -GModelIndex GFileSystemModel::index(const StringView& path) const +GModelIndex GFileSystemModel::index(const StringView& path, int column) const { FileSystemPath canonical_path(path); const Node* node = m_root; if (canonical_path.string() == "/") - return m_root->index(*this); + return m_root->index(*this, column); for (int i = 0; i < canonical_path.parts().size(); ++i) { auto& part = canonical_path.parts()[i]; bool found = false; for (auto& child : node->children) { - if (child->name == part) { - child->reify_if_needed(*this); - node = child; + if (child.name == part) { + const_cast<Node&>(child).reify_if_needed(*this); + node = &child; found = true; if (i == canonical_path.parts().size() - 1) - return node->index(*this); + return child.index(*this, column); break; } } @@ -130,12 +145,10 @@ GModelIndex GFileSystemModel::index(const StringView& path) const return {}; } -String GFileSystemModel::path(const GModelIndex& index) const +String GFileSystemModel::full_path(const GModelIndex& index) const { - if (!index.is_valid()) - return {}; - auto& node = *(Node*)index.internal_data(); - node.reify_if_needed(*this); + auto& node = this->node(index); + const_cast<Node&>(node).reify_if_needed(*this); return node.full_path(*this); } @@ -143,9 +156,25 @@ GFileSystemModel::GFileSystemModel(const StringView& root_path, Mode mode) : m_root_path(canonicalized_path(root_path)) , m_mode(mode) { - m_open_folder_icon = GIcon::default_icon("filetype-folder-open"); - m_closed_folder_icon = GIcon::default_icon("filetype-folder"); + m_directory_icon = GIcon::default_icon("filetype-folder"); m_file_icon = GIcon::default_icon("filetype-unknown"); + m_symlink_icon = GIcon::default_icon("filetype-symlink"); + m_socket_icon = GIcon::default_icon("filetype-socket"); + m_executable_icon = GIcon::default_icon("filetype-executable"); + m_filetype_image_icon = GIcon::default_icon("filetype-image"); + m_filetype_sound_icon = GIcon::default_icon("filetype-sound"); + m_filetype_html_icon = GIcon::default_icon("filetype-html"); + + setpwent(); + while (auto* passwd = getpwent()) + m_user_names.set(passwd->pw_uid, passwd->pw_name); + endpwent(); + + setgrent(); + while (auto* group = getgrent()) + m_group_names.set(group->gr_gid, group->gr_name); + endgrent(); + update(); } @@ -153,73 +182,324 @@ GFileSystemModel::~GFileSystemModel() { } -void GFileSystemModel::update() +String GFileSystemModel::name_for_uid(uid_t uid) const { - cleanup(); + auto it = m_user_names.find(uid); + if (it == m_user_names.end()) + return String::number(uid); + return (*it).value; +} - m_root = new Node; - m_root->name = m_root_path; - m_root->reify_if_needed(*this); +String GFileSystemModel::name_for_gid(uid_t gid) const +{ + auto it = m_user_names.find(gid); + if (it == m_user_names.end()) + return String::number(gid); + return (*it).value; +} - did_update(); +static String permission_string(mode_t mode) +{ + StringBuilder builder; + if (S_ISDIR(mode)) + builder.append("d"); + else if (S_ISLNK(mode)) + builder.append("l"); + else if (S_ISBLK(mode)) + builder.append("b"); + else if (S_ISCHR(mode)) + builder.append("c"); + else if (S_ISFIFO(mode)) + builder.append("f"); + else if (S_ISSOCK(mode)) + builder.append("s"); + else if (S_ISREG(mode)) + builder.append("-"); + else + builder.append("?"); + + builder.appendf("%c%c%c%c%c%c%c%c", + mode & S_IRUSR ? 'r' : '-', + mode & S_IWUSR ? 'w' : '-', + mode & S_ISUID ? 's' : (mode & S_IXUSR ? 'x' : '-'), + mode & S_IRGRP ? 'r' : '-', + mode & S_IWGRP ? 'w' : '-', + mode & S_ISGID ? 's' : (mode & S_IXGRP ? 'x' : '-'), + mode & S_IROTH ? 'r' : '-', + mode & S_IWOTH ? 'w' : '-'); + + if (mode & S_ISVTX) + builder.append("t"); + else + builder.appendf("%c", mode & S_IXOTH ? 'x' : '-'); + return builder.to_string(); } -void GFileSystemModel::cleanup() +void GFileSystemModel::set_root_path(const StringView& root_path) { - if (m_root) { - m_root->cleanup(); - delete m_root; - m_root = nullptr; - } + m_root_path = canonicalized_path(root_path); + + if (on_root_path_change) + on_root_path_change(); + + update(); +} + +void GFileSystemModel::update() +{ + m_root = make<Node>(); + m_root->reify_if_needed(*this); + + did_update(); } int GFileSystemModel::row_count(const GModelIndex& index) const { - if (!index.is_valid()) - return 1; - auto& node = *(Node*)index.internal_data(); + Node& node = const_cast<Node&>(this->node(index)); node.reify_if_needed(*this); - if (node.type == Node::Type::Directory) + if (node.is_directory()) return node.children.size(); return 0; } +const GFileSystemModel::Node& GFileSystemModel::node(const GModelIndex& index) const +{ + if (!index.is_valid()) + return *m_root; + return *(Node*)index.internal_data(); +} + GModelIndex GFileSystemModel::index(int row, int column, const GModelIndex& parent) const { - if (!parent.is_valid()) - return create_index(row, column, m_root); - auto& node = *(Node*)parent.internal_data(); - return create_index(row, column, node.children[row]); + auto& node = this->node(parent); + const_cast<Node&>(node).reify_if_needed(*this); + if (row >= node.children.size()) + return {}; + return create_index(row, column, &node.children[row]); } GModelIndex GFileSystemModel::parent_index(const GModelIndex& index) const { if (!index.is_valid()) return {}; - auto& node = *(const Node*)index.internal_data(); + auto& node = this->node(index); if (!node.parent) { ASSERT(&node == m_root); return {}; } - return node.parent->index(*this); + return node.parent->index(*this, index.column()); } GVariant GFileSystemModel::data(const GModelIndex& index, Role role) const { - if (!index.is_valid()) + ASSERT(index.is_valid()); + auto& node = this->node(index); + + if (role == Role::Custom) { + // For GFileSystemModel, custom role means the full path. + ASSERT(index.column() == Column::Name); + return node.full_path(*this); + } + + if (role == Role::DragData) { + if (index.column() == Column::Name) { + StringBuilder builder; + builder.append("file://"); + builder.append(node.full_path(*this)); + return builder.to_string(); + } return {}; - auto& node = *(const Node*)index.internal_data(); - if (role == GModel::Role::Display) - return node.name; - if (role == GModel::Role::Icon) { - if (node.type == Node::Directory) - return m_closed_folder_icon; - return m_file_icon; + } + + if (role == Role::Sort) { + switch (index.column()) { + case Column::Icon: + return node.is_directory() ? 0 : 1; + case Column::Name: + return node.name; + case Column::Size: + return (int)node.size; + case Column::Owner: + return name_for_uid(node.uid); + case Column::Group: + return name_for_gid(node.gid); + case Column::Permissions: + return permission_string(node.mode); + case Column::ModificationTime: + return node.mtime; + case Column::Inode: + return (int)node.inode; + } + ASSERT_NOT_REACHED(); + } + + if (role == Role::Display) { + switch (index.column()) { + case Column::Icon: + return icon_for(node); + case Column::Name: + return node.name; + case Column::Size: + return (int)node.size; + case Column::Owner: + return name_for_uid(node.uid); + case Column::Group: + return name_for_gid(node.gid); + case Column::Permissions: + return permission_string(node.mode); + case Column::ModificationTime: + return timestamp_string(node.mtime); + case Column::Inode: + return (int)node.inode; + } + } + + if (role == Role::Icon) { + return icon_for(node); } return {}; } +GIcon GFileSystemModel::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 GFileSystemModel::icon_for(const Node& node) const +{ + if (node.name.to_lowercase().ends_with(".png")) { + if (!node.thumbnail) { + if (!const_cast<GFileSystemModel*>(this)->fetch_thumbnail_for(node)) + return m_filetype_image_icon; + } + return GIcon(m_filetype_image_icon.bitmap_for_size(16), *node.thumbnail); + } + + return icon_for_file(node.mode, node.name); +} + +static HashMap<String, RefPtr<GraphicsBitmap>> s_thumbnail_cache; + +static RefPtr<GraphicsBitmap> render_thumbnail(const StringView& path) +{ + auto png_bitmap = GraphicsBitmap::load_from_file(path); + if (!png_bitmap) + return nullptr; + auto thumbnail = GraphicsBitmap::create(png_bitmap->format(), { 32, 32 }); + Painter painter(*thumbnail); + painter.draw_scaled_bitmap(thumbnail->rect(), *png_bitmap, png_bitmap->rect()); + return thumbnail; +} + +bool GFileSystemModel::fetch_thumbnail_for(const Node& node) +{ + // See if we already have the thumbnail + // we're looking for in the cache. + auto path = node.full_path(*this); + auto it = s_thumbnail_cache.find(path); + if (it != s_thumbnail_cache.end()) { + if (!(*it).value) + return false; + node.thumbnail = (*it).value; + return true; + } + + // Otherwise, arrange to render the thumbnail + // in background and make it available later. + + s_thumbnail_cache.set(path, nullptr); + m_thumbnail_progress_total++; + + auto weak_this = make_weak_ptr(); + + LibThread::BackgroundAction<RefPtr<GraphicsBitmap>>::create( + [path] { + return render_thumbnail(path); + }, + + [this, path, weak_this](auto thumbnail) { + s_thumbnail_cache.set(path, move(thumbnail)); + + // The model was destroyed, no need to update + // progress or call any event handlers. + if (weak_this.is_null()) + return; + + m_thumbnail_progress++; + if (on_thumbnail_progress) + on_thumbnail_progress(m_thumbnail_progress, m_thumbnail_progress_total); + if (m_thumbnail_progress == m_thumbnail_progress_total) { + m_thumbnail_progress = 0; + m_thumbnail_progress_total = 0; + } + + did_update(); + }); + + return false; +} + int GFileSystemModel::column_count(const GModelIndex&) const { - return 1; + return Column::__Count; +} + +String GFileSystemModel::column_name(int column) const +{ + switch (column) { + case Column::Icon: + return ""; + case Column::Name: + return "Name"; + case Column::Size: + return "Size"; + case Column::Owner: + return "Owner"; + case Column::Group: + return "Group"; + case Column::Permissions: + return "Mode"; + case Column::ModificationTime: + return "Modified"; + case Column::Inode: + return "Inode"; + } + ASSERT_NOT_REACHED(); +} + +GModel::ColumnMetadata GFileSystemModel::column_metadata(int column) const +{ + switch (column) { + case Column::Icon: + return { 16, TextAlignment::Center, nullptr, GModel::ColumnMetadata::Sortable::False }; + case Column::Name: + return { 120, TextAlignment::CenterLeft }; + case Column::Size: + return { 80, TextAlignment::CenterRight }; + case Column::Owner: + return { 50, TextAlignment::CenterLeft }; + case Column::Group: + return { 50, TextAlignment::CenterLeft }; + case Column::ModificationTime: + return { 110, TextAlignment::CenterLeft }; + case Column::Permissions: + return { 65, TextAlignment::CenterLeft }; + case Column::Inode: + return { 60, TextAlignment::CenterRight }; + } + ASSERT_NOT_REACHED(); } diff --git a/Libraries/LibGUI/GFileSystemModel.h b/Libraries/LibGUI/GFileSystemModel.h index 8e4e481ec8..3919193017 100644 --- a/Libraries/LibGUI/GFileSystemModel.h +++ b/Libraries/LibGUI/GFileSystemModel.h @@ -1,9 +1,15 @@ #pragma once +#include <AK/HashMap.h> +#include <AK/NonnullOwnPtrVector.h> +#include <LibCore/CNotifier.h> #include <LibGUI/GModel.h> +#include <sys/stat.h> +#include <time.h> -class GFileSystemModel : public GModel { - friend class Node; +class GFileSystemModel : public GModel + , public Weakable<GFileSystemModel> { + friend struct Node; public: enum Mode { @@ -12,6 +18,53 @@ public: FilesAndDirectories }; + enum Column { + Icon = 0, + Name, + Size, + Owner, + Group, + Permissions, + ModificationTime, + Inode, + __Count, + }; + + struct Node { + ~Node() { close(m_watch_fd); } + + String name; + size_t size { 0 }; + mode_t mode { 0 }; + uid_t uid { 0 }; + gid_t gid { 0 }; + ino_t inode { 0 }; + time_t mtime { 0 }; + + size_t total_size { 0 }; + + mutable RefPtr<GraphicsBitmap> thumbnail; + bool is_directory() const { return S_ISDIR(mode); } + bool is_executable() const { return mode & S_IXUSR; } + + String full_path(const GFileSystemModel&) const; + + private: + friend class GFileSystemModel; + + Node* parent { nullptr }; + NonnullOwnPtrVector<Node> children; + bool has_traversed { false }; + + int m_watch_fd { -1 }; + RefPtr<CNotifier> m_notifier; + + GModelIndex index(const GFileSystemModel&, int column) const; + void traverse_if_needed(const GFileSystemModel&); + void reify_if_needed(const GFileSystemModel&); + bool fetch_data_using_lstat(const String& full_path); + }; + static NonnullRefPtr<GFileSystemModel> create(const StringView& root_path = "/", Mode mode = Mode::FilesAndDirectories) { return adopt(*new GFileSystemModel(root_path, mode)); @@ -19,27 +72,63 @@ public: virtual ~GFileSystemModel() override; String root_path() const { return m_root_path; } - String path(const GModelIndex&) const; - GModelIndex index(const StringView& path) const; + void set_root_path(const StringView&); + String full_path(const GModelIndex&) const; + GModelIndex index(const StringView& path, int column) const; + + const Node& node(const GModelIndex& index) const; + GIcon icon_for_file(const mode_t mode, const String& name) const; + + Function<void(int done, int total)> on_thumbnail_progress; + Function<void()> on_root_path_change; + virtual int tree_column() const { return Column::Name; } virtual int row_count(const GModelIndex& = GModelIndex()) const override; virtual int column_count(const GModelIndex& = GModelIndex()) const override; + virtual String column_name(int column) const override; + virtual ColumnMetadata column_metadata(int column) const override; virtual GVariant data(const GModelIndex&, Role = Role::Display) const override; virtual void update() override; virtual GModelIndex parent_index(const GModelIndex&) const override; virtual GModelIndex index(int row, int column = 0, const GModelIndex& parent = GModelIndex()) const override; + 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: GFileSystemModel(const StringView& root_path, Mode); + String name_for_uid(uid_t) const; + String name_for_gid(gid_t) const; + + HashMap<uid_t, String> m_user_names; + HashMap<gid_t, String> m_group_names; + + bool fetch_thumbnail_for(const Node& node); + GIcon icon_for(const Node& node) const; + String m_root_path; Mode m_mode { Invalid }; + OwnPtr<Node> m_root { nullptr }; - struct Node; - Node* m_root { nullptr }; - void cleanup(); - - GIcon m_open_folder_icon; - GIcon m_closed_folder_icon; + GIcon m_directory_icon; GIcon m_file_icon; + GIcon m_symlink_icon; + GIcon m_socket_icon; + GIcon m_executable_icon; + GIcon m_filetype_image_icon; + GIcon m_filetype_sound_icon; + GIcon m_filetype_html_icon; + + unsigned m_thumbnail_progress { 0 }; + unsigned m_thumbnail_progress_total { 0 }; }; diff --git a/Libraries/LibGUI/Makefile b/Libraries/LibGUI/Makefile index 7d71bf950a..51ab2ed7fa 100644 --- a/Libraries/LibGUI/Makefile +++ b/Libraries/LibGUI/Makefile @@ -41,7 +41,6 @@ OBJS = \ GTreeView.o \ GFileSystemModel.o \ GFilePicker.o \ - GDirectoryModel.o \ GSplitter.o \ GSpinBox.o \ GGroupBox.o \ |