mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-21 20:15:17 +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
Notes:
sideshowbarker
2024-07-19 10:13:28 +09:00
Author: https://github.com/bugaevc Commit: https://github.com/SerenityOS/serenity/commit/fdeb91e000f Pull-request: https://github.com/SerenityOS/serenity/pull/1050
12 changed files with 597 additions and 700 deletions
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
Reference in a new issue