Use a unified filesystem model
This removes some duplication of effort. So far, sorting adjustments are not exposed in the UI.
This commit is contained in:
parent
2ac918b7ab
commit
b935b0baf8
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
|
Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted.
|
purpose with or without fee is hereby granted.
|
||||||
|
|
156
fastiv.c
156
fastiv.c
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
// fastiv.c: fast image viewer
|
// fastiv.c: fast image viewer
|
||||||
//
|
//
|
||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
|
@ -25,15 +25,12 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
#include <fnmatch.h>
|
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "fiv-browser.h"
|
#include "fiv-browser.h"
|
||||||
#include "fiv-io.h"
|
#include "fiv-io.h"
|
||||||
#include "fiv-sidebar.h"
|
#include "fiv-sidebar.h"
|
||||||
#include "fiv-thumbnail.h"
|
#include "fiv-thumbnail.h"
|
||||||
#include "fiv-view.h"
|
#include "fiv-view.h"
|
||||||
#include "xdg.h"
|
|
||||||
|
|
||||||
// --- Utilities ---------------------------------------------------------------
|
// --- Utilities ---------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -260,14 +257,13 @@ enum {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
gchar **supported_globs;
|
FivIoModel *model; ///< "directory" contents
|
||||||
gboolean filtering;
|
gchar *directory; ///< URI of the currently browsed directory
|
||||||
|
GList *directory_back; ///< History paths as URIs going backwards
|
||||||
|
GList *directory_forward; ///< History paths as URIs going forwards
|
||||||
|
GPtrArray *files; ///< "directory" contents as URIs
|
||||||
|
|
||||||
gchar *uri; ///< Current image URI, if any
|
gchar *uri; ///< Current image URI, if any
|
||||||
gchar *directory; ///< URI of the currently browsed directory
|
|
||||||
GList *directory_back; ///< History paths going backwards
|
|
||||||
GList *directory_forward; ///< History paths going forwards
|
|
||||||
GPtrArray *files; ///< "directory" contents as URIs
|
|
||||||
gint files_index; ///< Where "uri" is within "files"
|
gint files_index; ///< Where "uri" is within "files"
|
||||||
|
|
||||||
GtkWidget *window;
|
GtkWidget *window;
|
||||||
|
@ -286,27 +282,6 @@ struct {
|
||||||
GtkWidget *view;
|
GtkWidget *view;
|
||||||
} g;
|
} g;
|
||||||
|
|
||||||
static gboolean
|
|
||||||
is_supported(const gchar *filename)
|
|
||||||
{
|
|
||||||
gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
|
|
||||||
if (!utf8)
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
gchar *lowercased = g_utf8_strdown(utf8, -1);
|
|
||||||
g_free(utf8);
|
|
||||||
|
|
||||||
// XXX: fnmatch() uses the /locale/ encoding, but who cares nowadays.
|
|
||||||
for (gchar **p = g.supported_globs; *p; p++)
|
|
||||||
if (!fnmatch(*p, lowercased, 0)) {
|
|
||||||
g_free(lowercased);
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_free(lowercased);
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
show_error_dialog(GError *error)
|
show_error_dialog(GError *error)
|
||||||
{
|
{
|
||||||
|
@ -346,17 +321,6 @@ switch_to_view(void)
|
||||||
gtk_widget_grab_focus(g.view);
|
gtk_widget_grab_focus(g.view);
|
||||||
}
|
}
|
||||||
|
|
||||||
static gint
|
|
||||||
files_compare(gconstpointer a, gconstpointer b)
|
|
||||||
{
|
|
||||||
GFile *file1 = g_file_new_for_uri(*(gchar **) a);
|
|
||||||
GFile *file2 = g_file_new_for_uri(*(gchar **) b);
|
|
||||||
gint result = fiv_io_filecmp(file1, file2);
|
|
||||||
g_object_unref(file1);
|
|
||||||
g_object_unref(file2);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gchar *
|
static gchar *
|
||||||
parent_uri(GFile *child_file)
|
parent_uri(GFile *child_file)
|
||||||
{
|
{
|
||||||
|
@ -415,7 +379,7 @@ load_directory_without_reload(const gchar *uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
load_directory(const gchar *uri)
|
load_directory_without_switching(const gchar *uri)
|
||||||
{
|
{
|
||||||
if (uri) {
|
if (uri) {
|
||||||
load_directory_without_reload(uri);
|
load_directory_without_reload(uri);
|
||||||
|
@ -429,42 +393,30 @@ load_directory(const gchar *uri)
|
||||||
g_ptr_array_set_size(g.files, 0);
|
g_ptr_array_set_size(g.files, 0);
|
||||||
g.files_index = -1;
|
g.files_index = -1;
|
||||||
|
|
||||||
GFile *file = g_file_new_for_uri(g.directory);
|
|
||||||
fiv_sidebar_set_location(FIV_SIDEBAR(g.browser_sidebar), file);
|
|
||||||
fiv_browser_load(
|
|
||||||
FIV_BROWSER(g.browser), g.filtering ? is_supported : NULL, g.directory);
|
|
||||||
|
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
GFileEnumerator *enumerator = g_file_enumerate_children(file,
|
GFile *file = g_file_new_for_uri(g.directory);
|
||||||
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
|
if (fiv_io_model_open(g.model, file, &error)) {
|
||||||
G_FILE_QUERY_INFO_NONE, NULL, &error);
|
g_ptr_array_free(g.files, TRUE);
|
||||||
if (enumerator) {
|
g.files = fiv_io_model_get_files(g.model);
|
||||||
GFileInfo *info = NULL;
|
|
||||||
GFile *child = NULL;
|
|
||||||
while (
|
|
||||||
g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) &&
|
|
||||||
info) {
|
|
||||||
// TODO(p): What encoding does g_file_info_get_name() return?
|
|
||||||
if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY &&
|
|
||||||
is_supported(g_file_info_get_name(info)))
|
|
||||||
g_ptr_array_add(g.files, g_file_get_uri(child));
|
|
||||||
}
|
|
||||||
g_object_unref(enumerator);
|
|
||||||
|
|
||||||
g_ptr_array_sort(g.files, files_compare);
|
|
||||||
update_files_index();
|
update_files_index();
|
||||||
} else if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) {
|
} else if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) {
|
||||||
g_error_free(error);
|
g_error_free(error);
|
||||||
} else {
|
} else {
|
||||||
show_error_dialog(error);
|
show_error_dialog(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
g_object_unref(file);
|
g_object_unref(file);
|
||||||
|
|
||||||
gtk_widget_set_sensitive(
|
gtk_widget_set_sensitive(
|
||||||
g.toolbar[TOOLBAR_FILE_PREVIOUS], g.files->len > 1);
|
g.toolbar[TOOLBAR_FILE_PREVIOUS], g.files->len > 1);
|
||||||
gtk_widget_set_sensitive(
|
gtk_widget_set_sensitive(
|
||||||
g.toolbar[TOOLBAR_FILE_NEXT], g.files->len > 1);
|
g.toolbar[TOOLBAR_FILE_NEXT], g.files->len > 1);
|
||||||
g_ptr_array_add(g.files, NULL);
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
load_directory(const gchar *uri)
|
||||||
|
{
|
||||||
|
load_directory_without_switching(uri);
|
||||||
|
|
||||||
// XXX: When something outside the filtered entries is open, the index is
|
// XXX: When something outside the filtered entries is open, the index is
|
||||||
// kept at -1, and browsing doesn't work. How to behave here?
|
// kept at -1, and browsing doesn't work. How to behave here?
|
||||||
|
@ -480,9 +432,8 @@ load_directory(const gchar *uri)
|
||||||
static void
|
static void
|
||||||
on_filtering_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)
|
on_filtering_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)
|
||||||
{
|
{
|
||||||
g.filtering = gtk_toggle_button_get_active(button);
|
g_object_set(
|
||||||
if (g.directory)
|
g.model, "filtering", gtk_toggle_button_get_active(button), NULL);
|
||||||
load_directory(NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -514,7 +465,7 @@ open(const gchar *uri)
|
||||||
gchar *parent = parent_uri(file);
|
gchar *parent = parent_uri(file);
|
||||||
if (!g.files->len /* hack to always load the directory after launch */ ||
|
if (!g.files->len /* hack to always load the directory after launch */ ||
|
||||||
!g.directory || strcmp(parent, g.directory))
|
!g.directory || strcmp(parent, g.directory))
|
||||||
load_directory(parent);
|
load_directory_without_switching(parent);
|
||||||
else
|
else
|
||||||
update_files_index();
|
update_files_index();
|
||||||
g_free(parent);
|
g_free(parent);
|
||||||
|
@ -579,8 +530,7 @@ static void
|
||||||
on_previous(void)
|
on_previous(void)
|
||||||
{
|
{
|
||||||
if (g.files_index >= 0) {
|
if (g.files_index >= 0) {
|
||||||
int previous =
|
int previous = (g.files->len + g.files_index - 1) % g.files->len;
|
||||||
(g.files->len - 1 + g.files_index - 1) % (g.files->len - 1);
|
|
||||||
open(g_ptr_array_index(g.files, previous));
|
open(g_ptr_array_index(g.files, previous));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -589,7 +539,7 @@ static void
|
||||||
on_next(void)
|
on_next(void)
|
||||||
{
|
{
|
||||||
if (g.files_index >= 0) {
|
if (g.files_index >= 0) {
|
||||||
int next = (g.files_index + 1) % (g.files->len - 1);
|
int next = (g.files_index + 1) % g.files->len;
|
||||||
open(g_ptr_array_index(g.files, next));
|
open(g_ptr_array_index(g.files, next));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -616,15 +566,15 @@ on_item_activated(G_GNUC_UNUSED FivBrowser *browser, GFile *location,
|
||||||
g_free(uri);
|
g_free(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
static void
|
||||||
open_any_uri(const char *uri, gboolean force_browser)
|
open_any_file(GFile *file, gboolean force_browser)
|
||||||
{
|
{
|
||||||
GFile *file = g_file_new_for_uri(uri);
|
gchar *uri = g_file_get_uri(file);
|
||||||
GFileType type = g_file_query_file_type(file, G_FILE_QUERY_INFO_NONE, NULL);
|
GFileType type = g_file_query_file_type(file, G_FILE_QUERY_INFO_NONE, NULL);
|
||||||
gboolean success = type != G_FILE_TYPE_UNKNOWN;
|
if (type == G_FILE_TYPE_UNKNOWN) {
|
||||||
if (!success) {
|
errno = ENOENT;
|
||||||
show_error_dialog(g_error_new(G_FILE_ERROR,
|
show_error_dialog(g_error_new(G_FILE_ERROR,
|
||||||
g_file_error_from_errno(errno), "%s: %s", uri, g_strerror(ENOENT)));
|
g_file_error_from_errno(errno), "%s: %s", uri, g_strerror(errno)));
|
||||||
} else if (type == G_FILE_TYPE_DIRECTORY) {
|
} else if (type == G_FILE_TYPE_DIRECTORY) {
|
||||||
load_directory(uri);
|
load_directory(uri);
|
||||||
} else if (force_browser) {
|
} else if (force_browser) {
|
||||||
|
@ -636,8 +586,7 @@ open_any_uri(const char *uri, gboolean force_browser)
|
||||||
} else {
|
} else {
|
||||||
open(uri);
|
open(uri);
|
||||||
}
|
}
|
||||||
g_object_unref(file);
|
g_free(uri);
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -648,7 +597,7 @@ on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location,
|
||||||
if (flags & GTK_PLACES_OPEN_NEW_WINDOW)
|
if (flags & GTK_PLACES_OPEN_NEW_WINDOW)
|
||||||
spawn_uri(uri);
|
spawn_uri(uri);
|
||||||
else
|
else
|
||||||
open_any_uri(uri, FALSE);
|
open_any_file(location, FALSE);
|
||||||
g_free(uri);
|
g_free(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -675,6 +624,15 @@ on_notify_thumbnail_size(
|
||||||
gtk_widget_set_sensitive(g.minus, size > FIV_THUMBNAIL_SIZE_MIN);
|
gtk_widget_set_sensitive(g.minus, size > FIV_THUMBNAIL_SIZE_MIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
on_notify_filtering(
|
||||||
|
GObject *object, GParamSpec *param_spec, gpointer user_data)
|
||||||
|
{
|
||||||
|
gboolean b = FALSE;
|
||||||
|
g_object_get(object, g_param_spec_get_name(param_spec), &b, NULL);
|
||||||
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(user_data), b);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
toggle_fullscreen(void)
|
toggle_fullscreen(void)
|
||||||
{
|
{
|
||||||
|
@ -800,7 +758,11 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
case GDK_KEY_Home:
|
case GDK_KEY_Home:
|
||||||
load_directory(g_get_home_dir());
|
if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) != g.view_box) {
|
||||||
|
gchar *uri = g_filename_to_uri(g_get_home_dir(), NULL, NULL);
|
||||||
|
load_directory(uri);
|
||||||
|
g_free(uri);
|
||||||
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -1265,6 +1227,8 @@ main(int argc, char *argv[])
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.model = g_object_new(FIV_TYPE_IO_MODEL, NULL);
|
||||||
|
|
||||||
gtk_window_set_default_icon_name(PROJECT_NAME);
|
gtk_window_set_default_icon_name(PROJECT_NAME);
|
||||||
gtk_icon_theme_add_resource_path(
|
gtk_icon_theme_add_resource_path(
|
||||||
gtk_icon_theme_get_default(), "/org/gnome/design/IconLibrary/");
|
gtk_icon_theme_get_default(), "/org/gnome/design/IconLibrary/");
|
||||||
|
@ -1293,7 +1257,7 @@ main(int argc, char *argv[])
|
||||||
gtk_box_pack_start(GTK_BOX(g.view_box), view_scroller, TRUE, TRUE, 0);
|
gtk_box_pack_start(GTK_BOX(g.view_box), view_scroller, TRUE, TRUE, 0);
|
||||||
|
|
||||||
g.browser_scroller = gtk_scrolled_window_new(NULL, NULL);
|
g.browser_scroller = gtk_scrolled_window_new(NULL, NULL);
|
||||||
g.browser = g_object_new(FIV_TYPE_BROWSER, NULL);
|
g.browser = fiv_browser_new(g.model);
|
||||||
gtk_widget_set_vexpand(g.browser, TRUE);
|
gtk_widget_set_vexpand(g.browser, TRUE);
|
||||||
gtk_widget_set_hexpand(g.browser, TRUE);
|
gtk_widget_set_hexpand(g.browser, TRUE);
|
||||||
g_signal_connect(g.browser, "item-activated",
|
g_signal_connect(g.browser, "item-activated",
|
||||||
|
@ -1307,7 +1271,7 @@ main(int argc, char *argv[])
|
||||||
|
|
||||||
// TODO(p): As with GtkFileChooserWidget, bind C-h to filtering,
|
// TODO(p): As with GtkFileChooserWidget, bind C-h to filtering,
|
||||||
// and mayhaps forward the rest to the sidebar, somehow.
|
// and mayhaps forward the rest to the sidebar, somehow.
|
||||||
g.browser_sidebar = g_object_new(FIV_TYPE_SIDEBAR, NULL);
|
g.browser_sidebar = fiv_sidebar_new(g.model);
|
||||||
g_signal_connect(g.browser_sidebar, "open-location",
|
g_signal_connect(g.browser_sidebar, "open-location",
|
||||||
G_CALLBACK(on_open_location), NULL);
|
G_CALLBACK(on_open_location), NULL);
|
||||||
|
|
||||||
|
@ -1375,13 +1339,11 @@ main(int argc, char *argv[])
|
||||||
G_CALLBACK(on_window_state_event), NULL);
|
G_CALLBACK(on_window_state_event), NULL);
|
||||||
gtk_container_add(GTK_CONTAINER(g.window), g.stack);
|
gtk_container_add(GTK_CONTAINER(g.window), g.stack);
|
||||||
|
|
||||||
char **types = fiv_io_all_supported_media_types();
|
|
||||||
g.supported_globs = extract_mime_globs((const char **) types);
|
|
||||||
g_strfreev(types);
|
|
||||||
|
|
||||||
g_signal_connect(g.browser, "notify::thumbnail-size",
|
g_signal_connect(g.browser, "notify::thumbnail-size",
|
||||||
G_CALLBACK(on_notify_thumbnail_size), NULL);
|
G_CALLBACK(on_notify_thumbnail_size), NULL);
|
||||||
on_toolbar_zoom(NULL, (gpointer) 0);
|
on_toolbar_zoom(NULL, (gpointer) 0);
|
||||||
|
g_signal_connect(g.model, "notify::filtering",
|
||||||
|
G_CALLBACK(on_notify_filtering), funnel);
|
||||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(funnel), TRUE);
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(funnel), TRUE);
|
||||||
|
|
||||||
// Try to get half of the screen vertically, in 4:3 aspect ratio.
|
// Try to get half of the screen vertically, in 4:3 aspect ratio.
|
||||||
|
@ -1401,22 +1363,20 @@ main(int argc, char *argv[])
|
||||||
unit = MAX(200, unit);
|
unit = MAX(200, unit);
|
||||||
gtk_window_set_default_size(GTK_WINDOW(g.window), 4 * unit, 3 * unit);
|
gtk_window_set_default_size(GTK_WINDOW(g.window), 4 * unit, 3 * unit);
|
||||||
|
|
||||||
g.files = g_ptr_array_new_full(16, g_free);
|
|
||||||
gchar *cwd = g_get_current_dir();
|
|
||||||
g.directory = g_filename_to_uri(cwd, NULL, NULL /* error */);
|
|
||||||
g_free(cwd);
|
|
||||||
|
|
||||||
// XXX: The widget wants to read the display's profile. The realize is ugly.
|
// XXX: The widget wants to read the display's profile. The realize is ugly.
|
||||||
gtk_widget_realize(g.view);
|
gtk_widget_realize(g.view);
|
||||||
|
|
||||||
gchar *uri = NULL;
|
g.files = g_ptr_array_new_full(0, g_free);
|
||||||
if (path_arg) {
|
if (path_arg) {
|
||||||
GFile *file = g_file_new_for_commandline_arg(path_arg);
|
GFile *file = g_file_new_for_commandline_arg(path_arg);
|
||||||
uri = g_file_get_uri(file);
|
open_any_file(file, browse);
|
||||||
|
g_object_unref(file);
|
||||||
|
}
|
||||||
|
if (!g.directory) {
|
||||||
|
GFile *file = g_file_new_for_path(".");
|
||||||
|
open_any_file(file, FALSE);
|
||||||
g_object_unref(file);
|
g_object_unref(file);
|
||||||
}
|
}
|
||||||
if (!uri || !open_any_uri(uri, browse))
|
|
||||||
open_any_uri(g.directory, FALSE);
|
|
||||||
|
|
||||||
gtk_widget_show_all(g.window);
|
gtk_widget_show_all(g.window);
|
||||||
gtk_main();
|
gtk_main();
|
||||||
|
|
102
fiv-browser.c
102
fiv-browser.c
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
// fiv-browser.c: fast image viewer - filesystem browser widget
|
// fiv-browser.c: fast image viewer - filesystem browser widget
|
||||||
//
|
//
|
||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
|
@ -48,7 +48,7 @@ struct _FivBrowser {
|
||||||
int item_height; ///< Thumbnail height in pixels
|
int item_height; ///< Thumbnail height in pixels
|
||||||
int item_spacing; ///< Space between items in pixels
|
int item_spacing; ///< Space between items in pixels
|
||||||
|
|
||||||
char *uri; ///< Current URI
|
FivIoModel *model; ///< Filesystem model
|
||||||
GArray *entries; ///< []Entry
|
GArray *entries; ///< []Entry
|
||||||
GArray *layouted_rows; ///< []Row
|
GArray *layouted_rows; ///< []Row
|
||||||
int selected;
|
int selected;
|
||||||
|
@ -541,9 +541,9 @@ thumbnailer_start(FivBrowser *self)
|
||||||
gchar *thumbnails_dir = fiv_thumbnail_get_root();
|
gchar *thumbnails_dir = fiv_thumbnail_get_root();
|
||||||
GFile *thumbnails = g_file_new_for_path(thumbnails_dir);
|
GFile *thumbnails = g_file_new_for_path(thumbnails_dir);
|
||||||
g_free(thumbnails_dir);
|
g_free(thumbnails_dir);
|
||||||
GFile *current = g_file_new_for_uri(self->uri);
|
|
||||||
gboolean is_a_thumbnail = g_file_has_prefix(current, thumbnails);
|
GFile *current = fiv_io_model_get_location(self->model);
|
||||||
g_object_unref(current);
|
gboolean is_a_thumbnail = current && g_file_has_prefix(current, thumbnails);
|
||||||
g_object_unref(thumbnails);
|
g_object_unref(thumbnails);
|
||||||
if (is_a_thumbnail)
|
if (is_a_thumbnail)
|
||||||
return;
|
return;
|
||||||
|
@ -651,22 +651,19 @@ destroy_widget_idle_source_func(GtkWidget *widget)
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
show_context_menu(GtkWidget *widget, const char *uri)
|
show_context_menu(GtkWidget *widget, GFile *file)
|
||||||
{
|
{
|
||||||
GFile *file = g_file_new_for_uri(uri);
|
|
||||||
GFileInfo *info = g_file_query_info(file,
|
GFileInfo *info = g_file_query_info(file,
|
||||||
G_FILE_ATTRIBUTE_STANDARD_NAME
|
G_FILE_ATTRIBUTE_STANDARD_NAME
|
||||||
"," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
|
"," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
|
||||||
G_FILE_QUERY_INFO_NONE, NULL, NULL);
|
G_FILE_QUERY_INFO_NONE, NULL, NULL);
|
||||||
if (!info) {
|
if (!info)
|
||||||
g_object_unref(file);
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// This will have no application pre-assigned, for use with GTK+'s dialog.
|
// This will have no application pre-assigned, for use with GTK+'s dialog.
|
||||||
OpenContext *ctx = g_malloc0(sizeof *ctx);
|
OpenContext *ctx = g_malloc0(sizeof *ctx);
|
||||||
g_weak_ref_init(&ctx->widget, widget);
|
g_weak_ref_init(&ctx->widget, widget);
|
||||||
ctx->file = file;
|
ctx->file = g_object_ref(file);
|
||||||
ctx->content_type = g_strdup(g_file_info_get_content_type(info));
|
ctx->content_type = g_strdup(g_file_info_get_content_type(info));
|
||||||
g_object_unref(info);
|
g_object_unref(info);
|
||||||
|
|
||||||
|
@ -750,9 +747,13 @@ fiv_browser_finalize(GObject *gobject)
|
||||||
{
|
{
|
||||||
FivBrowser *self = FIV_BROWSER(gobject);
|
FivBrowser *self = FIV_BROWSER(gobject);
|
||||||
thumbnailer_abort(self);
|
thumbnailer_abort(self);
|
||||||
g_free(self->uri);
|
|
||||||
g_array_free(self->entries, TRUE);
|
g_array_free(self->entries, TRUE);
|
||||||
g_array_free(self->layouted_rows, TRUE);
|
g_array_free(self->layouted_rows, TRUE);
|
||||||
|
if (self->model) {
|
||||||
|
g_signal_handlers_disconnect_by_data(self->model, self);
|
||||||
|
g_clear_object(&self->model);
|
||||||
|
}
|
||||||
|
|
||||||
cairo_surface_destroy(self->glow);
|
cairo_surface_destroy(self->glow);
|
||||||
g_clear_object(&self->pointer);
|
g_clear_object(&self->pointer);
|
||||||
|
|
||||||
|
@ -931,7 +932,7 @@ fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
|
||||||
|
|
||||||
const Entry *entry = entry_at(self, event->x, event->y);
|
const Entry *entry = entry_at(self, event->x, event->y);
|
||||||
if (!entry && event->button == GDK_BUTTON_SECONDARY) {
|
if (!entry && event->button == GDK_BUTTON_SECONDARY) {
|
||||||
show_context_menu(widget, self->uri);
|
show_context_menu(widget, fiv_io_model_get_location(self->model));
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
if (!entry)
|
if (!entry)
|
||||||
|
@ -952,7 +953,10 @@ fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
|
||||||
// On X11, after closing the menu, the pointer otherwise remains,
|
// On X11, after closing the menu, the pointer otherwise remains,
|
||||||
// no matter what its new location is.
|
// no matter what its new location is.
|
||||||
gdk_window_set_cursor(gtk_widget_get_window(widget), NULL);
|
gdk_window_set_cursor(gtk_widget_get_window(widget), NULL);
|
||||||
show_context_menu(widget, entry->uri);
|
|
||||||
|
GFile *file = g_file_new_for_uri(entry->uri);
|
||||||
|
show_context_menu(widget, file);
|
||||||
|
g_object_unref(file);
|
||||||
return TRUE;
|
return TRUE;
|
||||||
default:
|
default:
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
@ -1149,62 +1153,38 @@ fiv_browser_init(FivBrowser *self)
|
||||||
|
|
||||||
// --- Public interface --------------------------------------------------------
|
// --- Public interface --------------------------------------------------------
|
||||||
|
|
||||||
static gint
|
static void
|
||||||
entry_compare(gconstpointer a, gconstpointer b)
|
on_model_files_changed(FivIoModel *model, FivBrowser *self)
|
||||||
{
|
{
|
||||||
const Entry *entry1 = a;
|
g_return_if_fail(model == self->model);
|
||||||
const Entry *entry2 = b;
|
|
||||||
GFile *location1 = g_file_new_for_uri(entry1->uri);
|
|
||||||
GFile *location2 = g_file_new_for_uri(entry2->uri);
|
|
||||||
gint result = fiv_io_filecmp(location1, location2);
|
|
||||||
g_object_unref(location1);
|
|
||||||
g_object_unref(location2);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
fiv_browser_load(
|
|
||||||
FivBrowser *self, FivBrowserFilterCallback cb, const char *uri)
|
|
||||||
{
|
|
||||||
g_return_if_fail(FIV_IS_BROWSER(self));
|
|
||||||
|
|
||||||
|
// TODO(p): Later implement arguments.
|
||||||
thumbnailer_abort(self);
|
thumbnailer_abort(self);
|
||||||
g_array_set_size(self->entries, 0);
|
g_array_set_size(self->entries, 0);
|
||||||
g_array_set_size(self->layouted_rows, 0);
|
g_array_set_size(self->layouted_rows, 0);
|
||||||
g_clear_pointer(&self->uri, g_free);
|
|
||||||
|
|
||||||
GFile *file = g_file_new_for_uri((self->uri = g_strdup(uri)));
|
|
||||||
|
|
||||||
GError *error = NULL;
|
|
||||||
GFileEnumerator *enumerator = g_file_enumerate_children(file,
|
|
||||||
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
|
|
||||||
G_FILE_QUERY_INFO_NONE, NULL, &error);
|
|
||||||
g_object_unref(file);
|
|
||||||
if (!enumerator) {
|
|
||||||
// Note that this has had a side-effect of clearing all entries.
|
|
||||||
g_error_free(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (TRUE) {
|
|
||||||
GFileInfo *info = NULL;
|
|
||||||
GFile *child = NULL;
|
|
||||||
if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) ||
|
|
||||||
!info)
|
|
||||||
break;
|
|
||||||
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
|
|
||||||
continue;
|
|
||||||
if (cb && !cb(g_file_info_get_name(info)))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
|
GPtrArray *files = fiv_io_model_get_files(self->model);
|
||||||
|
for (guint i = 0; i < files->len; i++) {
|
||||||
g_array_append_val(self->entries,
|
g_array_append_val(self->entries,
|
||||||
((Entry) {.thumbnail = NULL, .uri = g_file_get_uri(child)}));
|
((Entry) {.thumbnail = NULL, .uri = files->pdata[i]}));
|
||||||
|
files->pdata[i] = NULL;
|
||||||
}
|
}
|
||||||
g_object_unref(enumerator);
|
g_ptr_array_free(files, TRUE);
|
||||||
|
|
||||||
// TODO(p): Support being passed a sort function.
|
|
||||||
g_array_sort(self->entries, entry_compare);
|
|
||||||
|
|
||||||
reload_thumbnails(self);
|
reload_thumbnails(self);
|
||||||
thumbnailer_start(self);
|
thumbnailer_start(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GtkWidget *
|
||||||
|
fiv_browser_new(FivIoModel *model)
|
||||||
|
{
|
||||||
|
g_return_val_if_fail(FIV_IS_IO_MODEL(model), NULL);
|
||||||
|
|
||||||
|
FivBrowser *self = g_object_new(FIV_TYPE_BROWSER, NULL);
|
||||||
|
self->model = g_object_ref(model);
|
||||||
|
|
||||||
|
g_signal_connect(
|
||||||
|
self->model, "files-changed", G_CALLBACK(on_model_files_changed), self);
|
||||||
|
on_model_files_changed(self->model, self);
|
||||||
|
return GTK_WIDGET(self);
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
// fiv-browser.h: fast image viewer - filesystem browser widget
|
// fiv-browser.h: fast image viewer - filesystem browser widget
|
||||||
//
|
//
|
||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
|
@ -17,12 +17,11 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fiv-io.h"
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
#define FIV_TYPE_BROWSER (fiv_browser_get_type())
|
#define FIV_TYPE_BROWSER (fiv_browser_get_type())
|
||||||
G_DECLARE_FINAL_TYPE(FivBrowser, fiv_browser, FIV, BROWSER, GtkWidget)
|
G_DECLARE_FINAL_TYPE(FivBrowser, fiv_browser, FIV, BROWSER, GtkWidget)
|
||||||
|
|
||||||
typedef gboolean (*FivBrowserFilterCallback) (const char *);
|
GtkWidget *fiv_browser_new(FivIoModel *model);
|
||||||
|
|
||||||
void fiv_browser_load(
|
|
||||||
FivBrowser *self, FivBrowserFilterCallback cb, const char *path);
|
|
||||||
|
|
324
fiv-io.c
324
fiv-io.c
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
// fiv-io.c: image operations
|
// fiv-io.c: image operations
|
||||||
//
|
//
|
||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
|
@ -132,22 +132,6 @@ fiv_io_all_supported_media_types(void)
|
||||||
return (char **) g_ptr_array_free(types, FALSE);
|
return (char **) g_ptr_array_free(types, FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
|
||||||
fiv_io_filecmp(GFile *location1, GFile *location2)
|
|
||||||
{
|
|
||||||
if (g_file_has_prefix(location1, location2))
|
|
||||||
return +1;
|
|
||||||
if (g_file_has_prefix(location2, location1))
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
gchar *name1 = g_file_get_parse_name(location1);
|
|
||||||
gchar *name2 = g_file_get_parse_name(location2);
|
|
||||||
int result = g_utf8_collate(name1, name2);
|
|
||||||
g_free(name1);
|
|
||||||
g_free(name2);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
#define FIV_IO_ERROR fiv_io_error_quark()
|
#define FIV_IO_ERROR fiv_io_error_quark()
|
||||||
|
@ -2463,6 +2447,312 @@ fiv_io_open_from_data(const char *data, size_t len, const gchar *uri,
|
||||||
return surface;
|
return surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Filesystem --------------------------------------------------------------
|
||||||
|
|
||||||
|
#include "xdg.h"
|
||||||
|
|
||||||
|
#include <fnmatch.h>
|
||||||
|
|
||||||
|
typedef struct _ModelEntry {
|
||||||
|
gchar *uri; ///< GIO URI
|
||||||
|
gint64 mtime_msec; ///< Modification time in milliseconds
|
||||||
|
} ModelEntry;
|
||||||
|
|
||||||
|
static void
|
||||||
|
model_entry_finalize(ModelEntry *entry)
|
||||||
|
{
|
||||||
|
g_free(entry->uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef enum _FivIoModelSort {
|
||||||
|
FIV_IO_MODEL_SORT_NAME,
|
||||||
|
FIV_IO_MODEL_SORT_MTIME,
|
||||||
|
} FivIoModelSort;
|
||||||
|
|
||||||
|
struct _FivIoModel {
|
||||||
|
GObject parent_instance;
|
||||||
|
gchar **supported_globs;
|
||||||
|
|
||||||
|
GFile *directory; ///< Currently loaded directory
|
||||||
|
GFileMonitor *monitor; ///< "directory" monitoring
|
||||||
|
GArray *subdirs; ///< "directory" contents
|
||||||
|
GArray *files; ///< "directory" contents
|
||||||
|
|
||||||
|
FivIoModelSort sort; ///< How to sort
|
||||||
|
gboolean sort_descending; ///< Whether to sort in reverse
|
||||||
|
gboolean filtering; ///< Only show non-hidden, supported
|
||||||
|
};
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(FivIoModel, fiv_io_model, G_TYPE_OBJECT)
|
||||||
|
|
||||||
|
enum {
|
||||||
|
PROP_FILTERING = 1,
|
||||||
|
N_PROPERTIES
|
||||||
|
};
|
||||||
|
|
||||||
|
static GParamSpec *model_properties[N_PROPERTIES];
|
||||||
|
|
||||||
|
enum {
|
||||||
|
FILES_CHANGED,
|
||||||
|
SUBDIRECTORIES_CHANGED,
|
||||||
|
LAST_SIGNAL,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Globals are, sadly, the canonical way of storing signal numbers.
|
||||||
|
static guint model_signals[LAST_SIGNAL];
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
model_supports(FivIoModel *self, const gchar *filename)
|
||||||
|
{
|
||||||
|
gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
|
||||||
|
if (!utf8)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
gchar *lowercased = g_utf8_strdown(utf8, -1);
|
||||||
|
g_free(utf8);
|
||||||
|
|
||||||
|
// XXX: fnmatch() uses the /locale/ encoding, but who cares nowadays.
|
||||||
|
// TODO(p): Use GPatternSpec and g_file_info_get_display_name().
|
||||||
|
for (gchar **p = self->supported_globs; *p; p++)
|
||||||
|
if (!fnmatch(*p, lowercased, 0)) {
|
||||||
|
g_free(lowercased);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_free(lowercased);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int
|
||||||
|
model_compare_name(GFile *location1, GFile *location2)
|
||||||
|
{
|
||||||
|
gchar *name1 = g_file_get_parse_name(location1);
|
||||||
|
gchar *name2 = g_file_get_parse_name(location2);
|
||||||
|
int result = g_utf8_collate(name1, name2);
|
||||||
|
g_free(name1);
|
||||||
|
g_free(name2);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int
|
||||||
|
model_compare_entries(FivIoModel *self, const ModelEntry *entry1, GFile *file1,
|
||||||
|
const ModelEntry *entry2, GFile *file2)
|
||||||
|
{
|
||||||
|
if (g_file_has_prefix(file1, file2))
|
||||||
|
return +1;
|
||||||
|
if (g_file_has_prefix(file2, file1))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
int result = 0;
|
||||||
|
switch (self->sort) {
|
||||||
|
case FIV_IO_MODEL_SORT_NAME:
|
||||||
|
result = model_compare_name(file1, file2);
|
||||||
|
break;
|
||||||
|
case FIV_IO_MODEL_SORT_MTIME:
|
||||||
|
result -= entry1->mtime_msec < entry2->mtime_msec;
|
||||||
|
result += entry1->mtime_msec > entry2->mtime_msec;
|
||||||
|
}
|
||||||
|
return self->sort_descending ? -result : +result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gint
|
||||||
|
model_compare(gconstpointer a, gconstpointer b, gpointer user_data)
|
||||||
|
{
|
||||||
|
const ModelEntry *entry1 = a;
|
||||||
|
const ModelEntry *entry2 = b;
|
||||||
|
GFile *file1 = g_file_new_for_uri(entry1->uri);
|
||||||
|
GFile *file2 = g_file_new_for_uri(entry2->uri);
|
||||||
|
int result = model_compare_entries(user_data, entry1, file1, entry2, file2);
|
||||||
|
g_object_unref(file1);
|
||||||
|
g_object_unref(file2);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
model_reload(FivIoModel *self, GError **error)
|
||||||
|
{
|
||||||
|
g_array_set_size(self->subdirs, 0);
|
||||||
|
g_array_set_size(self->files, 0);
|
||||||
|
|
||||||
|
GFileEnumerator *enumerator = g_file_enumerate_children(self->directory,
|
||||||
|
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE ","
|
||||||
|
G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
|
||||||
|
G_FILE_ATTRIBUTE_TIME_MODIFIED "," G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC,
|
||||||
|
G_FILE_QUERY_INFO_NONE, NULL, error);
|
||||||
|
if (!enumerator) {
|
||||||
|
// Note that this has had a side-effect of clearing all entries.
|
||||||
|
g_signal_emit(self, model_signals[FILES_CHANGED], 0);
|
||||||
|
g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
GFileInfo *info = NULL;
|
||||||
|
GFile *child = NULL;
|
||||||
|
while (g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) &&
|
||||||
|
info) {
|
||||||
|
if (self->filtering && g_file_info_get_is_hidden(info))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ModelEntry entry = {};
|
||||||
|
GDateTime *mtime = g_file_info_get_modification_date_time(info);
|
||||||
|
if (mtime) {
|
||||||
|
entry.mtime_msec = g_date_time_to_unix(mtime) * 1000 +
|
||||||
|
g_date_time_get_microsecond(mtime) / 1000;
|
||||||
|
g_date_time_unref(mtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *name = g_file_info_get_name(info);
|
||||||
|
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) {
|
||||||
|
entry.uri = g_file_get_uri(child);
|
||||||
|
g_array_append_val(self->subdirs, entry);
|
||||||
|
} else if (!self->filtering || model_supports(self, name)) {
|
||||||
|
entry.uri = g_file_get_uri(child);
|
||||||
|
g_array_append_val(self->files, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_object_unref(enumerator);
|
||||||
|
g_array_sort_with_data(self->subdirs, model_compare, self);
|
||||||
|
g_array_sort_with_data(self->files, model_compare, self);
|
||||||
|
|
||||||
|
g_signal_emit(self, model_signals[FILES_CHANGED], 0);
|
||||||
|
g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
static void
|
||||||
|
fiv_io_model_finalize(GObject *gobject)
|
||||||
|
{
|
||||||
|
FivIoModel *self = FIV_IO_MODEL(gobject);
|
||||||
|
g_clear_object(&self->directory);
|
||||||
|
g_clear_object(&self->monitor);
|
||||||
|
g_array_free(self->subdirs, TRUE);
|
||||||
|
g_array_free(self->files, TRUE);
|
||||||
|
|
||||||
|
G_OBJECT_CLASS(fiv_io_model_parent_class)->finalize(gobject);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
fiv_io_model_get_property(
|
||||||
|
GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
|
||||||
|
{
|
||||||
|
FivIoModel *self = FIV_IO_MODEL(object);
|
||||||
|
switch (property_id) {
|
||||||
|
case PROP_FILTERING:
|
||||||
|
g_value_set_boolean(value, self->filtering);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
fiv_io_model_set_property(
|
||||||
|
GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
|
||||||
|
{
|
||||||
|
FivIoModel *self = FIV_IO_MODEL(object);
|
||||||
|
switch (property_id) {
|
||||||
|
case PROP_FILTERING:
|
||||||
|
if (self->filtering == g_value_get_boolean(value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
self->filtering = !self->filtering;
|
||||||
|
g_object_notify_by_pspec(object, model_properties[PROP_FILTERING]);
|
||||||
|
(void) model_reload(self, NULL /* error */);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
fiv_io_model_class_init(FivIoModelClass *klass)
|
||||||
|
{
|
||||||
|
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
||||||
|
object_class->get_property = fiv_io_model_get_property;
|
||||||
|
object_class->set_property = fiv_io_model_set_property;
|
||||||
|
object_class->finalize = fiv_io_model_finalize;
|
||||||
|
|
||||||
|
model_properties[PROP_FILTERING] = g_param_spec_boolean(
|
||||||
|
"filtering", "Filtering", "Only show non-hidden, supported entries",
|
||||||
|
TRUE, G_PARAM_READWRITE);
|
||||||
|
g_object_class_install_properties(
|
||||||
|
object_class, N_PROPERTIES, model_properties);
|
||||||
|
|
||||||
|
// TODO(p): Arguments something like: index, added, removed.
|
||||||
|
model_signals[FILES_CHANGED] =
|
||||||
|
g_signal_new("files-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
|
||||||
|
NULL, NULL, NULL, G_TYPE_NONE, 0);
|
||||||
|
model_signals[SUBDIRECTORIES_CHANGED] =
|
||||||
|
g_signal_new("subdirectories-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
|
||||||
|
NULL, NULL, NULL, G_TYPE_NONE, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
static void
|
||||||
|
fiv_io_model_init(FivIoModel *self)
|
||||||
|
{
|
||||||
|
self->filtering = TRUE;
|
||||||
|
|
||||||
|
char **types = fiv_io_all_supported_media_types();
|
||||||
|
self->supported_globs = extract_mime_globs((const char **) types);
|
||||||
|
g_strfreev(types);
|
||||||
|
|
||||||
|
self->files = g_array_new(FALSE, TRUE, sizeof(ModelEntry));
|
||||||
|
self->subdirs = g_array_new(FALSE, TRUE, sizeof(ModelEntry));
|
||||||
|
g_array_set_clear_func(
|
||||||
|
self->subdirs, (GDestroyNotify) model_entry_finalize);
|
||||||
|
g_array_set_clear_func(
|
||||||
|
self->files, (GDestroyNotify) model_entry_finalize);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean
|
||||||
|
fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error)
|
||||||
|
{
|
||||||
|
g_return_val_if_fail(FIV_IS_IO_MODEL(self), FALSE);
|
||||||
|
g_return_val_if_fail(G_IS_FILE(directory), FALSE);
|
||||||
|
|
||||||
|
g_clear_object(&self->directory);
|
||||||
|
g_clear_object(&self->monitor);
|
||||||
|
self->directory = g_object_ref(directory);
|
||||||
|
|
||||||
|
// TODO(p): Process the ::changed signal.
|
||||||
|
self->monitor = g_file_monitor_directory(
|
||||||
|
directory, G_FILE_MONITOR_WATCH_MOVES, NULL, NULL /* error */);
|
||||||
|
return model_reload(self, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
GFile *
|
||||||
|
fiv_io_model_get_location(FivIoModel *self)
|
||||||
|
{
|
||||||
|
g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
|
||||||
|
return self->directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
GPtrArray *
|
||||||
|
fiv_io_model_get_files(FivIoModel *self)
|
||||||
|
{
|
||||||
|
GPtrArray *a = g_ptr_array_new_full(self->files->len, g_free);
|
||||||
|
for (guint i = 0; i < self->files->len; i++)
|
||||||
|
g_ptr_array_add(
|
||||||
|
a, g_strdup(g_array_index(self->files, ModelEntry, i).uri));
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
GPtrArray *
|
||||||
|
fiv_io_model_get_subdirectories(FivIoModel *self)
|
||||||
|
{
|
||||||
|
GPtrArray *a = g_ptr_array_new_full(self->subdirs->len, g_free);
|
||||||
|
for (guint i = 0; i < self->subdirs->len; i++)
|
||||||
|
g_ptr_array_add(
|
||||||
|
a, g_strdup(g_array_index(self->subdirs, ModelEntry, i).uri));
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Export ------------------------------------------------------------------
|
// --- Export ------------------------------------------------------------------
|
||||||
|
|
||||||
unsigned char *
|
unsigned char *
|
||||||
|
|
17
fiv-io.h
17
fiv-io.h
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
// fiv-io.h: image operations
|
// fiv-io.h: image operations
|
||||||
//
|
//
|
||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
|
@ -77,7 +77,20 @@ cairo_surface_t *fiv_io_open(
|
||||||
cairo_surface_t *fiv_io_open_from_data(const char *data, size_t len,
|
cairo_surface_t *fiv_io_open_from_data(const char *data, size_t len,
|
||||||
const gchar *uri, FivIoProfile profile, gboolean enhance, GError **error);
|
const gchar *uri, FivIoProfile profile, gboolean enhance, GError **error);
|
||||||
|
|
||||||
int fiv_io_filecmp(GFile *f1, GFile *f2);
|
// --- Filesystem --------------------------------------------------------------
|
||||||
|
|
||||||
|
#define FIV_TYPE_IO_MODEL (fiv_io_model_get_type())
|
||||||
|
G_DECLARE_FINAL_TYPE(FivIoModel, fiv_io_model, FIV, IO_MODEL, GObject)
|
||||||
|
|
||||||
|
/// Loads a directory. Clears itself even on failure.
|
||||||
|
gboolean fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error);
|
||||||
|
|
||||||
|
/// Returns the current location as a GFile.
|
||||||
|
/// There is no ownership transfer, and the object may be NULL.
|
||||||
|
GFile *fiv_io_model_get_location(FivIoModel *self);
|
||||||
|
|
||||||
|
GPtrArray *fiv_io_model_get_files(FivIoModel *self);
|
||||||
|
GPtrArray *fiv_io_model_get_subdirectories(FivIoModel *self);
|
||||||
|
|
||||||
// --- Export ------------------------------------------------------------------
|
// --- Export ------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
// fiv-sidebar.c: molesting GtkPlacesSidebar
|
// fiv-sidebar.c: molesting GtkPlacesSidebar
|
||||||
//
|
//
|
||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
#include "fiv-io.h" // fiv_io_filecmp
|
#include "fiv-io.h"
|
||||||
#include "fiv-sidebar.h"
|
#include "fiv-sidebar.h"
|
||||||
|
|
||||||
struct _FivSidebar {
|
struct _FivSidebar {
|
||||||
|
@ -25,7 +25,7 @@ struct _FivSidebar {
|
||||||
GtkPlacesSidebar *places;
|
GtkPlacesSidebar *places;
|
||||||
GtkWidget *toolbar;
|
GtkWidget *toolbar;
|
||||||
GtkWidget *listbox;
|
GtkWidget *listbox;
|
||||||
GFile *location;
|
FivIoModel *model;
|
||||||
};
|
};
|
||||||
|
|
||||||
G_DEFINE_TYPE(FivSidebar, fiv_sidebar, GTK_TYPE_SCROLLED_WINDOW)
|
G_DEFINE_TYPE(FivSidebar, fiv_sidebar, GTK_TYPE_SCROLLED_WINDOW)
|
||||||
|
@ -44,7 +44,10 @@ static void
|
||||||
fiv_sidebar_dispose(GObject *gobject)
|
fiv_sidebar_dispose(GObject *gobject)
|
||||||
{
|
{
|
||||||
FivSidebar *self = FIV_SIDEBAR(gobject);
|
FivSidebar *self = FIV_SIDEBAR(gobject);
|
||||||
g_clear_object(&self->location);
|
if (self->model) {
|
||||||
|
g_signal_handlers_disconnect_by_data(self->model, self);
|
||||||
|
g_clear_object(&self->model);
|
||||||
|
}
|
||||||
|
|
||||||
G_OBJECT_CLASS(fiv_sidebar_parent_class)->dispose(gobject);
|
G_OBJECT_CLASS(fiv_sidebar_parent_class)->dispose(gobject);
|
||||||
}
|
}
|
||||||
|
@ -128,29 +131,18 @@ create_row(GFile *file, const char *icon_name)
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
static gint
|
|
||||||
listbox_compare(
|
|
||||||
GtkListBoxRow *row1, GtkListBoxRow *row2, G_GNUC_UNUSED gpointer user_data)
|
|
||||||
{
|
|
||||||
return fiv_io_filecmp(
|
|
||||||
g_object_get_qdata(G_OBJECT(row1), fiv_sidebar_location_quark()),
|
|
||||||
g_object_get_qdata(G_OBJECT(row2), fiv_sidebar_location_quark()));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
update_location(FivSidebar *self, GFile *location)
|
update_location(FivSidebar *self)
|
||||||
{
|
{
|
||||||
if (location) {
|
GFile *location = fiv_io_model_get_location(self->model);
|
||||||
g_clear_object(&self->location);
|
if (!location)
|
||||||
self->location = g_object_ref(location);
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
gtk_places_sidebar_set_location(self->places, self->location);
|
gtk_places_sidebar_set_location(self->places, location);
|
||||||
gtk_container_foreach(GTK_CONTAINER(self->listbox),
|
gtk_container_foreach(GTK_CONTAINER(self->listbox),
|
||||||
(GtkCallback) gtk_widget_destroy, NULL);
|
(GtkCallback) gtk_widget_destroy, NULL);
|
||||||
g_return_if_fail(self->location != NULL);
|
|
||||||
|
|
||||||
GFile *iter = g_object_ref(self->location);
|
GFile *iter = g_object_ref(location);
|
||||||
GtkWidget *row = NULL;
|
GtkWidget *row = NULL;
|
||||||
while (TRUE) {
|
while (TRUE) {
|
||||||
GFile *parent = g_file_get_parent(iter);
|
GFile *parent = g_file_get_parent(iter);
|
||||||
|
@ -164,33 +156,17 @@ update_location(FivSidebar *self, GFile *location)
|
||||||
|
|
||||||
// Other options are "folder-{visiting,open}-symbolic", though the former
|
// Other options are "folder-{visiting,open}-symbolic", though the former
|
||||||
// is mildly inappropriate (means: open in another window).
|
// is mildly inappropriate (means: open in another window).
|
||||||
if ((row = create_row(self->location, "circle-filled-symbolic")))
|
if ((row = create_row(location, "circle-filled-symbolic")))
|
||||||
gtk_container_add(GTK_CONTAINER(self->listbox), row);
|
gtk_container_add(GTK_CONTAINER(self->listbox), row);
|
||||||
|
|
||||||
GFileEnumerator *enumerator = g_file_enumerate_children(self->location,
|
GPtrArray *subdirs = fiv_io_model_get_subdirectories(self->model);
|
||||||
G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME
|
for (guint i = 0; i < subdirs->len; i++) {
|
||||||
"," G_FILE_ATTRIBUTE_STANDARD_NAME
|
GFile *file = g_file_new_for_uri(subdirs->pdata[i]);
|
||||||
"," G_FILE_ATTRIBUTE_STANDARD_TYPE
|
if ((row = create_row(file, "go-down-symbolic")))
|
||||||
"," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
|
|
||||||
G_FILE_QUERY_INFO_NONE, NULL, NULL);
|
|
||||||
if (!enumerator)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// TODO(p): gtk_list_box_set_filter_func(), or even use a model,
|
|
||||||
// which could be shared with FivBrowser.
|
|
||||||
while (TRUE) {
|
|
||||||
GFileInfo *info = NULL;
|
|
||||||
GFile *child = NULL;
|
|
||||||
if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) ||
|
|
||||||
!info)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY &&
|
|
||||||
!g_file_info_get_is_hidden(info) &&
|
|
||||||
(row = create_row(child, "go-down-symbolic")))
|
|
||||||
gtk_container_add(GTK_CONTAINER(self->listbox), row);
|
gtk_container_add(GTK_CONTAINER(self->listbox), row);
|
||||||
|
g_object_unref(file);
|
||||||
}
|
}
|
||||||
g_object_unref(enumerator);
|
g_ptr_array_free(subdirs, TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -212,7 +188,7 @@ on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location,
|
||||||
g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, location, flags);
|
g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, location, flags);
|
||||||
|
|
||||||
// Deselect the item in GtkPlacesSidebar, if unsuccessful.
|
// Deselect the item in GtkPlacesSidebar, if unsuccessful.
|
||||||
update_location(self, NULL);
|
update_location(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -272,8 +248,8 @@ resolve_location(FivSidebar *self, const char *text)
|
||||||
g_file_peek_path(file))
|
g_file_peek_path(file))
|
||||||
return file;
|
return file;
|
||||||
|
|
||||||
GFile *absolute =
|
GFile *absolute = g_file_get_child_for_display_name(
|
||||||
g_file_get_child_for_display_name(self->location, text, NULL);
|
fiv_io_model_get_location(self->model), text, NULL);
|
||||||
if (!absolute)
|
if (!absolute)
|
||||||
return file;
|
return file;
|
||||||
|
|
||||||
|
@ -355,7 +331,7 @@ on_show_enter_location(
|
||||||
g_object_unref(completion);
|
g_object_unref(completion);
|
||||||
|
|
||||||
// Deselect the item in GtkPlacesSidebar, if unsuccessful.
|
// Deselect the item in GtkPlacesSidebar, if unsuccessful.
|
||||||
update_location(self, NULL);
|
update_location(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -389,8 +365,6 @@ fiv_sidebar_init(FivSidebar *self)
|
||||||
GTK_LIST_BOX(self->listbox), GTK_SELECTION_NONE);
|
GTK_LIST_BOX(self->listbox), GTK_SELECTION_NONE);
|
||||||
g_signal_connect(self->listbox, "row-activated",
|
g_signal_connect(self->listbox, "row-activated",
|
||||||
G_CALLBACK(on_open_breadcrumb), self);
|
G_CALLBACK(on_open_breadcrumb), self);
|
||||||
gtk_list_box_set_sort_func(
|
|
||||||
GTK_LIST_BOX(self->listbox), listbox_compare, self, NULL);
|
|
||||||
|
|
||||||
// Fill up what would otherwise be wasted space,
|
// Fill up what would otherwise be wasted space,
|
||||||
// as it is in the examples of Nautilus and Thunar.
|
// as it is in the examples of Nautilus and Thunar.
|
||||||
|
@ -417,11 +391,19 @@ fiv_sidebar_init(FivSidebar *self)
|
||||||
|
|
||||||
// --- Public interface --------------------------------------------------------
|
// --- Public interface --------------------------------------------------------
|
||||||
|
|
||||||
void
|
GtkWidget *
|
||||||
fiv_sidebar_set_location(FivSidebar *self, GFile *location)
|
fiv_sidebar_new(FivIoModel *model)
|
||||||
{
|
{
|
||||||
g_return_if_fail(FIV_IS_SIDEBAR(self));
|
g_return_val_if_fail(FIV_IS_IO_MODEL(model), NULL);
|
||||||
update_location(self, location);
|
|
||||||
|
FivSidebar *self = g_object_new(FIV_TYPE_SIDEBAR, NULL);
|
||||||
|
self->model = g_object_ref(model);
|
||||||
|
|
||||||
|
// TODO(p): There should be an extra signal to watch location changes only.
|
||||||
|
g_signal_connect_swapped(self->model, "subdirectories-changed",
|
||||||
|
G_CALLBACK(update_location), self);
|
||||||
|
|
||||||
|
return GTK_WIDGET(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
// fiv-sidebar.h: molesting GtkPlacesSidebar
|
// fiv-sidebar.h: molesting GtkPlacesSidebar
|
||||||
//
|
//
|
||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
|
@ -17,11 +17,13 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fiv-io.h"
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
#define FIV_TYPE_SIDEBAR (fiv_sidebar_get_type())
|
#define FIV_TYPE_SIDEBAR (fiv_sidebar_get_type())
|
||||||
G_DECLARE_FINAL_TYPE(FivSidebar, fiv_sidebar, FIV, SIDEBAR, GtkScrolledWindow)
|
G_DECLARE_FINAL_TYPE(FivSidebar, fiv_sidebar, FIV, SIDEBAR, GtkScrolledWindow)
|
||||||
|
|
||||||
void fiv_sidebar_set_location(FivSidebar *self, GFile *location);
|
GtkWidget *fiv_sidebar_new(FivIoModel *model);
|
||||||
void fiv_sidebar_show_enter_location(FivSidebar *self);
|
void fiv_sidebar_show_enter_location(FivSidebar *self);
|
||||||
GtkBox *fiv_sidebar_get_toolbar(FivSidebar *self);
|
GtkBox *fiv_sidebar_get_toolbar(FivSidebar *self);
|
||||||
|
|
Loading…
Reference in New Issue