aboutsummaryrefslogtreecommitdiff
path: root/fiv.c
diff options
context:
space:
mode:
Diffstat (limited to 'fiv.c')
-rw-r--r--fiv.c1309
1 files changed, 885 insertions, 424 deletions
diff --git a/fiv.c b/fiv.c
index d58a5a5..43041b0 100644
--- a/fiv.c
+++ b/fiv.c
@@ -1,7 +1,7 @@
//
// fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer
//
-// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -27,6 +27,7 @@
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
+#include <string.h>
#ifdef G_OS_WIN32
#include <io.h>
@@ -38,6 +39,7 @@
#include "fiv-browser.h"
#include "fiv-collection.h"
#include "fiv-io.h"
+#include "fiv-io-model.h"
#include "fiv-sidebar.h"
#include "fiv-thumbnail.h"
#include "fiv-view.h"
@@ -94,16 +96,16 @@ struct key_section {
static struct key help_keys_general[] = {
{"F1", "Show help"},
{"F10", "Open menu"},
- {"<Control>comma", "Preferences"},
- {"<Control>question", "Keyboard shortcuts"},
- {"q <Control>q", "Quit"},
- {"<Control>w", "Quit"},
+ {"<Primary>comma", "Preferences"},
+ {"<Primary>question", "Keyboard shortcuts"},
+ {"q <Primary>q", "Quit"},
+ {"<Primary>w", "Quit"},
{}
};
static struct key help_keys_navigation[] = {
- {"<Control>l", "Open location..."},
- {"<Control>n", "Open a new window"},
+ {"<Primary>l", "Open location..."},
+ {"<Primary>n", "Open a new window"},
{"<Alt>Left", "Go back in history"},
{"<Alt>Right", "Go forward in history"},
{}
@@ -120,16 +122,20 @@ static struct key_group help_keys_browser[] = {
{"General: Navigation", help_keys_navigation},
{"General: View", help_keys_view},
{"Navigation", (struct key[]) {
- {"<Alt>Up", "Go to parent directory"},
{"<Alt>Home", "Go home"},
+ {"<Alt>Up", "Go to parent directory"},
+ {"bracketleft", "Go to previous directory in tree"},
+ {"bracketright", "Go to next directory in tree"},
{"Return", "Open selected item"},
{"<Alt>Return", "Show file information"},
{}
}},
{"View", (struct key[]) {
+ {"F7", "Toggle toolbar"},
{"F9", "Toggle navigation sidebar"},
{"F5 r <Control>r", "Reload"},
{"h <Control>h", "Toggle hiding unsupported files"},
+ {"t <Control>t", "Toggle showing filenames"},
{"<Control>plus", "Larger thumbnails"},
{"<Control>minus", "Smaller thumbnails"},
{}
@@ -148,7 +154,7 @@ static struct key_group help_keys_viewer[] = {
{}
}},
{"View", (struct key[]) {
- {"F9", "Toggle toolbar"},
+ {"F7", "Toggle toolbar"},
{"F5 r <Primary>r", "Reload"},
{}
}},
@@ -383,12 +389,6 @@ on_about_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
GtkStyleContext *style = gtk_widget_get_style_context(widget);
gtk_render_background(style, cr, 0, 0, allocation.width, allocation.height);
- // The transformation matrix turns out/is applied wrongly on Quartz.
- gboolean broken_backend = cairo_surface_get_type(cairo_get_target(cr)) ==
- CAIRO_SURFACE_TYPE_QUARTZ;
- if (broken_backend)
- cairo_push_group(cr);
-
cairo_translate(cr, (allocation.width - ABOUT_SIZE * ABOUT_SCALE) / 2,
ABOUT_SIZE * ABOUT_SCALE / 4);
cairo_scale(cr, ABOUT_SCALE, ABOUT_SCALE);
@@ -414,11 +414,6 @@ on_about_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
cairo_restore(cr);
draw_ligature(cr);
-
- if (broken_backend) {
- cairo_pop_group_to_source(cr);
- cairo_paint(cr);
- }
return TRUE;
}
@@ -459,7 +454,7 @@ show_about_dialog(GtkWidget *parent)
GtkWidget *website = gtk_label_new(NULL);
gtk_label_set_selectable(GTK_LABEL(website), TRUE);
- const char *url = "https://git.janouch.name/p/" PROJECT_NAME;
+ const char *url = PROJECT_URL;
gchar *link = g_strdup_printf("<a href='%s'>%s</a>", url, url);
gtk_label_set_markup(GTK_LABEL(website), link);
g_free(link);
@@ -515,12 +510,146 @@ show_about_dialog(GtkWidget *parent)
cairo_pattern_destroy(ctx.v_pattern);
}
+// --- Settings ----------------------------------------------------------------
+
+static void
+preferences_make_row(
+ GtkWidget *grid, int *row, GSettings *settings, GSettingsSchemaKey *key)
+{
+ const char *name = g_settings_schema_key_get_name(key);
+ const char *summary = g_settings_schema_key_get_summary(key);
+ const char *description = g_settings_schema_key_get_description(key);
+
+ GtkWidget *widget = NULL;
+ const GVariantType *type = g_settings_schema_key_get_value_type(key);
+ if (g_variant_type_equal(type, G_VARIANT_TYPE_BOOLEAN)) {
+ widget = gtk_switch_new();
+ g_settings_bind(
+ settings, name, widget, "active", G_SETTINGS_BIND_DEFAULT);
+ } else {
+ const gchar *type = NULL;
+ GVariant *value = NULL, *range = g_settings_schema_key_get_range(key);
+ g_variant_get(range, "(&sv)", &type, &value);
+ GVariantIter iter = {};
+ g_variant_iter_init(&iter, value);
+ if (g_str_equal(type, "enum")) {
+ widget = gtk_combo_box_text_new();
+
+ GVariant *child = NULL;
+ while ((child = g_variant_iter_next_value(&iter))) {
+ const char *id = g_variant_get_string(child, NULL);
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(widget), id, id);
+ g_variant_unref(child);
+ }
+
+ g_settings_bind(
+ settings, name, widget, "active-id", G_SETTINGS_BIND_DEFAULT);
+ }
+ g_variant_unref(value);
+ g_variant_unref(range);
+ }
+
+ // Ignore unimplemented value types.
+ if (!widget)
+ return;
+
+ GtkWidget *label = gtk_label_new(summary ? summary : name);
+ gtk_label_set_xalign(GTK_LABEL(label), 0);
+ gtk_widget_set_hexpand(label, TRUE);
+ gtk_grid_attach(GTK_GRID(grid), label, 0, (*row), 1, 1);
+ gtk_widget_set_halign(widget, GTK_ALIGN_END);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, (*row)++, 1, 1);
+
+ if (description) {
+ GtkWidget *label = gtk_label_new(description);
+ PangoAttrList *attr_list = pango_attr_list_new();
+ pango_attr_list_insert(
+ attr_list, pango_attr_scale_new(PANGO_SCALE_SMALL));
+ gtk_label_set_attributes(
+ GTK_LABEL(label), pango_attr_list_ref(attr_list));
+ pango_attr_list_unref(attr_list);
+
+ gtk_label_set_xalign(GTK_LABEL(label), 0);
+ gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
+ gtk_widget_set_sensitive(label, FALSE);
+ gtk_widget_set_size_request(label, 0, -1);
+ gtk_grid_attach(GTK_GRID(grid), label, 0, (*row)++, 1, 1);
+ }
+}
+
+static void
+show_preferences(GtkWidget *parent)
+{
+ GSettingsSchema *schema = NULL;
+ GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
+ g_object_get(settings, "settings-schema", &schema, NULL);
+
+ GtkWidget *dialog = gtk_widget_new(GTK_TYPE_DIALOG,
+ "use-header-bar", TRUE,
+ "title", "Preferences",
+ "transient-for", parent,
+ "destroy-with-parent", TRUE, NULL);
+
+ GtkWidget *grid = gtk_grid_new();
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 12);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 24);
+ g_object_set(grid, "margin", 12, NULL);
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
+ grid, TRUE, TRUE, 0);
+
+ int row = 0;
+ gchar **keys = g_settings_schema_list_keys(schema);
+ for (gchar **p = keys; *p; p++) {
+#ifndef GDK_WINDOWING_X11
+ if (g_str_equal(*p, "native-view-window"))
+ continue;
+#endif
+ GSettingsSchemaKey *key = g_settings_schema_get_key(schema, *p);
+ preferences_make_row(grid, &row, settings, key);
+ g_settings_schema_key_unref(key);
+ }
+ g_strfreev(keys);
+ g_object_unref(settings);
+
+ gtk_window_set_default_size(GTK_WINDOW(dialog), 600, -1);
+ gtk_widget_show_all(dialog);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+}
+
// --- Main --------------------------------------------------------------------
// TODO(p): See if it's possible to give separators room to shrink
// by some minor amount of pixels, margin-wise.
#define B make_toolbar_button
#define T make_toolbar_toggle
+#define R make_toolbar_radio
+#define BROWSEBAR(XX) \
+ XX(SIDEBAR, T("sidebar-show-symbolic", "Show sidebar")) \
+ XX(S1, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
+ XX(DIR_PREVIOUS, B("go-previous-symbolic", "Previous directory")) \
+ XX(DIR_NEXT, B("go-next-symbolic", "Next directory")) \
+ XX(S2, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
+ XX(PLUS, B("zoom-in-symbolic", "Larger thumbnails")) \
+ XX(MINUS, B("zoom-out-symbolic", "Smaller thumbnails")) \
+ XX(S3, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
+ XX(FILENAMES, T("text-symbolic", "Show filenames")) \
+ XX(FILTER, T("funnel-symbolic", "Hide unsupported files")) \
+ XX(S4, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
+ XX(SORT_DIR, B("view-sort-ascending-symbolic", "Sort ascending")) \
+ XX(SORT_NAME, R("Name", "Sort by filename")) \
+ XX(SORT_TIME, R("Time", "Sort by time of last modification")) \
+ XX(S5, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
+ /* We are YouTube. */ \
+ XX(FULLSCREEN, B("view-fullscreen-symbolic", "Fullscreen"))
+
+enum {
+#define XX(id, constructor) BROWSEBAR_ ## id,
+ BROWSEBAR(XX)
+#undef XX
+ BROWSEBAR_COUNT
+};
+
#define TOOLBAR(XX) \
XX(BROWSE, B("view-grid-symbolic", "Browse")) \
XX(FILE_PREVIOUS, B("go-previous-symbolic", "Previous file")) \
@@ -572,10 +701,9 @@ struct {
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
- gint files_index; ///< Where "uri" is within "files"
+ gint files_index; ///< Where "uri" is within the model's files
GtkWidget *window;
GtkWidget *menu;
@@ -583,11 +711,8 @@ struct {
GtkWidget *browser_paned;
GtkWidget *browser_sidebar;
- GtkWidget *plus;
- GtkWidget *minus;
- GtkWidget *funnel;
- GtkWidget *sort_field[FIV_IO_MODEL_SORT_COUNT];
- GtkWidget *sort_direction[2];
+ GtkWidget *browser_toolbar;
+ GtkWidget *browsebar[BROWSEBAR_COUNT];
GtkWidget *browser_scroller;
GtkWidget *browser;
@@ -663,56 +788,38 @@ parent_uri(GFile *child_file)
static void
update_files_index(void)
{
+ gsize files_len = 0;
+ FivIoModelEntry *const *files = fiv_io_model_get_files(g.model, &files_len);
+
g.files_index = -1;
- for (guint i = 0; i < g.files->len; i++)
- if (!g_strcmp0(g.uri, g_ptr_array_index(g.files, i)))
+ for (guint i = 0; i < files_len; i++)
+ if (!g_strcmp0(g.uri, files[i]->uri))
g.files_index = i;
}
static void
-load_directory_without_reload(const char *uri)
+change_directory_without_reload(const char *uri)
{
- gchar *uri_duplicated = g_strdup(uri);
- if (g.directory_back && !strcmp(uri, g.directory_back->data)) {
- // We're going back in history.
- if (g.directory) {
- g.directory_forward =
- g_list_prepend(g.directory_forward, g.directory);
- g.directory = NULL;
- }
+ if (g.directory) {
+ // Note that this function can be passed g.directory directly.
+ if (!strcmp(uri, g.directory))
+ return;
- GList *link = g.directory_back;
- g.directory_back = g_list_remove_link(g.directory_back, link);
- g_list_free_full(link, g_free);
- } else if (g.directory_forward && !strcmp(uri, g.directory_forward->data)) {
- // We're going forward in history.
- if (g.directory) {
- g.directory_back =
- g_list_prepend(g.directory_back, g.directory);
- g.directory = NULL;
- }
-
- GList *link = g.directory_forward;
- g.directory_forward = g_list_remove_link(g.directory_forward, link);
- g_list_free_full(link, g_free);
- } else if (g.directory && strcmp(uri, g.directory)) {
// We're on a new subpath.
g_list_free_full(g.directory_forward, g_free);
g.directory_forward = NULL;
g.directory_back = g_list_prepend(g.directory_back, g.directory);
- g.directory = NULL;
}
- g_free(g.directory);
- g.directory = uri_duplicated;
+ g.directory = g_strdup(uri);
}
static void
load_directory_without_switching(const char *uri)
{
if (uri) {
- load_directory_without_reload(uri);
+ change_directory_without_reload(uri);
GtkAdjustment *vadjustment = gtk_scrolled_window_get_vadjustment(
GTK_SCROLLED_WINDOW(g.browser_scroller));
@@ -723,7 +830,7 @@ load_directory_without_switching(const char *uri)
GError *error = NULL;
GFile *file = g_file_new_for_uri(g.directory);
if (fiv_io_model_open(g.model, file, &error)) {
- // This is handled by our ::files-changed callback.
+ // This is handled by our ::reloaded callback.
} else if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) {
g_error_free(error);
} else {
@@ -738,9 +845,6 @@ load_directory(const char *uri)
{
load_directory_without_switching(uri);
- // XXX: When something outside the filtered entries is open, the index is
- // kept at -1, and browsing doesn't work. How to behave here?
- // Should we add it to the pointer array as an exception?
if (uri) {
switch_to_browser_noselect();
@@ -750,23 +854,71 @@ load_directory(const char *uri)
}
static void
-on_model_files_changed(FivIoModel *model, G_GNUC_UNUSED gpointer user_data)
+go_back(void)
+{
+ if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box) {
+ switch_to_browser_noselect();
+ } else if (g.directory_back) {
+ if (g.directory)
+ g.directory_forward =
+ g_list_prepend(g.directory_forward, g.directory);
+
+ const gchar *uri = g.directory = g.directory_back->data;
+
+ GList *link = g.directory_back;
+ g.directory_back = g_list_remove_link(g.directory_back, link);
+ g_list_free(link);
+
+ load_directory(uri);
+ }
+}
+
+static void
+go_forward(void)
+{
+ if (g.directory_forward) {
+ if (g.directory)
+ g.directory_back =
+ g_list_prepend(g.directory_back, g.directory);
+
+ const gchar *uri = g.directory = g.directory_forward->data;
+
+ GList *link = g.directory_forward;
+ g.directory_forward = g_list_remove_link(g.directory_forward, link);
+ g_list_free(link);
+
+ load_directory(uri);
+ } else if (g.uri) {
+ switch_to_view();
+ }
+}
+
+static void
+on_model_reloaded(FivIoModel *model, G_GNUC_UNUSED gpointer user_data)
{
g_return_if_fail(model == g.model);
- gsize len = 0;
- const FivIoModelEntry *files = fiv_io_model_get_files(g.model, &len);
- g_ptr_array_free(g.files, TRUE);
- g.files = g_ptr_array_new_full(len, g_free);
- for (gsize i = 0; i < len; i++)
- g_ptr_array_add(g.files, g_strdup(files[i].uri));
+ gsize files_len = 0;
+ (void) fiv_io_model_get_files(g.model, &files_len);
update_files_index();
- gtk_widget_set_sensitive(
- g.toolbar[TOOLBAR_FILE_PREVIOUS], g.files->len > 1);
- gtk_widget_set_sensitive(
- g.toolbar[TOOLBAR_FILE_NEXT], g.files->len > 1);
+ gtk_widget_set_sensitive(g.toolbar[TOOLBAR_FILE_PREVIOUS], files_len > 1);
+ gtk_widget_set_sensitive(g.toolbar[TOOLBAR_FILE_NEXT], files_len > 1);
+}
+
+static void
+on_model_files_changed(FivIoModel *model, G_GNUC_UNUSED FivIoModelEntry *old,
+ G_GNUC_UNUSED FivIoModelEntry *new, G_GNUC_UNUSED gpointer user_data)
+{
+ on_model_reloaded(model, NULL);
+}
+
+static void
+on_sidebar_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)
+{
+ gboolean active = gtk_toggle_button_get_active(button);
+ gtk_widget_set_visible(g.browser_sidebar, active);
}
static void
@@ -777,21 +929,33 @@ on_filtering_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)
}
static void
-on_sort_field(G_GNUC_UNUSED GtkMenuItem *item, gpointer data)
+on_filenames_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)
+{
+ gboolean active = gtk_toggle_button_get_active(button);
+ g_object_set(g.browser, "show-labels", active, NULL);
+}
+
+static void
+on_sort_field(G_GNUC_UNUSED GtkToggleButton *button, gpointer data)
{
- int old = -1, new = (int) (intptr_t) data;
+ gboolean active = gtk_toggle_button_get_active(button);
+ if (!active)
+ return;
+
+ FivIoModelSort old = FIV_IO_MODEL_SORT_COUNT;
+ FivIoModelSort new = (FivIoModelSort) (intptr_t) data;
g_object_get(g.model, "sort-field", &old, NULL);
if (old != new)
g_object_set(g.model, "sort-field", new, NULL);
}
static void
-on_sort_direction(G_GNUC_UNUSED GtkMenuItem *item, gpointer data)
+on_sort_direction(G_GNUC_UNUSED GtkToggleButton *button,
+ G_GNUC_UNUSED gpointer data)
{
- gboolean old = FALSE, new = (gboolean) (intptr_t) data;
+ gboolean old = FALSE;
g_object_get(g.model, "sort-descending", &old, NULL);
- if (old != new)
- g_object_set(g.model, "sort-descending", new, NULL);
+ g_object_set(g.model, "sort-descending", !old, NULL);
}
static void
@@ -828,13 +992,17 @@ open_image(const char *uri)
// So that load_directory() itself can be used for reloading.
gchar *parent = parent_uri(file);
g_object_unref(file);
- if (!g.files->len /* hack to always load the directory after launch */ ||
- !g.directory || strcmp(parent, g.directory))
+ if (!fiv_io_model_get_location(g.model) || !g.directory ||
+ strcmp(parent, g.directory))
load_directory_without_switching(parent);
else
update_files_index();
g_free(parent);
+ // XXX: When something outside currently filtered entries is open,
+ // g.files_index is kept at -1, and browsing doesn't work.
+ // How to behave here?
+
switch_to_view();
}
@@ -902,18 +1070,22 @@ on_open(void)
static void
on_previous(void)
{
+ gsize files_len = 0;
+ FivIoModelEntry *const *files = fiv_io_model_get_files(g.model, &files_len);
if (g.files_index >= 0) {
- int previous = (g.files->len + g.files_index - 1) % g.files->len;
- open_image(g_ptr_array_index(g.files, previous));
+ int previous = (files_len + g.files_index - 1) % files_len;
+ open_image(files[previous]->uri);
}
}
static void
on_next(void)
{
+ gsize files_len = 0;
+ FivIoModelEntry *const *files = fiv_io_model_get_files(g.model, &files_len);
if (g.files_index >= 0) {
- int next = (g.files_index + 1) % g.files->len;
- open_image(g_ptr_array_index(g.files, next));
+ int next = (g.files_index + 1) % files_len;
+ open_image(files[next]->uri);
}
}
@@ -1089,6 +1261,40 @@ on_view_drag_data_received(G_GNUC_UNUSED GtkWidget *widget,
}
static void
+on_notify_sidebar_visible(
+ GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED 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(g.browsebar[BROWSEBAR_SIDEBAR]), b);
+}
+
+static void
+on_dir_previous(void)
+{
+ GFile *directory = fiv_io_model_get_previous_directory(g.model);
+ if (directory) {
+ gchar *uri = g_file_get_uri(directory);
+ g_object_unref(directory);
+ load_directory(uri);
+ g_free(uri);
+ }
+}
+
+static void
+on_dir_next(void)
+{
+ GFile *directory = fiv_io_model_get_next_directory(g.model);
+ if (directory) {
+ gchar *uri = g_file_get_uri(directory);
+ g_object_unref(directory);
+ load_directory(uri);
+ g_free(uri);
+ }
+}
+
+static void
on_toolbar_zoom(G_GNUC_UNUSED GtkButton *button, gpointer user_data)
{
FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT;
@@ -1105,10 +1311,22 @@ static void
on_notify_thumbnail_size(
GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data)
{
- FivThumbnailSize size = 0;
+ FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT;
g_object_get(object, g_param_spec_get_name(param_spec), &size, NULL);
- gtk_widget_set_sensitive(g.plus, size < FIV_THUMBNAIL_SIZE_MAX);
- gtk_widget_set_sensitive(g.minus, size > FIV_THUMBNAIL_SIZE_MIN);
+ gtk_widget_set_sensitive(
+ g.browsebar[BROWSEBAR_PLUS], size < FIV_THUMBNAIL_SIZE_MAX);
+ gtk_widget_set_sensitive(
+ g.browsebar[BROWSEBAR_MINUS], size > FIV_THUMBNAIL_SIZE_MIN);
+}
+
+static void
+on_notify_show_labels(
+ GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data)
+{
+ gboolean show_labels = 0;
+ g_object_get(object, g_param_spec_get_name(param_spec), &show_labels, NULL);
+ gtk_toggle_button_set_active(
+ GTK_TOGGLE_BUTTON(g.browsebar[BROWSEBAR_FILENAMES]), show_labels);
}
static void
@@ -1117,7 +1335,8 @@ on_notify_filtering(
{
gboolean b = FALSE;
g_object_get(object, g_param_spec_get_name(param_spec), &b, NULL);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g.funnel), b);
+ gtk_toggle_button_set_active(
+ GTK_TOGGLE_BUTTON(g.browsebar[BROWSEBAR_FILTER]), b);
}
static void
@@ -1126,8 +1345,8 @@ on_notify_sort_field(
{
gint field = -1;
g_object_get(object, g_param_spec_get_name(param_spec), &field, NULL);
- gtk_check_menu_item_set_active(
- GTK_CHECK_MENU_ITEM(g.sort_field[field]), TRUE);
+ gtk_toggle_button_set_active(
+ GTK_TOGGLE_BUTTON(g.browsebar[BROWSEBAR_SORT_NAME + field]), TRUE);
}
static void
@@ -1136,8 +1355,18 @@ on_notify_sort_descending(
{
gboolean b = FALSE;
g_object_get(object, g_param_spec_get_name(param_spec), &b, NULL);
- gtk_check_menu_item_set_active(
- GTK_CHECK_MENU_ITEM(g.sort_direction[b]), TRUE);
+
+ const char *title = b
+ ? "Sort ascending"
+ : "Sort descending";
+ const char *name = b
+ ? "view-sort-ascending-symbolic"
+ : "view-sort-descending-symbolic";
+
+ GtkButton *button = GTK_BUTTON(g.browsebar[BROWSEBAR_SORT_DIR]);
+ GtkImage *image = GTK_IMAGE(gtk_button_get_image(button));
+ gtk_widget_set_tooltip_text(GTK_WIDGET(button), title);
+ gtk_image_set_from_icon_name(image, name, GTK_ICON_SIZE_BUTTON);
}
static void
@@ -1161,9 +1390,14 @@ on_window_state_event(G_GNUC_UNUSED GtkWidget *widget,
? "view-restore-symbolic"
: "view-fullscreen-symbolic";
- GtkButton *button = GTK_BUTTON(g.toolbar[TOOLBAR_FULLSCREEN]);
- GtkImage *image = GTK_IMAGE(gtk_button_get_image(button));
- gtk_image_set_from_icon_name(image, name, GTK_ICON_SIZE_BUTTON);
+ gtk_image_set_from_icon_name(
+ GTK_IMAGE(gtk_button_get_image(
+ GTK_BUTTON(g.toolbar[TOOLBAR_FULLSCREEN]))),
+ name, GTK_ICON_SIZE_BUTTON);
+ gtk_image_set_from_icon_name(
+ GTK_IMAGE(gtk_button_get_image(
+ GTK_BUTTON(g.browsebar[BROWSEBAR_FULLSCREEN]))),
+ name, GTK_ICON_SIZE_BUTTON);
}
static void
@@ -1214,20 +1448,6 @@ show_help_shortcuts(void)
}
static void
-show_preferences(void)
-{
- char *argv[] = {"dconf-editor", PROJECT_NS PROJECT_NAME, NULL};
- GError *error = NULL;
- if (!g_spawn_async(
- NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &error)) {
- if (g_error_matches(error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT))
- g_prefix_error_literal(&error,
- "Please install dconf-editor, or use the gsettings utility.\n");
- show_error_dialog(error);
- }
-}
-
-static void
toggle_sunlight(void)
{
GtkSettings *settings = gtk_settings_get_default();
@@ -1237,94 +1457,16 @@ toggle_sunlight(void)
g_object_set(settings, property, !set, NULL);
}
-// Cursor keys, e.g., simply cannot be bound through accelerators
-// (and GtkWidget::keynav-failed would arguably be an awful solution).
-//
-// GtkBindingSets can be added directly through GtkStyleContext,
-// but that would still require setting up action signals on the widget class,
-// which is extremely cumbersome. GtkWidget::move-focus has no return value,
-// so we can't override that and abort further handling.
-//
-// Therefore, bind directly to keypresses. Order can be fine-tuned with
-// g_signal_connect{,after}(), or overriding the handler and either tactically
-// chaining up or using gtk_window_propagate_key_event().
static gboolean
on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
G_GNUC_UNUSED gpointer data)
{
switch (event->state & gtk_accelerator_get_default_mod_mask()) {
- case GDK_MOD1_MASK | GDK_SHIFT_MASK:
- if (event->keyval == GDK_KEY_D)
- toggle_sunlight();
- break;
case GDK_CONTROL_MASK:
- case GDK_CONTROL_MASK | GDK_SHIFT_MASK:
switch (event->keyval) {
case GDK_KEY_h:
- gtk_button_clicked(GTK_BUTTON(g.funnel));
- return TRUE;
- case GDK_KEY_l:
- fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar));
- return TRUE;
- case GDK_KEY_n:
- if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
- spawn_uri(g.uri);
- else
- spawn_uri(g.directory);
- return TRUE;
- case GDK_KEY_o:
- on_open();
- return TRUE;
- case GDK_KEY_q:
- case GDK_KEY_w:
- gtk_widget_destroy(g.window);
- return TRUE;
-
- case GDK_KEY_question:
- show_help_shortcuts();
- return TRUE;
- case GDK_KEY_comma:
- show_preferences();
- return TRUE;
- }
- break;
- case GDK_MOD1_MASK:
- switch (event->keyval) {
- case GDK_KEY_Left:
- if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
- switch_to_browser_noselect();
- else if (g.directory_back)
- load_directory(g.directory_back->data);
- return TRUE;
- case GDK_KEY_Right:
- if (g.directory_forward)
- load_directory(g.directory_forward->data);
- else if (g.uri)
- switch_to_view();
- return TRUE;
- }
- break;
- case GDK_SHIFT_MASK:
- switch (event->keyval) {
- case GDK_KEY_F1:
- show_about_dialog(g.window);
- return TRUE;
- }
- break;
- case 0:
- switch (event->keyval) {
- case GDK_KEY_q:
- gtk_widget_destroy(g.window);
- return TRUE;
- case GDK_KEY_o:
- on_open();
- return TRUE;
- case GDK_KEY_F1:
- show_help_contents();
- return TRUE;
- case GDK_KEY_F11:
- case GDK_KEY_f:
- toggle_fullscreen();
+ // XXX: Command-H is already occupied on macOS.
+ gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER]));
return TRUE;
}
}
@@ -1340,8 +1482,15 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
gtk_accelerator_parse(accelerator, &key, &mods);
g_free(accelerator);
+ // TODO(p): See how Unity 7 behaves,
+ // we might want to keep GtkApplicationWindow:show-menubar then.
+ gboolean shell_shows_menubar = FALSE;
+ (void) g_object_get(gtk_settings_get_default(),
+ "gtk-shell-shows-menubar", &shell_shows_menubar, NULL);
+
guint mask = gtk_accelerator_get_default_mod_mask();
- if (key && event->keyval == key && (event->state & mask) == mods) {
+ if (key && event->keyval == key && (event->state & mask) == mods &&
+ !shell_shows_menubar) {
gtk_widget_show(g.menu);
// _gtk_menu_shell_set_keyboard_mode() is private.
@@ -1351,6 +1500,17 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
return FALSE;
}
+// Cursor keys, e.g., simply cannot be bound through accelerators
+// (and GtkWidget::keynav-failed would arguably be an awful solution).
+//
+// GtkBindingSets can be added directly through GtkStyleContext,
+// but that would still require setting up action signals on the widget class,
+// which is extremely cumbersome. GtkWidget::move-focus has no return value,
+// so we can't override that and abort further handling.
+//
+// Therefore, bind directly to keypresses. Order can be fine-tuned with
+// g_signal_connect{,after}(), or overriding the handler and either tactically
+// chaining up or using gtk_window_propagate_key_event().
static gboolean
on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
G_GNUC_UNUSED gpointer data)
@@ -1358,7 +1518,7 @@ on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
switch (event->state & gtk_accelerator_get_default_mod_mask()) {
case 0:
switch (event->keyval) {
- case GDK_KEY_F9:
+ case GDK_KEY_F7:
gtk_widget_set_visible(g.view_toolbar,
!gtk_widget_is_visible(g.view_toolbar));
return TRUE;
@@ -1395,6 +1555,9 @@ on_key_press_browser_paned(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
case GDK_KEY_r:
load_directory(NULL);
return TRUE;
+ case GDK_KEY_t:
+ gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILENAMES]));
+ return TRUE;
}
break;
case GDK_MOD1_MASK:
@@ -1417,21 +1580,35 @@ on_key_press_browser_paned(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
break;
case 0:
switch (event->keyval) {
+ case GDK_KEY_F7:
+ gtk_widget_set_visible(g.browser_toolbar,
+ !gtk_widget_is_visible(g.browser_toolbar));
+ return TRUE;
case GDK_KEY_F9:
gtk_widget_set_visible(g.browser_sidebar,
!gtk_widget_is_visible(g.browser_sidebar));
return TRUE;
+ case GDK_KEY_bracketleft:
+ on_dir_previous();
+ return TRUE;
+ case GDK_KEY_bracketright:
+ on_dir_next();
+ return TRUE;
+
case GDK_KEY_Escape:
fiv_browser_select(FIV_BROWSER(g.browser), NULL);
return TRUE;
case GDK_KEY_h:
- gtk_button_clicked(GTK_BUTTON(g.funnel));
+ gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILTER]));
return TRUE;
case GDK_KEY_F5:
case GDK_KEY_r:
load_directory(NULL);
return TRUE;
+ case GDK_KEY_t:
+ gtk_button_clicked(GTK_BUTTON(g.browsebar[BROWSEBAR_FILENAMES]));
+ return TRUE;
}
}
return FALSE;
@@ -1445,7 +1622,7 @@ on_button_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event)
switch (event->button) {
case 4: // back (GdkWin32, GdkQuartz)
case 8: // back
- switch_to_browser_noselect();
+ go_back();
return TRUE;
case GDK_BUTTON_PRIMARY:
if (event->type == GDK_2BUTTON_PRESS) {
@@ -1467,15 +1644,11 @@ on_button_press_browser_paned(
switch (event->button) {
case 4: // back (GdkWin32, GdkQuartz)
case 8: // back
- if (g.directory_back)
- load_directory(g.directory_back->data);
+ go_back();
return TRUE;
case 5: // forward (GdkWin32, GdkQuartz)
case 9: // forward
- if (g.directory_forward)
- load_directory(g.directory_forward->data);
- else if (g.uri)
- switch_to_view();
+ go_forward();
return TRUE;
default:
return FALSE;
@@ -1510,6 +1683,81 @@ make_toolbar_toggle(const char *symbolic, const char *tooltip)
return button;
}
+static GtkWidget *
+make_toolbar_radio(const char *label, const char *tooltip)
+{
+ GtkWidget *button = gtk_radio_button_new_with_label(NULL, label);
+ gtk_widget_set_tooltip_text(button, tooltip);
+ gtk_widget_set_focus_on_click(button, FALSE);
+ gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(button), FALSE);
+ gtk_style_context_add_class(
+ gtk_widget_get_style_context(button), GTK_STYLE_CLASS_FLAT);
+ return button;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+browsebar_connect(int index, GCallback callback)
+{
+ g_signal_connect_swapped(g.browsebar[index], "clicked", callback, NULL);
+}
+
+static GtkWidget *
+make_browser_toolbar(void)
+{
+#define XX(id, constructor) g.browsebar[BROWSEBAR_ ## id] = constructor;
+ BROWSEBAR(XX)
+#undef XX
+
+ // GtkStatusBar solves a problem we do not have here.
+ GtkWidget *browser_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_style_context_add_class(
+ gtk_widget_get_style_context(browser_toolbar), "fiv-toolbar");
+ GtkBox *box = GTK_BOX(browser_toolbar);
+
+ // Exploring different versions of awkward layouts.
+ for (int i = 0; i <= BROWSEBAR_S2; i++)
+ gtk_box_pack_start(box, g.browsebar[i], FALSE, FALSE, 0);
+ for (int i = BROWSEBAR_COUNT; --i >= BROWSEBAR_S5; )
+ gtk_box_pack_end(box, g.browsebar[i], FALSE, FALSE, 0);
+
+ GtkWidget *center = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ for (int i = BROWSEBAR_S2; ++i < BROWSEBAR_S5; )
+ gtk_box_pack_start(GTK_BOX(center), g.browsebar[i], FALSE, FALSE, 0);
+ gtk_box_set_center_widget(box, center);
+
+ g_signal_connect(g.browsebar[BROWSEBAR_SIDEBAR], "toggled",
+ G_CALLBACK(on_sidebar_toggled), NULL);
+
+ browsebar_connect(BROWSEBAR_DIR_PREVIOUS, G_CALLBACK(on_dir_previous));
+ browsebar_connect(BROWSEBAR_DIR_NEXT, G_CALLBACK(on_dir_next));
+ browsebar_connect(BROWSEBAR_SORT_DIR, G_CALLBACK(on_sort_direction));
+ browsebar_connect(BROWSEBAR_FULLSCREEN, G_CALLBACK(toggle_fullscreen));
+
+ g_signal_connect(g.browsebar[BROWSEBAR_PLUS], "clicked",
+ G_CALLBACK(on_toolbar_zoom), (gpointer) +1);
+ g_signal_connect(g.browsebar[BROWSEBAR_MINUS], "clicked",
+ G_CALLBACK(on_toolbar_zoom), (gpointer) -1);
+
+ g_signal_connect(g.browsebar[BROWSEBAR_FILTER], "toggled",
+ G_CALLBACK(on_filtering_toggled), NULL);
+ g_signal_connect(g.browsebar[BROWSEBAR_FILENAMES], "toggled",
+ G_CALLBACK(on_filenames_toggled), NULL);
+
+ GtkRadioButton *last = GTK_RADIO_BUTTON(g.browsebar[BROWSEBAR_SORT_NAME]);
+ for (int i = BROWSEBAR_SORT_NAME; i <= BROWSEBAR_SORT_TIME; i++) {
+ GtkRadioButton *radio = GTK_RADIO_BUTTON(g.browsebar[i]);
+ g_signal_connect(radio, "toggled", G_CALLBACK(on_sort_field),
+ (gpointer) (gintptr) i - BROWSEBAR_SORT_NAME);
+ gtk_radio_button_join_group(radio, last);
+ last = radio;
+ }
+ return browser_toolbar;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
static void
on_view_actions_changed(void)
{
@@ -1655,7 +1903,8 @@ make_view_toolbar(void)
// GtkStatusBar solves a problem we do not have here.
GtkWidget *view_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_widget_set_name(view_toolbar, "toolbar");
+ gtk_style_context_add_class(
+ gtk_widget_get_style_context(view_toolbar), "fiv-toolbar");
GtkBox *box = GTK_BOX(view_toolbar);
// Exploring different versions of awkward layouts.
@@ -1746,124 +1995,202 @@ make_browser_sidebar(FivIoModel *model)
g_signal_connect(sidebar, "open-location",
G_CALLBACK(on_open_location), NULL);
- g.plus = gtk_button_new_from_icon_name("zoom-in-symbolic",
- GTK_ICON_SIZE_BUTTON);
- gtk_widget_set_tooltip_text(g.plus, "Larger thumbnails");
- g_signal_connect(g.plus, "clicked",
- G_CALLBACK(on_toolbar_zoom), (gpointer) +1);
-
- g.minus = gtk_button_new_from_icon_name("zoom-out-symbolic",
- GTK_ICON_SIZE_BUTTON);
- gtk_widget_set_tooltip_text(g.minus, "Smaller thumbnails");
- g_signal_connect(g.minus, "clicked",
- G_CALLBACK(on_toolbar_zoom), (gpointer) -1);
+ g_signal_connect(sidebar, "notify::visible",
+ G_CALLBACK(on_notify_sidebar_visible), NULL);
- GtkWidget *zoom_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_style_context_add_class(
- gtk_widget_get_style_context(zoom_group), GTK_STYLE_CLASS_LINKED);
- gtk_box_pack_start(GTK_BOX(zoom_group), g.plus, FALSE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(zoom_group), g.minus, FALSE, FALSE, 0);
-
- g.funnel = gtk_toggle_button_new();
- gtk_container_add(GTK_CONTAINER(g.funnel),
- gtk_image_new_from_icon_name("funnel-symbolic", GTK_ICON_SIZE_BUTTON));
- gtk_widget_set_tooltip_text(g.funnel, "Hide unsupported files");
- g_signal_connect(g.funnel, "toggled",
- G_CALLBACK(on_filtering_toggled), NULL);
-
- GtkWidget *menu = gtk_menu_new();
- g.sort_field[0] = gtk_radio_menu_item_new_with_mnemonic(NULL, "By _Name");
- g.sort_field[1] = gtk_radio_menu_item_new_with_mnemonic(
- gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(g.sort_field[0])),
- "By _Modification Time");
- for (int i = FIV_IO_MODEL_SORT_MIN; i <= FIV_IO_MODEL_SORT_MAX; i++) {
- g_signal_connect(g.sort_field[i], "activate",
- G_CALLBACK(on_sort_field), (void *) (intptr_t) i);
- gtk_menu_shell_append(GTK_MENU_SHELL(menu), g.sort_field[i]);
- }
-
- g.sort_direction[0] =
- gtk_radio_menu_item_new_with_mnemonic(NULL, "_Ascending");
- g.sort_direction[1] = gtk_radio_menu_item_new_with_mnemonic(
- gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(g.sort_direction[0])),
- "_Descending");
- g_signal_connect(g.sort_direction[0], "activate",
- G_CALLBACK(on_sort_direction), (void *) 0);
- g_signal_connect(g.sort_direction[1], "activate",
- G_CALLBACK(on_sort_direction), (void *) 1);
-
- gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
- gtk_menu_shell_append(GTK_MENU_SHELL(menu), g.sort_direction[0]);
- gtk_menu_shell_append(GTK_MENU_SHELL(menu), g.sort_direction[1]);
- gtk_widget_show_all(menu);
-
- GtkWidget *sort = gtk_menu_button_new();
- gtk_widget_set_tooltip_text(sort, "Sort order");
- gtk_button_set_image(GTK_BUTTON(sort),
- gtk_image_new_from_icon_name(
- "view-sort-ascending-symbolic", GTK_ICON_SIZE_BUTTON));
- gtk_menu_button_set_popup(GTK_MENU_BUTTON(sort), menu);
-
- GtkWidget *model_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_style_context_add_class(
- gtk_widget_get_style_context(model_group), GTK_STYLE_CLASS_LINKED);
- gtk_box_pack_start(GTK_BOX(model_group), g.funnel, FALSE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(model_group), sort, FALSE, FALSE, 0);
-
- GtkBox *toolbar = fiv_sidebar_get_toolbar(FIV_SIDEBAR(sidebar));
- gtk_box_pack_start(toolbar, zoom_group, FALSE, FALSE, 0);
- gtk_box_pack_start(toolbar, model_group, FALSE, FALSE, 0);
- gtk_widget_set_halign(GTK_WIDGET(toolbar), GTK_ALIGN_CENTER);
+ g_object_notify(G_OBJECT(sidebar), "visible");
g_signal_connect(g.browser, "notify::thumbnail-size",
G_CALLBACK(on_notify_thumbnail_size), NULL);
+ g_signal_connect(g.browser, "notify::show-labels",
+ G_CALLBACK(on_notify_show_labels), NULL);
g_signal_connect(model, "notify::filtering",
G_CALLBACK(on_notify_filtering), NULL);
g_signal_connect(model, "notify::sort-field",
G_CALLBACK(on_notify_sort_field), NULL);
g_signal_connect(model, "notify::sort-descending",
G_CALLBACK(on_notify_sort_descending), NULL);
+
on_toolbar_zoom(NULL, (gpointer) 0);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g.funnel), TRUE);
- // TODO(p): Invoke sort configuration notifications explicitly.
+
+ g_object_notify(G_OBJECT(g.model), "filtering");
+ g_object_notify(G_OBJECT(g.model), "sort-field");
+ g_object_notify(G_OBJECT(g.model), "sort-descending");
return sidebar;
}
+// --- Actions -----------------------------------------------------------------
+
+#define ACTION(name) static void on_action_ ## name(void)
+
+ACTION(new_window) {
+ if (gtk_stack_get_visible_child(GTK_STACK(g.stack)) == g.view_box)
+ spawn_uri(g.uri);
+ else
+ spawn_uri(g.directory);
+}
+
+ACTION(quit) {
+ gtk_widget_destroy(g.window);
+}
+
+ACTION(location) {
+ fiv_sidebar_show_enter_location(FIV_SIDEBAR(g.browser_sidebar));
+}
+
+ACTION(preferences) {
+ show_preferences(g.window);
+}
+
+ACTION(about) {
+ show_about_dialog(g.window);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+typedef struct {
+ const char *name; ///< Unprefixed action name
+ GCallback callback; ///< Simple callback
+ const char **accels; ///< NULL-terminated accelerator list
+} ActionEntry;
+
+static ActionEntry g_actions[] = {
+ {"preferences", on_action_preferences,
+ (const char *[]) {"<Primary>comma", NULL}},
+ {"new-window", on_action_new_window,
+ (const char *[]) {"<Primary>n", NULL}},
+ {"open", on_open,
+ (const char *[]) {"<Primary>o", "o", NULL}},
+ {"quit", on_action_quit,
+ (const char *[]) {"<Primary>q", "<Primary>w", "q", NULL}},
+ {"toggle-fullscreen", toggle_fullscreen,
+ (const char *[]) {"F11", "f", NULL}},
+ {"toggle-sunlight", toggle_sunlight,
+ (const char *[]) {"<Alt><Shift>d", NULL}},
+ {"go-back", go_back,
+ (const char *[]) {"<Alt>Left", "BackSpace", NULL}},
+ {"go-forward", go_forward,
+ (const char *[]) {"<Alt>Right", NULL}},
+ {"go-location", on_action_location,
+ (const char *[]) {"<Primary>l", NULL}},
+ {"help", show_help_contents,
+ (const char *[]) {"F1", NULL}},
+ {"shortcuts", show_help_shortcuts,
+ // Similar to win.show-help-overlay in gtkapplication.c.
+ (const char *[]) {"<Primary>question", "<Primary>F1", NULL}},
+ {"about", on_action_about,
+ (const char *[]) {"<Shift>F1", NULL}},
+ {}
+};
+
+static void
+dispatch_action(G_GNUC_UNUSED GSimpleAction *action,
+ G_GNUC_UNUSED GVariant *parameter, gpointer user_data)
+{
+ GCallback callback = user_data;
+ callback();
+}
+
+static void
+set_up_action(GtkApplication *app, const ActionEntry *a)
+{
+ GSimpleAction *action = g_simple_action_new(a->name, NULL);
+ g_signal_connect(action, "activate",
+ G_CALLBACK(dispatch_action), a->callback);
+ g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(action));
+ g_object_unref(action);
+
+ gchar *full_name = g_strdup_printf("app.%s", a->name);
+ gtk_application_set_accels_for_action(app, full_name, a->accels);
+ g_free(full_name);
+}
+
+// --- Menu --------------------------------------------------------------------
+
+typedef struct {
+ const char *label; ///< Label, with a mnemonic
+ const char *action; ///< Prefixed action name
+ gboolean macos; ///< Show in the macOS global menu?
+} MenuItem;
+
+typedef struct {
+ const char *label; ///< Label, with a mnemonic
+ const MenuItem *items; ///< ""-sectioned menu items
+} MenuRoot;
+
+// We're single-instance, skip the "win" namespace for simplicity.
+static MenuRoot g_menu[] = {
+ {"_File", (MenuItem[]) {
+ {"_New Window", "app.new-window", TRUE},
+ {"_Open...", "app.open", TRUE},
+ {"", NULL, TRUE},
+ {"_Quit", "app.quit", FALSE},
+ {}
+ }},
+ {"_Go", (MenuItem[]) {
+ {"_Back", "app.go-back", TRUE},
+ {"_Forward", "app.go-forward", TRUE},
+ {"", NULL, TRUE},
+ {"_Location...", "app.go-location", TRUE},
+ {}
+ }},
+ {"_Help", (MenuItem[]) {
+ {"_Contents", "app.help", TRUE},
+ {"_Keyboard Shortcuts", "app.shortcuts", TRUE},
+ {"_About", "app.about", FALSE},
+ {}
+ }},
+ {}
+};
+
+static GMenuModel *
+make_submenu(const MenuItem *items)
+{
+ GMenu *menu = g_menu_new();
+ while (items->label) {
+ GMenu *section = g_menu_new();
+ for (; items->label; items++) {
+ // Empty strings are interpreted as separators.
+ if (!*items->label) {
+ items++;
+ break;
+ }
+
+ GMenuItem *subitem = g_menu_item_new(items->label, items->action);
+ if (!items->macos) {
+ g_menu_item_set_attribute(
+ subitem, "hidden-when", "s", "macos-menubar");
+ }
+
+ g_menu_append_item(section, subitem);
+ g_object_unref(subitem);
+ }
+ g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
+ g_object_unref(section);
+ }
+ return G_MENU_MODEL(menu);
+}
+
+static GMenuModel *
+make_menu_model(void)
+{
+ GMenu *menu = g_menu_new();
+ for (const MenuRoot *root = g_menu; root->label; root++) {
+ GMenuModel *submenu = make_submenu(root->items);
+ g_menu_append_submenu(menu, root->label, submenu);
+ g_object_unref(submenu);
+ }
+ return G_MENU_MODEL(menu);
+}
+
static GtkWidget *
-make_menu_bar(void)
-{
- g.menu = gtk_menu_bar_new();
-
- GtkWidget *item_quit = gtk_menu_item_new_with_mnemonic("_Quit");
- g_signal_connect_swapped(item_quit, "activate",
- G_CALLBACK(gtk_widget_destroy), g.window);
-
- GtkWidget *menu_file = gtk_menu_new();
- gtk_menu_shell_append(GTK_MENU_SHELL(menu_file), item_quit);
- GtkWidget *item_file = gtk_menu_item_new_with_mnemonic("_File");
- gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_file), menu_file);
- gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_file);
-
- GtkWidget *item_contents = gtk_menu_item_new_with_mnemonic("_Contents");
- g_signal_connect_swapped(item_contents, "activate",
- G_CALLBACK(show_help_contents), NULL);
- GtkWidget *item_shortcuts =
- gtk_menu_item_new_with_mnemonic("_Keyboard Shortcuts");
- g_signal_connect_swapped(item_shortcuts, "activate",
- G_CALLBACK(show_help_shortcuts), NULL);
- GtkWidget *item_about = gtk_menu_item_new_with_mnemonic("_About");
- g_signal_connect_swapped(item_about, "activate",
- G_CALLBACK(show_about_dialog), g.window);
-
- GtkWidget *menu_help = gtk_menu_new();
- gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_contents);
- gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_shortcuts);
- gtk_menu_shell_append(GTK_MENU_SHELL(menu_help), item_about);
- GtkWidget *item_help = gtk_menu_item_new_with_mnemonic("_Help");
- gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_help), menu_help);
- gtk_menu_shell_append(GTK_MENU_SHELL(g.menu), item_help);
+make_menu_bar(GMenuModel *model)
+{
+ g.menu = gtk_menu_bar_new_from_model(model);
// Don't let it take up space by default. Firefox sets a precedent here.
+ // (gtk_application_window_set_show_menubar() doesn't seem viable for use
+ // for this purpose.)
gtk_widget_show_all(g.menu);
gtk_widget_set_no_show_all(g.menu, TRUE);
gtk_widget_hide(g.menu);
@@ -1871,6 +2198,8 @@ make_menu_bar(void)
return g.menu;
}
+// --- Application -------------------------------------------------------------
+
// This is incredibly broken https://stackoverflow.com/a/51054396/76313
// thus resolving the problem using overlaps.
// We're trying to be universal for light and dark themes both. It's hard.
@@ -1878,12 +2207,14 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
@define-color fiv-semiselected \
mix(@theme_selected_bg_color, @content_view_bg, 0.5); \
fiv-view, fiv-browser { background: @content_view_bg; } \
- placessidebar.fiv .toolbar { padding: 2px 6px; } \
placessidebar.fiv box > separator { margin: 4px 0; } \
- #toolbar button { padding-left: 0; padding-right: 0; } \
- #toolbar > button:first-child { padding-left: 4px; } \
- #toolbar > button:last-child { padding-right: 4px; } \
- #toolbar separator { \
+ placessidebar.fiv row { min-height: 2em; } \
+ .fiv-toolbar button { padding-left: 0; padding-right: 0; } \
+ .fiv-toolbar button.text-button { \
+ padding-left: 4px; padding-right: 4px; } \
+ .fiv-toolbar > button:first-child { padding-left: 4px; } \
+ .fiv-toolbar > button:last-child { padding-right: 4px; } \
+ .fiv-toolbar separator { \
background: mix(@insensitive_fg_color, \
@insensitive_bg_color, 0.4); margin: 6px 8px; \
} \
@@ -1902,6 +2233,12 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
background-size: 40px 40px; \
background-position: 0 0, 0 20px, 20px -20px, -20px 0px; \
} \
+ fiv-browser.item.label, fiv-browser.item.symbolic.label { \
+ color: @theme_fg_color; \
+ } \
+ fiv-browser.item.label:backdrop:not(:selected) { \
+ color: @theme_unfocused_fg_color; \
+ } \
fiv-browser.item:selected { \
color: @theme_selected_bg_color; \
border-color: @theme_selected_bg_color; \
@@ -1935,113 +2272,19 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
.fiv-information label { padding: 0 4px; }";
static void
-output_thumbnail(gchar **uris, gboolean extract, const char *size_arg)
-{
- if (!uris)
- exit_fatal("No path given");
- if (uris[1])
- exit_fatal("Only one thumbnail at a time may be produced");
-
- FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT;
- if (size_arg) {
- for (size = 0; size < FIV_THUMBNAIL_SIZE_COUNT; size++) {
- if (!strcmp(
- fiv_thumbnail_sizes[size].thumbnail_spec_name, size_arg))
- break;
- }
- if (size >= FIV_THUMBNAIL_SIZE_COUNT)
- exit_fatal("unknown thumbnail size: %s", size_arg);
- }
-
-#ifdef G_OS_WIN32
- _setmode(fileno(stdout), _O_BINARY);
-#endif
-
- GError *error = NULL;
- GFile *file = g_file_new_for_uri(uris[0]);
- cairo_surface_t *surface = NULL;
- if (extract && (surface = fiv_thumbnail_extract(file, size, &error)))
- fiv_io_serialize_to_stdout(surface, FIV_IO_SERIALIZE_LOW_QUALITY);
- else if (size_arg &&
- (g_clear_error(&error),
- (surface = fiv_thumbnail_produce(file, size, &error))))
- fiv_io_serialize_to_stdout(surface, 0);
- else
- g_assert(error != NULL);
-
- g_object_unref(file);
- if (error)
- exit_fatal("%s", error->message);
-
- cairo_surface_destroy(surface);
-}
-
-int
-main(int argc, char *argv[])
+on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
{
- gboolean show_version = FALSE, show_supported_media_types = FALSE,
- invalidate_cache = FALSE, browse = FALSE, extract_thumbnail = FALSE;
- gchar **args = NULL, *thumbnail_size = NULL;
- const GOptionEntry options[] = {
- {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args,
- NULL, "[PATH | URI]..."},
- {"list-supported-media-types", 0, G_OPTION_FLAG_IN_MAIN,
- G_OPTION_ARG_NONE, &show_supported_media_types,
- "Output supported media types and exit", NULL},
- {"browse", 0, G_OPTION_FLAG_IN_MAIN,
- G_OPTION_ARG_NONE, &browse,
- "Start in filesystem browsing mode", NULL},
- {"extract-thumbnail", 0, G_OPTION_FLAG_IN_MAIN,
- G_OPTION_ARG_NONE, &extract_thumbnail,
- "Output any embedded thumbnail (superseding --thumbnail)", NULL},
- {"thumbnail", 0, G_OPTION_FLAG_IN_MAIN,
- G_OPTION_ARG_STRING, &thumbnail_size,
- "Generate thumbnails, up to SIZE, and output that size", "SIZE"},
- {"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
- G_OPTION_ARG_NONE, &invalidate_cache,
- "Invalidate the wide thumbnail cache", NULL},
- {"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
- &show_version, "Output version information and exit", NULL},
- {},
- };
-
- GError *error = NULL;
- gboolean initialized = gtk_init_with_args(
- &argc, &argv, " - Image browser and viewer", options, NULL, &error);
- if (show_version) {
- const char *version = PROJECT_VERSION;
- printf("%s %s\n", PROJECT_NAME, &version[*version == 'v']);
- return 0;
- }
- if (show_supported_media_types) {
- for (char **types = fiv_io_all_supported_media_types(); *types; )
- g_print("%s\n", *types++);
- return 0;
- }
- if (invalidate_cache) {
- fiv_thumbnail_invalidate();
- return 0;
- }
- if (!initialized)
- exit_fatal("%s", error->message);
-
- // Normalize all arguments to URIs.
- for (gsize i = 0; args && args[i]; i++) {
- GFile *resolved = g_file_new_for_commandline_arg(args[i]);
- g_free(args[i]);
- args[i] = g_file_get_uri(resolved);
- g_object_unref(resolved);
- }
- if (extract_thumbnail || thumbnail_size) {
- output_thumbnail(args, extract_thumbnail, thumbnail_size);
- return 0;
- }
+ // We can't prevent GApplication from adding --gapplication-service.
+ if (g_application_get_flags(app) & G_APPLICATION_IS_SERVICE)
+ exit(EXIT_FAILURE);
// It doesn't make much sense to have command line arguments able to
// resolve to the VFS they may end up being contained within.
fiv_collection_register();
g.model = g_object_new(FIV_TYPE_IO_MODEL, NULL);
+ g_signal_connect(g.model, "reloaded",
+ G_CALLBACK(on_model_reloaded), NULL);
g_signal_connect(g.model, "files-changed",
G_CALLBACK(on_model_files_changed), NULL);
@@ -2068,7 +2311,7 @@ main(int argc, char *argv[])
G_CALLBACK(on_view_drag_data_received), NULL);
gtk_container_add(GTK_CONTAINER(view_scroller), g.view);
- // We need to hide it together with the separator.
+ // We need to hide it together with its separator.
g.view_toolbar = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_box_pack_start(GTK_BOX(g.view_toolbar),
make_view_toolbar(), FALSE, FALSE, 0);
@@ -2108,10 +2351,23 @@ main(int argc, char *argv[])
G_CALLBACK(on_item_activated), NULL);
gtk_container_add(GTK_CONTAINER(g.browser_scroller), g.browser);
+ // We need to hide it together with its separator.
+ g.browser_toolbar = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_box_pack_start(GTK_BOX(g.browser_toolbar),
+ make_browser_toolbar(), FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(g.browser_toolbar),
+ gtk_separator_new(GTK_ORIENTATION_VERTICAL), FALSE, FALSE, 0);
+
+ GtkWidget *browser_right = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_box_pack_start(GTK_BOX(browser_right),
+ g.browser_toolbar, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(browser_right),
+ g.browser_scroller, TRUE, TRUE, 0);
+
g.browser_sidebar = make_browser_sidebar(g.model);
g.browser_paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
gtk_paned_add1(GTK_PANED(g.browser_paned), g.browser_sidebar);
- gtk_paned_add2(GTK_PANED(g.browser_paned), g.browser_scroller);
+ gtk_paned_add2(GTK_PANED(g.browser_paned), browser_right);
g_signal_connect(g.browser_paned, "key-press-event",
G_CALLBACK(on_key_press_browser_paned), NULL);
g_signal_connect(g.browser_paned, "button-press-event",
@@ -2123,18 +2379,35 @@ main(int argc, char *argv[])
gtk_container_add(GTK_CONTAINER(g.stack), g.view_box);
gtk_container_add(GTK_CONTAINER(g.stack), g.browser_paned);
- g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
- g_signal_connect(g.window, "destroy",
- G_CALLBACK(gtk_main_quit), NULL);
+ g.window = gtk_application_window_new(GTK_APPLICATION(app));
+ g_signal_connect_swapped(g.window, "destroy",
+ G_CALLBACK(g_application_quit), app);
g_signal_connect(g.window, "key-press-event",
G_CALLBACK(on_key_press), NULL);
g_signal_connect(g.window, "window-state-event",
G_CALLBACK(on_window_state_event), NULL);
+ for (const ActionEntry *a = g_actions; a->name; a++)
+ set_up_action(GTK_APPLICATION(app), a);
+
+ // GtkApplicationWindow overrides GtkContainer/GtkWidget virtual methods
+ // so that it has the menu bar as an extra child (if it so decides).
+ // However, we currently want this menu bar to only show on a key press,
+ // and to hide as soon as it's no longer being used.
+ // Messing with the window's internal state seems at best quirky,
+ // so we'll manage the menu entirely by ourselves.
+ gtk_application_window_set_show_menubar(
+ GTK_APPLICATION_WINDOW(g.window), FALSE);
+
+ GMenuModel *menu = make_menu_model();
+ gtk_application_set_menubar(GTK_APPLICATION(app), menu);
+ // The default "app menu" is good, in particular for macOS, so keep it.
+
GtkWidget *menu_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar());
+ gtk_container_add(GTK_CONTAINER(menu_box), make_menu_bar(menu));
gtk_container_add(GTK_CONTAINER(menu_box), g.stack);
gtk_container_add(GTK_CONTAINER(g.window), menu_box);
+ g_object_unref(menu);
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
if (g_settings_get_boolean(settings, "dark-theme"))
@@ -2145,6 +2418,8 @@ main(int argc, char *argv[])
gtk_widget_show_all(menu_box);
gtk_widget_set_visible(g.browser_sidebar,
g_settings_get_boolean(settings, "show-browser-sidebar"));
+ gtk_widget_set_visible(g.browser_toolbar,
+ g_settings_get_boolean(settings, "show-browser-toolbar"));
gtk_widget_set_visible(g.view_toolbar,
g_settings_get_boolean(settings, "show-view-toolbar"));
@@ -2174,24 +2449,34 @@ main(int argc, char *argv[])
// XXX: The widget wants to read the display's profile. The realize is ugly.
gtk_widget_realize(g.view);
+}
+
+static struct {
+ gboolean browse, collection, extract_thumbnail;
+ gchar **args, *thumbnail_size, *thumbnail_size_search;
+} o;
+static void
+on_app_activate(
+ G_GNUC_UNUSED GApplication *app, G_GNUC_UNUSED gpointer user_data)
+{
// XXX: We follow the behaviour of Firefox and Eye of GNOME, which both
// interpret multiple command line arguments differently, as a collection.
- // However, single-element collections are unrepresentable this way.
- // Should we allow multiple targets only in a special new mode?
- g.files = g_ptr_array_new_full(0, g_free);
- if (args) {
- const gchar *target = *args;
- if (args[1]) {
- fiv_collection_reload(args);
+ // However, single-element collections are unrepresentable this way,
+ // so we have a switch to enforce it.
+ g.files_index = -1;
+ if (o.args) {
+ const gchar *target = *o.args;
+ if (o.args[1] || o.collection) {
+ fiv_collection_reload(o.args);
target = FIV_COLLECTION_SCHEME ":/";
}
GFile *file = g_file_new_for_uri(target);
- open_any_file(file, browse);
+ open_any_file(file, o.browse);
g_object_unref(file);
- g_strfreev(args);
}
+
if (!g.directory) {
GFile *file = g_file_new_for_path(".");
open_any_file(file, FALSE);
@@ -2199,6 +2484,182 @@ main(int argc, char *argv[])
}
gtk_widget_show(g.window);
- gtk_main();
- return 0;
+}
+
+// --- Plumbing ----------------------------------------------------------------
+
+static FivThumbnailSize
+output_thumbnail_prologue(gchar **uris, const char *size_arg)
+{
+ if (!uris)
+ exit_fatal("No path given");
+ if (uris[1])
+ exit_fatal("Only one thumbnail at a time may be produced");
+
+ FivThumbnailSize size = FIV_THUMBNAIL_SIZE_COUNT;
+ if (size_arg) {
+ for (size = 0; size < FIV_THUMBNAIL_SIZE_COUNT; size++) {
+ if (!strcmp(
+ fiv_thumbnail_sizes[size].thumbnail_spec_name, size_arg))
+ break;
+ }
+ if (size >= FIV_THUMBNAIL_SIZE_COUNT)
+ exit_fatal("unknown thumbnail size: %s", size_arg);
+ }
+
+#ifdef G_OS_WIN32
+ _setmode(fileno(stdout), _O_BINARY);
+#endif
+ return size;
+}
+
+static void
+output_thumbnail_for_search(gchar **uris, const char *size_arg)
+{
+ FivThumbnailSize size = output_thumbnail_prologue(uris, size_arg);
+
+ GError *error = NULL;
+ GFile *file = g_file_new_for_uri(uris[0]);
+ cairo_surface_t *surface = NULL;
+ GBytes *bytes = NULL;
+ if ((surface = fiv_thumbnail_produce(file, size, &error)) &&
+ (bytes = fiv_io_serialize_for_search(surface, &error))) {
+ fwrite(
+ g_bytes_get_data(bytes, NULL), 1, g_bytes_get_size(bytes), stdout);
+ g_bytes_unref(bytes);
+ } else {
+ g_assert(error != NULL);
+ }
+
+ g_object_unref(file);
+ if (error)
+ exit_fatal("%s", error->message);
+
+ cairo_surface_destroy(surface);
+}
+
+static void
+output_thumbnail(gchar **uris, gboolean extract, const char *size_arg)
+{
+ FivThumbnailSize size = output_thumbnail_prologue(uris, size_arg);
+
+ GError *error = NULL;
+ GFile *file = g_file_new_for_uri(uris[0]);
+ cairo_surface_t *surface = NULL;
+ if (extract && (surface = fiv_thumbnail_extract(file, size, &error)))
+ fiv_io_serialize_to_stdout(surface, FIV_IO_SERIALIZE_LOW_QUALITY);
+ else if (size_arg &&
+ (g_clear_error(&error),
+ (surface = fiv_thumbnail_produce(file, size, &error))))
+ fiv_io_serialize_to_stdout(surface, 0);
+ else
+ g_assert(error != NULL);
+
+ g_object_unref(file);
+ if (error)
+ exit_fatal("%s", error->message);
+
+ cairo_surface_destroy(surface);
+}
+
+static gint
+on_app_handle_local_options(G_GNUC_UNUSED GApplication *app,
+ GVariantDict *options, G_GNUC_UNUSED gpointer user_data)
+{
+ if (g_variant_dict_contains(options, "version")) {
+ const char *version = PROJECT_VERSION;
+ printf("%s %s\n", PROJECT_NAME, &version[*version == 'v']);
+ return 0;
+ }
+ if (g_variant_dict_contains(options, "list-supported-media-types")) {
+ char **types = fiv_io_all_supported_media_types();
+ for (char **p = types; *p; p++)
+ g_print("%s\n", *p);
+ g_strfreev(types);
+ return 0;
+ }
+ if (g_variant_dict_contains(options, "invalidate-cache")) {
+ fiv_thumbnail_invalidate();
+ return 0;
+ }
+
+ // Normalize all arguments to URIs, and run thumbnailing modes first.
+ for (gsize i = 0; o.args && o.args[i]; i++) {
+ GFile *resolved = g_file_new_for_commandline_arg(o.args[i]);
+ g_free(o.args[i]);
+ o.args[i] = g_file_get_uri(resolved);
+ g_object_unref(resolved);
+ }
+
+ // These come from an option group that doesn't get copied to "options".
+ if (o.thumbnail_size_search) {
+ output_thumbnail_for_search(o.args, o.thumbnail_size_search);
+ return 0;
+ }
+ if (o.extract_thumbnail || o.thumbnail_size) {
+ output_thumbnail(o.args, o.extract_thumbnail, o.thumbnail_size);
+ return 0;
+ }
+ return -1;
+}
+
+int
+main(int argc, char *argv[])
+{
+ const GOptionEntry options[] = {
+ {G_OPTION_REMAINING, 0, 0,
+ G_OPTION_ARG_FILENAME_ARRAY, &o.args,
+ NULL, "[PATH | URI]..."},
+ {"browse", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, &o.browse,
+ "Start in filesystem browsing mode", NULL},
+ {"collection", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, &o.collection,
+ "Always put arguments in a collection (implies --browse)", NULL},
+ {"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, NULL,
+ "Invalidate the wide thumbnail cache", NULL},
+ {"list-supported-media-types", 0, G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, NULL,
+ "Output supported media types and exit", NULL},
+ {"version", 'V', G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE, NULL,
+ "Output version information and exit", NULL},
+ {},
+ };
+ const GOptionEntry options_internal[] = {
+ {"extract-thumbnail", 0, 0,
+ G_OPTION_ARG_NONE, &o.extract_thumbnail,
+ "Output any embedded thumbnail (superseding --thumbnail)", NULL},
+ {"thumbnail", 0, 0,
+ G_OPTION_ARG_STRING, &o.thumbnail_size,
+ "Generate thumbnails, up to SIZE, and output that size", "SIZE"},
+ {"thumbnail-for-search", 0, 0,
+ G_OPTION_ARG_STRING, &o.thumbnail_size_search,
+ "Output an image file suitable for searching by content", "SIZE"},
+ {},
+ };
+
+ // We never get the ::open signal, thanks to G_OPTION_ARG_FILENAME_ARRAY.
+ GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
+ g_application_set_option_context_parameter_string(
+ G_APPLICATION(app), " - Image browser and viewer");
+ g_application_add_main_option_entries(G_APPLICATION(app), options);
+
+ GOptionGroup *internals = g_option_group_new(
+ "internal", "Internal Options:", "Show internal options", NULL, NULL);
+ g_option_group_add_entries(internals, options_internal);
+ g_application_add_option_group(G_APPLICATION(app), internals);
+
+ g_signal_connect(app, "handle-local-options",
+ G_CALLBACK(on_app_handle_local_options), NULL);
+ g_signal_connect(app, "startup",
+ G_CALLBACK(on_app_startup), NULL);
+ g_signal_connect(app, "activate",
+ G_CALLBACK(on_app_activate), NULL);
+
+ int status = g_application_run(G_APPLICATION(app), argc, argv);
+ g_object_unref(app);
+ g_strfreev(o.args);
+ return status;
}