Compare commits
5 Commits
e6ad6c6302
...
c597e7bc2c
Author | SHA1 | Date | |
---|---|---|---|
c597e7bc2c | |||
1c40fa8adb | |||
fee901a590 | |||
7ab1a6d246 | |||
1e6689aed4 |
@ -44,6 +44,8 @@ _~/.config/gtk-3.0/gtk.css_:
|
|||||||
|
|
||||||
fastiv-browser { -FastivBrowser-spacing: 0; padding: 0; border: 0; margin: 0; }
|
fastiv-browser { -FastivBrowser-spacing: 0; padding: 0; border: 0; margin: 0; }
|
||||||
|
|
||||||
|
The GTK+ inspector will be very helpful, should you want to experiment.
|
||||||
|
|
||||||
Contributing and Support
|
Contributing and Support
|
||||||
------------------------
|
------------------------
|
||||||
Use https://git.janouch.name/p/fastiv to report any bugs, request features,
|
Use https://git.janouch.name/p/fastiv to report any bugs, request features,
|
||||||
|
199
fastiv-browser.c
199
fastiv-browser.c
@ -73,8 +73,7 @@ static void
|
|||||||
entry_free(Entry *self)
|
entry_free(Entry *self)
|
||||||
{
|
{
|
||||||
g_free(self->filename);
|
g_free(self->filename);
|
||||||
if (self->thumbnail)
|
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
|
||||||
cairo_surface_destroy(self->thumbnail);
|
|
||||||
g_clear_object(&self->icon);
|
g_clear_object(&self->icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,8 +348,7 @@ entry_add_thumbnail(gpointer data, gpointer user_data)
|
|||||||
{
|
{
|
||||||
Entry *self = data;
|
Entry *self = data;
|
||||||
g_clear_object(&self->icon);
|
g_clear_object(&self->icon);
|
||||||
if (self->thumbnail)
|
g_clear_pointer(&self->thumbnail, cairo_surface_destroy);
|
||||||
cairo_surface_destroy(self->thumbnail);
|
|
||||||
|
|
||||||
FastivBrowser *browser = FASTIV_BROWSER(user_data);
|
FastivBrowser *browser = FASTIV_BROWSER(user_data);
|
||||||
self->thumbnail = rescale_thumbnail(
|
self->thumbnail = rescale_thumbnail(
|
||||||
@ -434,6 +432,167 @@ reload_thumbnails(FastivBrowser *self)
|
|||||||
gtk_widget_queue_resize(GTK_WIDGET(self));
|
gtk_widget_queue_resize(GTK_WIDGET(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Context menu-------------------------------------------------------------
|
||||||
|
|
||||||
|
typedef struct _OpenContext {
|
||||||
|
GWeakRef widget;
|
||||||
|
GFile *file;
|
||||||
|
char *content_type;
|
||||||
|
GAppInfo *app_info;
|
||||||
|
} OpenContext;
|
||||||
|
|
||||||
|
static void
|
||||||
|
open_context_notify(gpointer data, G_GNUC_UNUSED GClosure *closure)
|
||||||
|
{
|
||||||
|
OpenContext *self = data;
|
||||||
|
g_weak_ref_clear(&self->widget);
|
||||||
|
g_clear_object(&self->app_info);
|
||||||
|
g_clear_object(&self->file);
|
||||||
|
g_free(self->content_type);
|
||||||
|
g_free(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
open_context_launch(GtkWidget *widget, OpenContext *self)
|
||||||
|
{
|
||||||
|
GdkAppLaunchContext *context =
|
||||||
|
gdk_display_get_app_launch_context(gtk_widget_get_display(widget));
|
||||||
|
gdk_app_launch_context_set_screen(context, gtk_widget_get_screen(widget));
|
||||||
|
gdk_app_launch_context_set_timestamp(context, gtk_get_current_event_time());
|
||||||
|
|
||||||
|
// TODO(p): Display errors.
|
||||||
|
GList *files = g_list_append(NULL, self->file);
|
||||||
|
if (g_app_info_launch(
|
||||||
|
self->app_info, files, G_APP_LAUNCH_CONTEXT(context), NULL)) {
|
||||||
|
g_app_info_set_as_last_used_for_type(
|
||||||
|
self->app_info, self->content_type, NULL);
|
||||||
|
}
|
||||||
|
g_list_free(files);
|
||||||
|
g_object_unref(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template)
|
||||||
|
{
|
||||||
|
OpenContext *ctx = g_malloc0(sizeof *ctx);
|
||||||
|
g_weak_ref_init(&ctx->widget, NULL);
|
||||||
|
ctx->file = g_object_ref(template->file);
|
||||||
|
ctx->content_type = g_strdup(template->content_type);
|
||||||
|
ctx->app_info = opener;
|
||||||
|
|
||||||
|
// It's documented that we can touch the child, if we want formatting:
|
||||||
|
// https://docs.gtk.org/gtk3/class.MenuItem.html
|
||||||
|
// XXX: Would g_app_info_get_display_name() be any better?
|
||||||
|
gchar *name = g_strdup_printf("Open With %s", g_app_info_get_name(opener));
|
||||||
|
GtkWidget *item = gtk_menu_item_new_with_label(name);
|
||||||
|
g_free(name);
|
||||||
|
g_signal_connect_data(item, "activate", G_CALLBACK(open_context_launch),
|
||||||
|
ctx, open_context_notify, 0);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
on_chooser_activate(GtkMenuItem *item, gpointer user_data)
|
||||||
|
{
|
||||||
|
OpenContext *ctx = user_data;
|
||||||
|
GtkWindow *window = NULL;
|
||||||
|
GtkWidget *widget = g_weak_ref_get(&ctx->widget);
|
||||||
|
if (widget) {
|
||||||
|
if (GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget))))
|
||||||
|
window = GTK_WINDOW(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window,
|
||||||
|
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type);
|
||||||
|
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
|
||||||
|
ctx->app_info = gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(dialog));
|
||||||
|
open_context_launch(GTK_WIDGET(item), ctx);
|
||||||
|
}
|
||||||
|
gtk_widget_destroy(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
destroy_widget_idle_source_func(GtkWidget *widget)
|
||||||
|
{
|
||||||
|
// The whole menu is deactivated /before/ any item is activated,
|
||||||
|
// and a destroyed child item will not activate.
|
||||||
|
gtk_widget_destroy(widget);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
show_context_menu(GtkWidget *widget, const char *filename)
|
||||||
|
{
|
||||||
|
GFile *file = g_file_new_for_path(filename);
|
||||||
|
GFileInfo *info = g_file_query_info(file,
|
||||||
|
G_FILE_ATTRIBUTE_STANDARD_NAME
|
||||||
|
"," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
|
||||||
|
G_FILE_QUERY_INFO_NONE, NULL, NULL);
|
||||||
|
if (!info) {
|
||||||
|
g_object_unref(file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will have no application pre-assigned, for use with GTK+'s dialog.
|
||||||
|
OpenContext *ctx = g_malloc0(sizeof *ctx);
|
||||||
|
g_weak_ref_init(&ctx->widget, widget);
|
||||||
|
ctx->file = file;
|
||||||
|
ctx->content_type = g_strdup(g_file_info_get_content_type(info));
|
||||||
|
g_object_unref(info);
|
||||||
|
|
||||||
|
GAppInfo *default_ =
|
||||||
|
g_app_info_get_default_for_type(ctx->content_type, FALSE);
|
||||||
|
GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type);
|
||||||
|
GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type);
|
||||||
|
|
||||||
|
GtkWidget *menu = gtk_menu_new();
|
||||||
|
if (default_) {
|
||||||
|
append_opener(menu, default_, ctx);
|
||||||
|
gtk_menu_shell_append(
|
||||||
|
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (GList *iter = recommended; iter; iter = iter->next) {
|
||||||
|
if (g_app_info_should_show(iter->data) &&
|
||||||
|
(!default_ || !g_app_info_equal(iter->data, default_)))
|
||||||
|
append_opener(menu, iter->data, ctx);
|
||||||
|
else
|
||||||
|
g_object_unref(iter->data);
|
||||||
|
}
|
||||||
|
if (recommended) {
|
||||||
|
g_list_free(recommended);
|
||||||
|
gtk_menu_shell_append(
|
||||||
|
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (GList *iter = fallback; iter; iter = iter->next) {
|
||||||
|
if (g_app_info_should_show(iter->data) &&
|
||||||
|
(!default_ || !g_app_info_equal(iter->data, default_)))
|
||||||
|
append_opener(menu, iter->data, ctx);
|
||||||
|
else
|
||||||
|
g_object_unref(iter->data);
|
||||||
|
}
|
||||||
|
if (fallback) {
|
||||||
|
g_list_free(fallback);
|
||||||
|
gtk_menu_shell_append(
|
||||||
|
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkWidget *item = gtk_menu_item_new_with_label("Open With...");
|
||||||
|
g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate),
|
||||||
|
ctx, open_context_notify, 0);
|
||||||
|
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
|
||||||
|
|
||||||
|
// As per GTK+ 3 Common Questions, 1.5.
|
||||||
|
g_object_ref_sink(menu);
|
||||||
|
g_signal_connect_swapped(menu, "deactivate",
|
||||||
|
G_CALLBACK(g_idle_add), destroy_widget_idle_source_func);
|
||||||
|
g_signal_connect(menu, "destroy", G_CALLBACK(g_object_unref), NULL);
|
||||||
|
|
||||||
|
gtk_widget_show_all(menu);
|
||||||
|
gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Boilerplate -------------------------------------------------------------
|
// --- Boilerplate -------------------------------------------------------------
|
||||||
|
|
||||||
// TODO(p): For proper navigation, we need to implement GtkScrollable.
|
// TODO(p): For proper navigation, we need to implement GtkScrollable.
|
||||||
@ -616,6 +775,14 @@ fastiv_browser_draw(GtkWidget *widget, cairo_t *cr)
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
open_entry(GtkWidget *self, const Entry *entry, gboolean new_window)
|
||||||
|
{
|
||||||
|
g_signal_emit(self, browser_signals[ITEM_ACTIVATED], 0, entry->filename,
|
||||||
|
new_window ? GTK_PLACES_OPEN_NEW_WINDOW : GTK_PLACES_OPEN_NORMAL);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
fastiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
|
fastiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
|
||||||
{
|
{
|
||||||
@ -623,9 +790,11 @@ fastiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
|
|||||||
->button_press_event(widget, event);
|
->button_press_event(widget, event);
|
||||||
|
|
||||||
FastivBrowser *self = FASTIV_BROWSER(widget);
|
FastivBrowser *self = FASTIV_BROWSER(widget);
|
||||||
if (event->type != GDK_BUTTON_PRESS || event->state != 0)
|
if (event->type != GDK_BUTTON_PRESS)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
if (event->button == GDK_BUTTON_PRIMARY &&
|
|
||||||
|
guint state = event->state & gtk_accelerator_get_default_mod_mask();
|
||||||
|
if (event->button == GDK_BUTTON_PRIMARY && state == 0 &&
|
||||||
gtk_widget_get_focus_on_click(widget))
|
gtk_widget_get_focus_on_click(widget))
|
||||||
gtk_widget_grab_focus(widget);
|
gtk_widget_grab_focus(widget);
|
||||||
|
|
||||||
@ -635,12 +804,20 @@ fastiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
|
|||||||
|
|
||||||
switch (event->button) {
|
switch (event->button) {
|
||||||
case GDK_BUTTON_PRIMARY:
|
case GDK_BUTTON_PRIMARY:
|
||||||
g_signal_emit(widget, browser_signals[ITEM_ACTIVATED], 0,
|
if (state == 0)
|
||||||
entry->filename, GTK_PLACES_OPEN_NORMAL);
|
return open_entry(widget, entry, FALSE);
|
||||||
return TRUE;
|
if (state == GDK_CONTROL_MASK)
|
||||||
|
return open_entry(widget, entry, TRUE);
|
||||||
|
return FALSE;
|
||||||
case GDK_BUTTON_MIDDLE:
|
case GDK_BUTTON_MIDDLE:
|
||||||
g_signal_emit(widget, browser_signals[ITEM_ACTIVATED], 0,
|
if (state == 0)
|
||||||
entry->filename, GTK_PLACES_OPEN_NEW_WINDOW);
|
return open_entry(widget, entry, TRUE);
|
||||||
|
return FALSE;
|
||||||
|
case GDK_BUTTON_SECONDARY:
|
||||||
|
// On X11, after closing the menu, the pointer otherwise remains,
|
||||||
|
// no matter what its new location is.
|
||||||
|
gdk_window_set_cursor(gtk_widget_get_window(widget), NULL);
|
||||||
|
show_context_menu(widget, entry->filename);
|
||||||
return TRUE;
|
return TRUE;
|
||||||
default:
|
default:
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
@ -239,8 +239,12 @@ complete_path(GFile *location, GtkListStore *model)
|
|||||||
g_file_info_get_is_hidden(info))
|
g_file_info_get_is_hidden(info))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// TODO(p): Resolve ~ paths a bit better.
|
|
||||||
char *parse_name = g_file_get_parse_name(child);
|
char *parse_name = g_file_get_parse_name(child);
|
||||||
|
if (!g_str_has_suffix(parse_name, G_DIR_SEPARATOR_S)) {
|
||||||
|
char *save = parse_name;
|
||||||
|
parse_name = g_strdup_printf("%s%c", parse_name, G_DIR_SEPARATOR);
|
||||||
|
g_free(save);
|
||||||
|
}
|
||||||
gtk_list_store_insert_with_values(model, NULL, -1, 0, parse_name, -1);
|
gtk_list_store_insert_with_values(model, NULL, -1, 0, parse_name, -1);
|
||||||
g_free(parse_name);
|
g_free(parse_name);
|
||||||
}
|
}
|
||||||
@ -310,6 +314,7 @@ on_show_enter_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar,
|
|||||||
GtkEntryCompletion *completion = gtk_entry_completion_new();
|
GtkEntryCompletion *completion = gtk_entry_completion_new();
|
||||||
gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model));
|
gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model));
|
||||||
gtk_entry_completion_set_text_column(completion, 0);
|
gtk_entry_completion_set_text_column(completion, 0);
|
||||||
|
// TODO(p): Complete ~ paths so that they start with ~, then we can filter.
|
||||||
gtk_entry_completion_set_match_func(
|
gtk_entry_completion_set_match_func(
|
||||||
completion, (GtkEntryCompletionMatchFunc) gtk_true, NULL, NULL);
|
completion, (GtkEntryCompletionMatchFunc) gtk_true, NULL, NULL);
|
||||||
g_object_unref(model);
|
g_object_unref(model);
|
||||||
|
9
fastiv.c
9
fastiv.c
@ -425,7 +425,7 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
|||||||
switch (event->keyval) {
|
switch (event->keyval) {
|
||||||
case GDK_KEY_Escape:
|
case GDK_KEY_Escape:
|
||||||
case GDK_KEY_q:
|
case GDK_KEY_q:
|
||||||
gtk_main_quit();
|
gtk_widget_destroy(g.window);
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
|
||||||
case GDK_KEY_o:
|
case GDK_KEY_o:
|
||||||
@ -438,10 +438,8 @@ on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
|
|
||||||
case GDK_KEY_F9:
|
case GDK_KEY_F9:
|
||||||
if (gtk_widget_is_visible(g.browser_sidebar))
|
gtk_widget_set_visible(g.browser_sidebar,
|
||||||
gtk_widget_hide(g.browser_sidebar);
|
!gtk_widget_is_visible(g.browser_sidebar));
|
||||||
else
|
|
||||||
gtk_widget_show(g.browser_sidebar);
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
|
||||||
case GDK_KEY_F11:
|
case GDK_KEY_F11:
|
||||||
@ -598,6 +596,7 @@ main(int argc, char *argv[])
|
|||||||
gtk_css_provider_load_from_data(provider, style, strlen(style), NULL);
|
gtk_css_provider_load_from_data(provider, style, strlen(style), NULL);
|
||||||
gtk_style_context_add_provider_for_screen(gdk_screen_get_default(),
|
gtk_style_context_add_provider_for_screen(gdk_screen_get_default(),
|
||||||
GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||||
|
g_object_unref(provider);
|
||||||
|
|
||||||
g.view_scroller = gtk_scrolled_window_new(NULL, NULL);
|
g.view_scroller = gtk_scrolled_window_new(NULL, NULL);
|
||||||
g.view = g_object_new(FASTIV_TYPE_VIEW, NULL);
|
g.view = g_object_new(FASTIV_TYPE_VIEW, NULL);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user