1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 16:07:47 +00:00

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.
This commit is contained in:
Sergey Bugaev 2020-01-10 18:58:00 +03:00 committed by Andreas Kling
parent 0f18a16e2c
commit fdeb91e000
12 changed files with 597 additions and 700 deletions

View file

@ -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();
}

View file

@ -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(&timestamp);
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 };
};

View file

@ -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);

View file

@ -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;

View file

@ -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 };
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();
}
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();
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<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));
}
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);
}
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<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]);
}
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()
{
}
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<Node>();
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<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();
}

View file

@ -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(&timestamp);
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 };
};

View file

@ -41,7 +41,6 @@ OBJS = \
GTreeView.o \
GFileSystemModel.o \
GFilePicker.o \
GDirectoryModel.o \
GSplitter.o \
GSpinBox.o \
GGroupBox.o \