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:
parent
0f18a16e2c
commit
fdeb91e000
12 changed files with 597 additions and 700 deletions
|
@ -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();
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibCore/CNotifier.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
class GDirectoryModel final : public GModel
|
||||
, public Weakable<GDirectoryModel> {
|
||||
public:
|
||||
static NonnullRefPtr<GDirectoryModel> create() { return adopt(*new GDirectoryModel); }
|
||||
virtual ~GDirectoryModel() override;
|
||||
|
||||
enum Column {
|
||||
Icon = 0,
|
||||
Name,
|
||||
Size,
|
||||
Owner,
|
||||
Group,
|
||||
Permissions,
|
||||
ModificationTime,
|
||||
Inode,
|
||||
__Count,
|
||||
};
|
||||
|
||||
virtual int row_count(const GModelIndex& = GModelIndex()) const override;
|
||||
virtual int column_count(const GModelIndex& = GModelIndex()) const override;
|
||||
virtual String column_name(int column) const override;
|
||||
virtual ColumnMetadata column_metadata(int column) const override;
|
||||
virtual GVariant data(const GModelIndex&, Role = Role::Display) const override;
|
||||
virtual void update() override;
|
||||
|
||||
String path() const { return m_path; }
|
||||
void open(const StringView& path);
|
||||
size_t bytes_in_files() const { return m_bytes_in_files; }
|
||||
|
||||
Function<void(int done, int total)> on_thumbnail_progress;
|
||||
Function<void()> on_path_change;
|
||||
|
||||
struct Entry {
|
||||
String name;
|
||||
size_t size { 0 };
|
||||
mode_t mode { 0 };
|
||||
uid_t uid { 0 };
|
||||
uid_t gid { 0 };
|
||||
ino_t inode { 0 };
|
||||
time_t mtime { 0 };
|
||||
mutable RefPtr<GraphicsBitmap> thumbnail;
|
||||
bool is_directory() const { return S_ISDIR(mode); }
|
||||
bool is_executable() const { return mode & S_IXUSR; }
|
||||
String full_path(const GDirectoryModel& model) const { return String::format("%s/%s", model.path().characters(), name.characters()); }
|
||||
};
|
||||
|
||||
const Entry& entry(int index) const
|
||||
{
|
||||
if (index < m_directories.size())
|
||||
return m_directories[index];
|
||||
return m_files[index - m_directories.size()];
|
||||
}
|
||||
|
||||
GIcon icon_for_file(const mode_t mode, const String name) const;
|
||||
|
||||
static String timestamp_string(time_t timestamp)
|
||||
{
|
||||
auto* tm = localtime(×tamp);
|
||||
return String::format("%4u-%02u-%02u %02u:%02u:%02u",
|
||||
tm->tm_year + 1900,
|
||||
tm->tm_mon + 1,
|
||||
tm->tm_mday,
|
||||
tm->tm_hour,
|
||||
tm->tm_min,
|
||||
tm->tm_sec);
|
||||
}
|
||||
|
||||
private:
|
||||
GDirectoryModel();
|
||||
|
||||
String name_for_uid(uid_t) const;
|
||||
String name_for_gid(gid_t) const;
|
||||
|
||||
bool fetch_thumbnail_for(const Entry& entry);
|
||||
GIcon icon_for(const Entry& entry) const;
|
||||
|
||||
String m_path;
|
||||
Vector<Entry> m_files;
|
||||
Vector<Entry> m_directories;
|
||||
size_t m_bytes_in_files;
|
||||
|
||||
GIcon m_directory_icon;
|
||||
GIcon m_file_icon;
|
||||
GIcon m_symlink_icon;
|
||||
GIcon m_socket_icon;
|
||||
GIcon m_executable_icon;
|
||||
GIcon m_filetype_image_icon;
|
||||
GIcon m_filetype_sound_icon;
|
||||
GIcon m_filetype_html_icon;
|
||||
|
||||
HashMap<uid_t, String> m_user_names;
|
||||
HashMap<gid_t, String> m_group_names;
|
||||
|
||||
RefPtr<CNotifier> m_notifier;
|
||||
|
||||
unsigned m_thumbnail_progress { 0 };
|
||||
unsigned m_thumbnail_progress_total { 0 };
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/NonnullOwnPtrVector.h>
|
||||
#include <LibCore/CNotifier.h>
|
||||
#include <LibGUI/GModel.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
class GFileSystemModel : public GModel {
|
||||
friend class Node;
|
||||
class GFileSystemModel : public GModel
|
||||
, public Weakable<GFileSystemModel> {
|
||||
friend struct Node;
|
||||
|
||||
public:
|
||||
enum Mode {
|
||||
|
@ -12,6 +18,53 @@ public:
|
|||
FilesAndDirectories
|
||||
};
|
||||
|
||||
enum Column {
|
||||
Icon = 0,
|
||||
Name,
|
||||
Size,
|
||||
Owner,
|
||||
Group,
|
||||
Permissions,
|
||||
ModificationTime,
|
||||
Inode,
|
||||
__Count,
|
||||
};
|
||||
|
||||
struct Node {
|
||||
~Node() { close(m_watch_fd); }
|
||||
|
||||
String name;
|
||||
size_t size { 0 };
|
||||
mode_t mode { 0 };
|
||||
uid_t uid { 0 };
|
||||
gid_t gid { 0 };
|
||||
ino_t inode { 0 };
|
||||
time_t mtime { 0 };
|
||||
|
||||
size_t total_size { 0 };
|
||||
|
||||
mutable RefPtr<GraphicsBitmap> thumbnail;
|
||||
bool is_directory() const { return S_ISDIR(mode); }
|
||||
bool is_executable() const { return mode & S_IXUSR; }
|
||||
|
||||
String full_path(const GFileSystemModel&) const;
|
||||
|
||||
private:
|
||||
friend class GFileSystemModel;
|
||||
|
||||
Node* parent { nullptr };
|
||||
NonnullOwnPtrVector<Node> children;
|
||||
bool has_traversed { false };
|
||||
|
||||
int m_watch_fd { -1 };
|
||||
RefPtr<CNotifier> m_notifier;
|
||||
|
||||
GModelIndex index(const GFileSystemModel&, int column) const;
|
||||
void traverse_if_needed(const GFileSystemModel&);
|
||||
void reify_if_needed(const GFileSystemModel&);
|
||||
bool fetch_data_using_lstat(const String& full_path);
|
||||
};
|
||||
|
||||
static NonnullRefPtr<GFileSystemModel> create(const StringView& root_path = "/", Mode mode = Mode::FilesAndDirectories)
|
||||
{
|
||||
return adopt(*new GFileSystemModel(root_path, mode));
|
||||
|
@ -19,27 +72,63 @@ public:
|
|||
virtual ~GFileSystemModel() override;
|
||||
|
||||
String root_path() const { return m_root_path; }
|
||||
String path(const GModelIndex&) const;
|
||||
GModelIndex index(const StringView& path) const;
|
||||
void set_root_path(const StringView&);
|
||||
String full_path(const GModelIndex&) const;
|
||||
GModelIndex index(const StringView& path, int column) const;
|
||||
|
||||
const Node& node(const GModelIndex& index) const;
|
||||
GIcon icon_for_file(const mode_t mode, const String& name) const;
|
||||
|
||||
Function<void(int done, int total)> on_thumbnail_progress;
|
||||
Function<void()> on_root_path_change;
|
||||
|
||||
virtual int tree_column() const { return Column::Name; }
|
||||
virtual int row_count(const GModelIndex& = GModelIndex()) const override;
|
||||
virtual int column_count(const GModelIndex& = GModelIndex()) const override;
|
||||
virtual String column_name(int column) const override;
|
||||
virtual ColumnMetadata column_metadata(int column) const override;
|
||||
virtual GVariant data(const GModelIndex&, Role = Role::Display) const override;
|
||||
virtual void update() override;
|
||||
virtual GModelIndex parent_index(const GModelIndex&) const override;
|
||||
virtual GModelIndex index(int row, int column = 0, const GModelIndex& parent = GModelIndex()) const override;
|
||||
|
||||
static String timestamp_string(time_t timestamp)
|
||||
{
|
||||
auto* tm = localtime(×tamp);
|
||||
return String::format("%4u-%02u-%02u %02u:%02u:%02u",
|
||||
tm->tm_year + 1900,
|
||||
tm->tm_mon + 1,
|
||||
tm->tm_mday,
|
||||
tm->tm_hour,
|
||||
tm->tm_min,
|
||||
tm->tm_sec);
|
||||
}
|
||||
|
||||
private:
|
||||
GFileSystemModel(const StringView& root_path, Mode);
|
||||
|
||||
String name_for_uid(uid_t) const;
|
||||
String name_for_gid(gid_t) const;
|
||||
|
||||
HashMap<uid_t, String> m_user_names;
|
||||
HashMap<gid_t, String> m_group_names;
|
||||
|
||||
bool fetch_thumbnail_for(const Node& node);
|
||||
GIcon icon_for(const Node& node) const;
|
||||
|
||||
String m_root_path;
|
||||
Mode m_mode { Invalid };
|
||||
OwnPtr<Node> m_root { nullptr };
|
||||
|
||||
struct Node;
|
||||
Node* m_root { nullptr };
|
||||
void cleanup();
|
||||
|
||||
GIcon m_open_folder_icon;
|
||||
GIcon m_closed_folder_icon;
|
||||
GIcon m_directory_icon;
|
||||
GIcon m_file_icon;
|
||||
GIcon m_symlink_icon;
|
||||
GIcon m_socket_icon;
|
||||
GIcon m_executable_icon;
|
||||
GIcon m_filetype_image_icon;
|
||||
GIcon m_filetype_sound_icon;
|
||||
GIcon m_filetype_html_icon;
|
||||
|
||||
unsigned m_thumbnail_progress { 0 };
|
||||
unsigned m_thumbnail_progress_total { 0 };
|
||||
};
|
||||
|
|
|
@ -41,7 +41,6 @@ OBJS = \
|
|||
GTreeView.o \
|
||||
GFileSystemModel.o \
|
||||
GFilePicker.o \
|
||||
GDirectoryModel.o \
|
||||
GSplitter.o \
|
||||
GSpinBox.o \
|
||||
GGroupBox.o \
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue