diff options
Diffstat (limited to 'Libraries/LibGUI/GFileSystemModel.cpp')
-rw-r--r-- | Libraries/LibGUI/GFileSystemModel.cpp | 538 |
1 files changed, 409 insertions, 129 deletions
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(); } |