FileManager: Port to GTableModel/GTableView.

Replace the custom DirectoryView widget with a GTableView subclass.
This was pleasantly straightforward and it's so cool seeing the huge
increase in app quality from GTableView. :^)
This commit is contained in:
Andreas Kling 2019-03-01 13:54:28 +01:00
parent b5dcad932e
commit ac8fb5da4c
Notes: sideshowbarker 2024-07-19 15:34:37 +09:00
8 changed files with 308 additions and 236 deletions

View File

@ -0,0 +1,180 @@
#include "DirectoryTableModel.h"
#include <dirent.h>
#include <stdio.h>
#include <unistd.h>
#include <AK/FileSystemPath.h>
#include <AK/StringBuilder.h>
DirectoryTableModel::DirectoryTableModel()
{
m_directory_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/folder16.rgb", { 16, 16 });
m_file_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/file16.rgb", { 16, 16 });
m_symlink_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/link16.rgb", { 16, 16 });
m_socket_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/socket16.rgb", { 16, 16 });
}
DirectoryTableModel::~DirectoryTableModel()
{
}
int DirectoryTableModel::row_count() const
{
return m_directories.size() + m_files.size();
}
int DirectoryTableModel::column_count() const
{
return Column::__Count;
}
String DirectoryTableModel::column_name(int column) const
{
switch (column) {
case Column::Icon: return "";
case Column::Name: return "Name";
case Column::Size: return "Size";
case Column::UID: return "UID";
case Column::GID: return "GID";
case Column::Permissions: return "Mode";
case Column::Inode: return "Inode";
}
ASSERT_NOT_REACHED();
}
GTableModel::ColumnMetadata DirectoryTableModel::column_metadata(int column) const
{
switch (column) {
case Column::Icon: return { 16, TextAlignment::Center };
case Column::Name: return { 120, TextAlignment::CenterLeft };
case Column::Size: return { 80, TextAlignment::CenterRight };
case Column::UID: return { 80, TextAlignment::CenterRight };
case Column::GID: return { 80, TextAlignment::CenterRight };
case Column::Permissions: return { 100, TextAlignment::CenterLeft };
case Column::Inode: return { 80, TextAlignment::CenterRight };
}
ASSERT_NOT_REACHED();
}
const GraphicsBitmap& DirectoryTableModel::icon_for(const Entry& entry) const
{
if (S_ISDIR(entry.mode))
return *m_directory_icon;
if (S_ISLNK(entry.mode))
return *m_symlink_icon;
if (S_ISSOCK(entry.mode))
return *m_socket_icon;
return *m_file_icon;
}
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();
}
GVariant DirectoryTableModel::data(int row, int column) const
{
auto& entry = this->entry(row);
switch (column) {
case Column::Icon: return icon_for(entry);
case Column::Name: return entry.name;
case Column::Size: return (int)entry.size;
case Column::UID: return (int)entry.uid;
case Column::GID: return (int)entry.gid;
case Column::Permissions: return permission_string(entry.mode);
case Column::Inode: return (int)entry.inode;
}
ASSERT_NOT_REACHED();
}
void DirectoryTableModel::update()
{
DIR* dirp = opendir(m_path.characters());
if (!dirp) {
perror("opendir");
exit(1);
}
m_directories.clear();
m_files.clear();
m_bytes_in_files = 0;
while (auto* de = readdir(dirp)) {
Entry entry;
entry.name = de->d_name;
struct stat st;
int rc = lstat(String::format("%s/%s", m_path.characters(), de->d_name).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;
auto& entries = S_ISDIR(st.st_mode) ? m_directories : m_files;
entries.append(move(entry));
if (S_ISREG(entry.mode))
m_bytes_in_files += st.st_size;
}
closedir(dirp);
did_update();
}
void DirectoryTableModel::open(const String& path)
{
if (m_path == path)
return;
DIR* dirp = opendir(path.characters());
if (!dirp)
return;
closedir(dirp);
m_path = path;
update();
set_selected_index({ 0, 0 });
}
void DirectoryTableModel::activate(const GModelIndex& index)
{
auto& entry = this->entry(index.row());
if (entry.is_directory()) {
FileSystemPath new_path(String::format("%s/%s", m_path.characters(), entry.name.characters()));
open(new_path.string());
}
}

View File

@ -0,0 +1,62 @@
#pragma once
#include <LibGUI/GTableModel.h>
#include <sys/stat.h>
class DirectoryTableModel final : public GTableModel {
public:
DirectoryTableModel();
virtual ~DirectoryTableModel() override;
enum Column {
Icon = 0,
Name,
Size,
UID,
GID,
Permissions,
Inode,
__Count,
};
virtual int row_count() const override;
virtual int column_count() const override;
virtual String column_name(int column) const override;
virtual ColumnMetadata column_metadata(int column) const override;
virtual GVariant data(int row, int column) const override;
virtual void update() override;
virtual void activate(const GModelIndex&) override;
String path() const { return m_path; }
void open(const String& path);
size_t bytes_in_files() const { return m_bytes_in_files; }
private:
struct Entry {
String name;
size_t size { 0 };
mode_t mode { 0 };
uid_t uid { 0 };
uid_t gid { 0 };
ino_t inode { 0 };
bool is_directory() const { return S_ISDIR(mode); }
};
const Entry& entry(int index) const
{
if (index < m_directories.size())
return m_directories[index];
return m_files[index - m_directories.size()];
}
const GraphicsBitmap& icon_for(const Entry& entry) const;
String m_path;
Vector<Entry> m_files;
Vector<Entry> m_directories;
size_t m_bytes_in_files;
RetainPtr<GraphicsBitmap> m_directory_icon;
RetainPtr<GraphicsBitmap> m_file_icon;
RetainPtr<GraphicsBitmap> m_symlink_icon;
RetainPtr<GraphicsBitmap> m_socket_icon;
};

View File

@ -0,0 +1,33 @@
#include "DirectoryTableView.h"
DirectoryTableView::DirectoryTableView(GWidget* parent)
: GTableView(parent)
{
set_model(make<DirectoryTableModel>());
}
DirectoryTableView::~DirectoryTableView()
{
}
void DirectoryTableView::open(const String& path)
{
model().open(path);
}
void DirectoryTableView::model_notification(const GModelNotification& notification)
{
if (notification.type() == GModelNotification::Type::ModelUpdated) {
set_status_message(String::format("%d item%s (%u byte%s)",
model().row_count(),
model().row_count() != 1 ? "s" : "",
model().bytes_in_files(),
model().bytes_in_files() != 1 ? "s" : ""));
}
}
void DirectoryTableView::set_status_message(const String& message)
{
if (on_status_message)
on_status_message(message);
}

View File

@ -0,0 +1,25 @@
#pragma once
#include <LibGUI/GTableView.h>
#include <sys/stat.h>
#include "DirectoryTableModel.h"
class DirectoryTableView final : public GTableView {
public:
explicit DirectoryTableView(GWidget* parent);
virtual ~DirectoryTableView() override;
void open(const String& path);
String path() const { return model().path(); }
Function<void(const String&)> on_path_change;
Function<void(String)> on_status_message;
private:
virtual void model_notification(const GModelNotification&) override;
DirectoryTableModel& model() { return static_cast<DirectoryTableModel&>(*GTableView::model()); }
const DirectoryTableModel& model() const { return static_cast<const DirectoryTableModel&>(*GTableView::model()); }
void set_status_message(const String&);
};

View File

@ -1,171 +0,0 @@
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <SharedGraphics/GraphicsBitmap.h>
#include <SharedGraphics/Painter.h>
#include <LibGUI/GScrollBar.h>
#include <AK/FileSystemPath.h>
#include "DirectoryView.h"
DirectoryView::DirectoryView(GWidget* parent)
: GWidget(parent)
{
m_directory_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/folder16.rgb", { 16, 16 });
m_file_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/file16.rgb", { 16, 16 });
m_symlink_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/link16.rgb", { 16, 16 });
m_socket_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/socket16.rgb", { 16, 16 });
m_scrollbar = new GScrollBar(Orientation::Vertical, this);
m_scrollbar->set_step(4);
m_scrollbar->set_big_step(30);
m_scrollbar->on_change = [this] (int) {
update();
};
}
DirectoryView::~DirectoryView()
{
}
void DirectoryView::resize_event(GResizeEvent& event)
{
m_scrollbar->set_relative_rect(event.size().width() - m_scrollbar->preferred_size().width(), 0, m_scrollbar->preferred_size().width(), event.size().height());
}
void DirectoryView::open(const String& path)
{
if (m_path == path)
return;
DIR* dirp = opendir(path.characters());
if (!dirp)
return;
closedir(dirp);
m_path = path;
reload();
if (on_path_change)
on_path_change(m_path);
update();
}
void DirectoryView::reload()
{
DIR* dirp = opendir(m_path.characters());
if (!dirp) {
perror("opendir");
exit(1);
}
m_directories.clear();
m_files.clear();
size_t bytes_in_files = 0;
while (auto* de = readdir(dirp)) {
Entry entry;
entry.name = de->d_name;
struct stat st;
int rc = lstat(String::format("%s/%s", m_path.characters(), de->d_name).characters(), &st);
if (rc < 0) {
perror("lstat");
continue;
}
entry.size = st.st_size;
entry.mode = st.st_mode;
auto& entries = S_ISDIR(st.st_mode) ? m_directories : m_files;
entries.append(move(entry));
if (S_ISREG(entry.mode))
bytes_in_files += st.st_size;
}
closedir(dirp);
int excess_height = max(0, (item_count() * item_height()) - height());
m_scrollbar->set_range(0, excess_height);
set_status_message(String::format("%d item%s (%u byte%s)",
item_count(),
item_count() != 1 ? "s" : "",
bytes_in_files,
bytes_in_files != 1 ? "s" : ""));
}
const GraphicsBitmap& DirectoryView::icon_for(const Entry& entry) const
{
if (S_ISDIR(entry.mode))
return *m_directory_icon;
if (S_ISLNK(entry.mode))
return *m_symlink_icon;
if (S_ISSOCK(entry.mode))
return *m_socket_icon;
return *m_file_icon;
}
static String pretty_byte_size(size_t size)
{
return String::format("%u", size);
}
bool DirectoryView::should_show_size_for(const Entry& entry) const
{
return S_ISREG(entry.mode);
}
Rect DirectoryView::row_rect(int item_index) const
{
return { 0, item_index * item_height(), width(), item_height() };
}
void DirectoryView::mousedown_event(GMouseEvent& event)
{
if (event.button() == GMouseButton::Left) {
for (int i = 0; i < item_count(); ++i) {
if (!row_rect(i).contains(event.position()))
continue;
auto& entry = this->entry(i);
if (entry.is_directory()) {
FileSystemPath new_path(String::format("%s/%s", m_path.characters(), entry.name.characters()));
open(new_path.string());
}
}
}
}
void DirectoryView::paint_event(GPaintEvent&)
{
Painter painter(*this);
painter.translate(0, -m_scrollbar->value());
int horizontal_padding = 5;
int icon_size = 16;
int painted_item_index = 0;
auto process_entries = [&] (const Vector<Entry>& entries) {
for (int i = 0; i < entries.size(); ++i, ++painted_item_index) {
auto& entry = entries[i];
int y = painted_item_index * item_height();
Rect icon_rect(horizontal_padding, y, icon_size, item_height());
Rect name_rect(icon_rect.right() + horizontal_padding, y, 100, item_height());
Rect size_rect(name_rect.right() + horizontal_padding, y, 64, item_height());
painter.fill_rect(row_rect(painted_item_index), painted_item_index % 2 ? Color(210, 210, 210) : Color::White);
painter.blit(icon_rect.location(), icon_for(entry), { 0, 0, icon_size, icon_size });
painter.draw_text(name_rect, entry.name, TextAlignment::CenterLeft, Color::Black);
if (should_show_size_for(entry))
painter.draw_text(size_rect, pretty_byte_size(entry.size), TextAlignment::CenterRight, Color::Black);
}
};
process_entries(m_directories);
process_entries(m_files);
Rect unpainted_rect(0, painted_item_index * item_height(), width(), height());
unpainted_rect.intersect(rect());
painter.fill_rect(unpainted_rect, Color::White);
}
void DirectoryView::set_status_message(String&& message)
{
if (on_status_message)
on_status_message(move(message));
}

View File

@ -1,59 +0,0 @@
#pragma once
#include <LibGUI/GWidget.h>
#include <AK/Function.h>
#include <sys/stat.h>
class GScrollBar;
class DirectoryView final : public GWidget {
public:
DirectoryView(GWidget* parent);
virtual ~DirectoryView() override;
void open(const String& path);
void reload();
Function<void(const String&)> on_path_change;
Function<void(String)> on_status_message;
int item_height() const { return 16; }
int item_count() const { return m_directories.size() + m_files.size(); }
private:
virtual void paint_event(GPaintEvent&) override;
virtual void resize_event(GResizeEvent&) override;
virtual void mousedown_event(GMouseEvent&) override;
void set_status_message(String&&);
Rect row_rect(int item_index) const;
struct Entry {
String name;
size_t size { 0 };
mode_t mode { 0 };
bool is_directory() const { return S_ISDIR(mode); }
};
const Entry& entry(int index) const
{
if (index < m_directories.size())
return m_directories[index];
return m_files[index - m_directories.size()];
}
const GraphicsBitmap& icon_for(const Entry&) const;
bool should_show_size_for(const Entry&) const;
Vector<Entry> m_files;
Vector<Entry> m_directories;
String m_path;
RetainPtr<GraphicsBitmap> m_directory_icon;
RetainPtr<GraphicsBitmap> m_file_icon;
RetainPtr<GraphicsBitmap> m_symlink_icon;
RetainPtr<GraphicsBitmap> m_socket_icon;
GScrollBar* m_scrollbar { nullptr };
};

View File

@ -1,5 +1,6 @@
OBJS = \
DirectoryView.o \
DirectoryTableModel.o \
DirectoryTableView.o \
main.o
APP = FileManager

View File

@ -8,7 +8,7 @@
#include <LibGUI/GAction.h>
#include <unistd.h>
#include <stdio.h>
#include "DirectoryView.h"
#include "DirectoryTableView.h"
int main(int argc, char** argv)
{
@ -63,20 +63,21 @@ int main(int argc, char** argv)
toolbar->add_action(copy_action.copy_ref());
toolbar->add_action(delete_action.copy_ref());
auto* directory_view = new DirectoryView(widget);
auto* directory_table_view = new DirectoryTableView(widget);
auto* statusbar = new GStatusBar(widget);
statusbar->set_text("Welcome!");
directory_view->on_path_change = [window] (const String& new_path) {
directory_table_view->on_path_change = [window] (const String& new_path) {
window->set_title(String::format("FileManager: %s", new_path.characters()));
};
directory_view->on_status_message = [statusbar] (String message) {
directory_table_view->on_status_message = [statusbar] (String message) {
statusbar->set_text(move(message));
};
directory_view->open("/");
directory_table_view->open("/");
directory_table_view->set_focus(true);
window->set_should_exit_app_on_close(true);
window->show();