From fdeb91e000f57407334c7cfdd11ccd7c97dd51c6 Mon Sep 17 00:00:00 2001 From: Sergey Bugaev Date: Fri, 10 Jan 2020 18:58:00 +0300 Subject: [PATCH] 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. --- Applications/FileManager/DirectoryView.cpp | 43 +- Applications/FileManager/DirectoryView.h | 10 +- Applications/FileManager/PropertiesDialog.cpp | 9 +- Applications/FileManager/PropertiesDialog.h | 6 +- Applications/FileManager/main.cpp | 44 +- Libraries/LibGUI/GDirectoryModel.cpp | 373 ------------ Libraries/LibGUI/GDirectoryModel.h | 106 ---- Libraries/LibGUI/GFilePicker.cpp | 42 +- Libraries/LibGUI/GFilePicker.h | 4 +- Libraries/LibGUI/GFileSystemModel.cpp | 550 +++++++++++++----- Libraries/LibGUI/GFileSystemModel.h | 109 +++- Libraries/LibGUI/Makefile | 1 - 12 files changed, 597 insertions(+), 700 deletions(-) delete mode 100644 Libraries/LibGUI/GDirectoryModel.cpp delete mode 100644 Libraries/LibGUI/GDirectoryModel.h diff --git a/Applications/FileManager/DirectoryView.cpp b/Applications/FileManager/DirectoryView.cpp index 9de5fa4b92..928a0a1e90 100644 --- a/Applications/FileManager/DirectoryView.cpp +++ b/Applications/FileManager/DirectoryView.cpp @@ -27,13 +27,13 @@ void DirectoryView::handle_activation(const GModelIndex& index) if (!index.is_valid()) return; dbgprintf("on activation: %d,%d, this=%p, m_model=%p\n", index.row(), index.column(), this, m_model.ptr()); - auto& entry = model().entry(index.row()); - auto path = canonicalized_path(String::format("%s/%s", model().path().characters(), entry.name.characters())); - if (entry.is_directory()) { + auto& node = model().node(index); + auto path = node.full_path(model()); + if (node.is_directory()) { open(path); return; } - if (entry.is_executable()) { + if (node.is_executable()) { if (fork() == 0) { int rc = execl(path.characters(), path.characters(), nullptr); if (rc < 0) @@ -83,7 +83,7 @@ void DirectoryView::handle_activation(const GModelIndex& index) DirectoryView::DirectoryView(GWidget* parent) : GStackWidget(parent) - , m_model(GDirectoryModel::create()) + , m_model(GFileSystemModel::create()) { set_active_widget(nullptr); m_item_view = GItemView::construct(this); @@ -92,22 +92,25 @@ DirectoryView::DirectoryView(GWidget* parent) m_table_view = GTableView::construct(this); m_table_view->set_model(GSortingProxyModel::create(m_model)); - m_table_view->model()->set_key_column_and_sort_order(GDirectoryModel::Column::Name, GSortOrder::Ascending); + m_table_view->model()->set_key_column_and_sort_order(GFileSystemModel::Column::Name, GSortOrder::Ascending); - m_item_view->set_model_column(GDirectoryModel::Column::Name); + m_item_view->set_model_column(GFileSystemModel::Column::Name); - m_model->on_path_change = [this] { + m_model->on_root_path_change = [this] { m_table_view->selection().clear(); m_item_view->selection().clear(); if (on_path_change) - on_path_change(model().path()); + on_path_change(model().root_path()); }; // NOTE: We're using the on_update hook on the GSortingProxyModel here instead of - // the GDirectoryModel's hook. This is because GSortingProxyModel has already - // installed an on_update hook on the GDirectoryModel internally. + // the GFileSystemModel's hook. This is because GSortingProxyModel has already + // installed an on_update hook on the GFileSystemModel internally. // FIXME: This is an unfortunate design. We should come up with something better. m_table_view->model()->on_update = [this] { + for_each_view_implementation([](auto& view) { + view.selection().clear(); + }); update_statusbar(); }; @@ -180,7 +183,7 @@ void DirectoryView::add_path_to_history(const StringView& path) void DirectoryView::open(const StringView& path) { add_path_to_history(path); - model().open(path); + model().set_root_path(path); } void DirectoryView::set_status_message(const StringView& message) @@ -191,9 +194,9 @@ void DirectoryView::set_status_message(const StringView& message) void DirectoryView::open_parent_directory() { - auto path = String::format("%s/..", model().path().characters()); + auto path = String::format("%s/..", model().root_path().characters()); add_path_to_history(path); - model().open(path); + model().set_root_path(path); } void DirectoryView::refresh() @@ -205,24 +208,25 @@ void DirectoryView::open_previous_directory() { if (m_path_history_position > 0) { m_path_history_position--; - model().open(m_path_history[m_path_history_position]); + model().set_root_path(m_path_history[m_path_history_position]); } } void DirectoryView::open_next_directory() { if (m_path_history_position < m_path_history.size() - 1) { m_path_history_position++; - model().open(m_path_history[m_path_history_position]); + model().set_root_path(m_path_history[m_path_history_position]); } } void DirectoryView::update_statusbar() { + size_t total_size = model().node({}).total_size; if (current_view().selection().is_empty()) { set_status_message(String::format("%d item%s (%s)", model().row_count(), model().row_count() != 1 ? "s" : "", - human_readable_size(model().bytes_in_files()).characters())); + human_readable_size(total_size).characters())); return; } @@ -230,8 +234,9 @@ void DirectoryView::update_statusbar() size_t selected_byte_count = 0; current_view().selection().for_each_index([&](auto& index) { - auto size_index = current_view().model()->index(index.row(), GDirectoryModel::Column::Size); - auto file_size = current_view().model()->data(size_index).to_int(); + auto& model = *current_view().model(); + auto size_index = model.sibling(index.row(), GFileSystemModel::Column::Size, model.parent_index(index)); + auto file_size = model.data(size_index).to_int(); selected_byte_count += file_size; }); diff --git a/Applications/FileManager/DirectoryView.h b/Applications/FileManager/DirectoryView.h index e9feeca8ba..2e314f8e60 100644 --- a/Applications/FileManager/DirectoryView.h +++ b/Applications/FileManager/DirectoryView.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include #include @@ -13,7 +13,7 @@ public: virtual ~DirectoryView() override; void open(const StringView& path); - String path() const { return model().path(); } + String path() const { return model().root_path(); } void open_parent_directory(); void open_previous_directory(); void open_next_directory(); @@ -55,11 +55,11 @@ public: callback(*m_item_view); } - GDirectoryModel& model() { return *m_model; } + GFileSystemModel& model() { return *m_model; } private: explicit DirectoryView(GWidget* parent); - const GDirectoryModel& model() const { return *m_model; } + const GFileSystemModel& model() const { return *m_model; } void handle_activation(const GModelIndex&); @@ -68,7 +68,7 @@ private: ViewMode m_view_mode { Invalid }; - NonnullRefPtr m_model; + NonnullRefPtr m_model; int m_path_history_position { 0 }; Vector m_path_history; void add_path_to_history(const StringView& path); diff --git a/Applications/FileManager/PropertiesDialog.cpp b/Applications/FileManager/PropertiesDialog.cpp index bde03b9e53..9b7a67029b 100644 --- a/Applications/FileManager/PropertiesDialog.cpp +++ b/Applications/FileManager/PropertiesDialog.cpp @@ -9,7 +9,7 @@ #include #include -PropertiesDialog::PropertiesDialog(GDirectoryModel& model, String path, bool disable_rename, CObject* parent) +PropertiesDialog::PropertiesDialog(GFileSystemModel& model, String path, bool disable_rename, CObject* parent) : GDialog(parent) , m_model(model) { @@ -92,8 +92,8 @@ PropertiesDialog::PropertiesDialog(GDirectoryModel& model, String path, bool dis 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) }); + properties.append({ "Created at:", GFileSystemModel::timestamp_string(st.st_ctime) }); + properties.append({ "Last modified:", GFileSystemModel::timestamp_string(st.st_mtime) }); make_property_value_pairs(properties, general_tab); @@ -127,7 +127,6 @@ 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())); } @@ -146,7 +145,7 @@ void PropertiesDialog::permission_changed(mode_t mask, bool set) String PropertiesDialog::make_full_path(String name) { - return String::format("%s/%s", m_model.path().characters(), name.characters()); + return String::format("%s/%s", m_model.root_path().characters(), name.characters()); } bool PropertiesDialog::apply_changes() diff --git a/Applications/FileManager/PropertiesDialog.h b/Applications/FileManager/PropertiesDialog.h index d4f3e6d709..7561eee805 100644 --- a/Applications/FileManager/PropertiesDialog.h +++ b/Applications/FileManager/PropertiesDialog.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -14,7 +14,7 @@ public: virtual ~PropertiesDialog() override; private: - explicit PropertiesDialog(GDirectoryModel&, String, bool disable_rename, CObject* parent = nullptr); + PropertiesDialog(GFileSystemModel&, String, bool disable_rename, CObject* parent = nullptr); struct PropertyValuePair { String property; @@ -58,7 +58,7 @@ private: void update(); String make_full_path(String name); - GDirectoryModel& m_model; + GFileSystemModel& m_model; RefPtr m_apply_button; RefPtr m_name_box; RefPtr m_icon; diff --git a/Applications/FileManager/main.cpp b/Applications/FileManager/main.cpp index 118d6ae6ef..d3f64ba9f8 100644 --- a/Applications/FileManager/main.cpp +++ b/Applications/FileManager/main.cpp @@ -70,10 +70,17 @@ int main(int argc, char** argv) auto splitter = GSplitter::construct(Orientation::Horizontal, widget); auto tree_view = GTreeView::construct(splitter); - auto file_system_model = GFileSystemModel::create("/", GFileSystemModel::Mode::DirectoriesOnly); - tree_view->set_model(file_system_model); + auto directories_model = GFileSystemModel::create("/", GFileSystemModel::Mode::DirectoriesOnly); + tree_view->set_model(directories_model); + tree_view->set_column_hidden(GFileSystemModel::Column::Icon, true); + tree_view->set_column_hidden(GFileSystemModel::Column::Size, true); + tree_view->set_column_hidden(GFileSystemModel::Column::Owner, true); + tree_view->set_column_hidden(GFileSystemModel::Column::Group, true); + tree_view->set_column_hidden(GFileSystemModel::Column::Permissions, true); + tree_view->set_column_hidden(GFileSystemModel::Column::ModificationTime, true); + tree_view->set_column_hidden(GFileSystemModel::Column::Inode, true); tree_view->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); - tree_view->set_preferred_size(200, 0); + tree_view->set_preferred_size(150, 0); auto directory_view = DirectoryView::construct(splitter); auto statusbar = GStatusBar::construct(widget); @@ -91,7 +98,7 @@ int main(int argc, char** argv) }; auto refresh_tree_view = [&] { - file_system_model->update(); + directories_model->update(); auto current_path = directory_view->path(); @@ -100,17 +107,13 @@ int main(int argc, char** argv) while (lstat(current_path.characters(), &st) != 0) { directory_view->open_parent_directory(); current_path = directory_view->path(); - if (current_path == file_system_model->root_path()) { + if (current_path == directories_model->root_path()) { break; } } - // not exactly sure why i have to reselect the root node first, but the index() fails if I dont - auto root_index = file_system_model->index(file_system_model->root_path()); - tree_view->selection().set(root_index); - - // reselect the existing folder in the tree - auto new_index = file_system_model->index(current_path); + // Reselect the existing folder in the tree. + auto new_index = directories_model->index(current_path, GFileSystemModel::Column::Name); tree_view->selection().set(new_index); tree_view->scroll_into_view(new_index, Orientation::Vertical); tree_view->update(); @@ -175,7 +178,8 @@ int main(int argc, char** argv) auto& view = directory_view->current_view(); auto& model = *view.model(); view.selection().for_each_index([&](const GModelIndex& index) { - auto name_index = model.index(index.row(), GDirectoryModel::Column::Name); + auto parent_index = model.parent_index(index); + auto name_index = model.index(index.row(), GFileSystemModel::Column::Name, parent_index); auto path = model.data(name_index, GModel::Role::Custom).to_string(); paths.append(path); }); @@ -186,7 +190,7 @@ int main(int argc, char** argv) Vector paths; auto& view = tree_view; view->selection().for_each_index([&](const GModelIndex& index) { - paths.append(file_system_model->path(index)); + paths.append(directories_model->full_path(index)); }); return paths; }; @@ -254,9 +258,10 @@ int main(int argc, char** argv) path = directory_view->path(); selected = selected_file_paths(); } else { - path = file_system_model->path(tree_view->selection().first()); + path = directories_model->full_path(tree_view->selection().first()); selected = tree_view_selected_file_paths(); } + RefPtr properties; if (selected.is_empty()) { properties = PropertiesDialog::construct(model, path, true, window); @@ -413,7 +418,7 @@ int main(int argc, char** argv) directory_view->on_path_change = [&](const String& new_path) { window->set_title(String::format("File Manager: %s", new_path.characters())); location_textbox->set_text(new_path); - auto new_index = file_system_model->index(new_path); + auto new_index = directories_model->index(new_path, GFileSystemModel::Column::Name); if (new_index.is_valid()) { tree_view->selection().set(new_index); tree_view->scroll_into_view(new_index, Orientation::Vertical); @@ -482,20 +487,19 @@ int main(int argc, char** argv) directory_view->on_context_menu_request = [&](const GAbstractView&, const GModelIndex& index, const GContextMenuEvent& event) { if (index.is_valid()) { - auto& entry = directory_view->model().entry(index.row()); + auto& node = directory_view->model().node(index); - if (entry.is_directory()) { + if (node.is_directory()) directory_context_menu->popup(event.screen_position()); - } else { + else file_context_menu->popup(event.screen_position()); - } } else { directory_view_context_menu->popup(event.screen_position()); } }; tree_view->on_selection_change = [&] { - auto path = file_system_model->path(tree_view->selection().first()); + auto path = directories_model->full_path(tree_view->selection().first()); if (directory_view->path() == path) return; directory_view->open(path); 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static HashMap> s_thumbnail_cache; - -static RefPtr 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>::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(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 -#include -#include -#include -#include - -class GDirectoryModel final : public GModel - , public Weakable { -public: - static NonnullRefPtr 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 on_thumbnail_progress; - Function 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 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 m_files; - Vector 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 m_user_names; - HashMap m_group_names; - - RefPtr 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 #include #include -#include #include +#include #include #include #include @@ -48,7 +48,7 @@ Optional 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 #include -class GDirectoryModel; +class GFileSystemModel; class GLabel; class GTextBox; @@ -44,7 +44,7 @@ private: } RefPtr m_view; - NonnullRefPtr m_model; + NonnullRefPtr m_model; FileSystemPath m_selected_file; RefPtr 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 #include #include +#include #include +#include +#include #include +#include +#include #include #include #include -struct GFileSystemModel::Node { - String name; - Node* parent { nullptr }; - Vector children; - enum Type { - Unknown, - Directory, - File - }; - Type type { Unknown }; +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(this)); + } + ASSERT_NOT_REACHED(); +} - bool has_traversed { false }; - - GModelIndex index(const GFileSystemModel& model) const - { - if (!parent) - return model.create_index(0, 0, const_cast(this)); - for (int row = 0; row < parent->children.size(); ++row) { - if (parent->children[row] == this) - return model.create_index(row, 0, const_cast(this)); - } - ASSERT_NOT_REACHED(); +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 cleanup() - { - for (auto& child: children) { - child->cleanup(); - delete child; - } + size = st.st_size; + mode = st.st_mode; + uid = st.st_uid; + gid = st.st_gid; + inode = st.st_ino; + mtime = st.st_mtime; + + return true; +} + +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; + } + + while (di.has_next()) { + String name = di.next_path(); + String child_path = String::format("%s/%s", full_path.characters(), name.characters()); + NonnullOwnPtr child = make(); + 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)); + } + + 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(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 lineage; + for (auto* ancestor = parent; ancestor; ancestor = ancestor->parent) { + lineage.append(ancestor->name); } - - void traverse_if_needed(const GFileSystemModel& model) - { - if (type != Node::Directory || has_traversed) - return; - has_traversed = true; - - 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; - } - - 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 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; - } - - String full_path(const GFileSystemModel& model) const - { - Vector 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]); - } + 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(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).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() { } +String GFileSystemModel::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 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; +} + +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::set_root_path(const StringView& root_path) +{ + m_root_path = canonicalized_path(root_path); + + if (on_root_path_change) + on_root_path_change(); + + update(); +} + void GFileSystemModel::update() { - cleanup(); - - m_root = new Node; - m_root->name = m_root_path; + m_root = make(); m_root->reify_if_needed(*this); did_update(); } -void GFileSystemModel::cleanup() -{ - if (m_root) { - m_root->cleanup(); - delete m_root; - m_root = nullptr; - } -} - int GFileSystemModel::row_count(const GModelIndex& index) const { - if (!index.is_valid()) - return 1; - auto& node = *(Node*)index.internal_data(); + Node& node = const_cast(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).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(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> s_thumbnail_cache; + +static RefPtr 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>::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 +#include +#include #include +#include +#include -class GFileSystemModel : public GModel { - friend class Node; +class GFileSystemModel : public GModel + , public Weakable { + 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 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 children; + bool has_traversed { false }; + + int m_watch_fd { -1 }; + RefPtr 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 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 on_thumbnail_progress; + Function 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 m_user_names; + HashMap 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 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 \