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
Notes: sideshowbarker 2024-07-19 10:13:28 +09:00
12 changed files with 597 additions and 700 deletions

View file

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

View file

@ -1,7 +1,7 @@
#pragma once
#include <AK/Vector.h>
#include <LibGUI/GDirectoryModel.h>
#include <LibGUI/GFileSystemModel.h>
#include <LibGUI/GItemView.h>
#include <LibGUI/GStackWidget.h>
#include <LibGUI/GTableView.h>
@ -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<GDirectoryModel> m_model;
NonnullRefPtr<GFileSystemModel> m_model;
int m_path_history_position { 0 };
Vector<String> m_path_history;
void add_path_to_history(const StringView& path);

View file

@ -9,7 +9,7 @@
#include <stdio.h>
#include <unistd.h>
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<u32>(user_pw->pw_uid)) });
properties.append({ "Group:", String::format("%s (%lu)", group_pw->pw_name, static_cast<u32>(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<GraphicsBitmap*>(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()

View file

@ -4,7 +4,7 @@
#include <LibCore/CFile.h>
#include <LibGUI/GButton.h>
#include <LibGUI/GDialog.h>
#include <LibGUI/GDirectoryModel.h>
#include <LibGUI/GFileSystemModel.h>
#include <LibGUI/GLabel.h>
#include <LibGUI/GTextBox.h>
@ -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<GButton> m_apply_button;
RefPtr<GTextBox> m_name_box;
RefPtr<GLabel> m_icon;

View file

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

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 \