//
// fiv-sidebar.c: molesting GtkPlacesSidebar
//
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch 
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include 
#include "fiv-collection.h"
#include "fiv-context-menu.h"
#include "fiv-io.h"
#include "fiv-sidebar.h"
struct _FivSidebar {
	GtkScrolledWindow parent_instance;
	GtkPlacesSidebar *places;
	GtkWidget *listbox;
	FivIoModel *model;
};
G_DEFINE_TYPE(FivSidebar, fiv_sidebar, GTK_TYPE_SCROLLED_WINDOW)
G_DEFINE_QUARK(fiv-sidebar-drag-gesture-quark, fiv_sidebar_drag_gesture)
G_DEFINE_QUARK(fiv-sidebar-location-quark, fiv_sidebar_location)
G_DEFINE_QUARK(fiv-sidebar-self-quark, fiv_sidebar_self)
enum {
	OPEN_LOCATION,
	LAST_SIGNAL,
};
// Globals are, sadly, the canonical way of storing signal numbers.
static guint sidebar_signals[LAST_SIGNAL];
static void
fiv_sidebar_dispose(GObject *gobject)
{
	FivSidebar *self = FIV_SIDEBAR(gobject);
	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);
}
static void
fiv_sidebar_realize(GtkWidget *widget)
{
	GTK_WIDGET_CLASS(fiv_sidebar_parent_class)->realize(widget);
	// Fucking GTK+. With no bookmarks, the revealer takes up space anyway.
	FivSidebar *self = FIV_SIDEBAR(widget);
	gtk_places_sidebar_set_drop_targets_visible(self->places, TRUE, NULL);
	gtk_places_sidebar_set_drop_targets_visible(self->places, FALSE, NULL);
}
static void
fiv_sidebar_class_init(FivSidebarClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	object_class->dispose = fiv_sidebar_dispose;
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
	widget_class->realize = fiv_sidebar_realize;
	// You're giving me no choice, Adwaita.
	// Your style is hardcoded to match against the class' CSS name.
	// And I need to replicate the internal widget structure.
	gtk_widget_class_set_css_name(widget_class, "placessidebar");
	// TODO(p): Consider a return value, and using it.
	sidebar_signals[OPEN_LOCATION] =
		g_signal_new("open-location", G_TYPE_FROM_CLASS(klass), 0, 0,
			NULL, NULL, NULL, G_TYPE_NONE,
			2, G_TYPE_FILE, GTK_TYPE_PLACES_OPEN_FLAGS);
}
static gboolean
on_rowlabel_query_tooltip(GtkWidget *widget,
	G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y,
	G_GNUC_UNUSED gboolean keyboard_tooltip, GtkTooltip *tooltip)
{
	GtkLabel *label = GTK_LABEL(widget);
	if (!pango_layout_is_ellipsized(gtk_label_get_layout(label)))
		return FALSE;
	gtk_tooltip_set_text(tooltip, gtk_label_get_text(label));
	return TRUE;
}
static gboolean
on_breadcrumb_button_release(
	G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event, gpointer user_data)
{
	// This also prevents unwanted primary button click handling in GtkListBox.
	if (event->x > gdk_window_get_width(event->window) ||
		event->y > gdk_window_get_height(event->window))
		return TRUE;
	guint state = event->state & gtk_accelerator_get_default_mod_mask();
	if (event->button != GDK_BUTTON_MIDDLE || state != 0)
		return FALSE;
	GtkListBoxRow *row = GTK_LIST_BOX_ROW(user_data);
	g_signal_emit(g_object_get_qdata(G_OBJECT(row), fiv_sidebar_self_quark()),
		sidebar_signals[OPEN_LOCATION], 0,
		g_object_get_qdata(G_OBJECT(row), fiv_sidebar_location_quark()),
		GTK_PLACES_OPEN_NEW_WINDOW);
	return TRUE;
}
static void
on_breadcrumb_gesture_drag_begin(GtkGestureDrag *drag,
	G_GNUC_UNUSED gdouble start_x, G_GNUC_UNUSED gdouble start_y,
	G_GNUC_UNUSED gpointer user_data)
{
	// Touch screen dragging is how you scroll the parent GtkScrolledWindow,
	// don't steal that gesture. Moreover, touch screen dragging fails
	// in the middle, without ever invoking drag-end.
	GtkGesture *gesture = GTK_GESTURE(drag);
	if (gdk_device_get_source(
			gdk_event_get_source_device(gtk_gesture_get_last_event(
				gesture, gtk_gesture_get_last_updated_sequence(gesture)))) ==
		GDK_SOURCE_TOUCHSCREEN)
		gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_DENIED);
}
static void
on_breadcrumb_gesture_drag_update(GtkGestureDrag *drag,
	gdouble offset_x, gdouble offset_y, gpointer user_data)
{
	GtkWidget *widget = GTK_WIDGET(user_data);
	gdouble start_x = 0, start_y = 0;
	if (!gtk_gesture_drag_get_start_point(drag, &start_x, &start_y) ||
		!gtk_drag_check_threshold(widget, start_x, start_y,
			start_x + offset_x, start_y + offset_y))
		return;
	GtkGesture *gesture = GTK_GESTURE(drag);
	gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_CLAIMED);
	GdkEvent *event = gdk_event_copy(gtk_gesture_get_last_event(
		gesture, gtk_gesture_get_last_updated_sequence(gesture)));
	GtkTargetList *target_list = gtk_target_list_new(NULL, 0);
	gtk_target_list_add_uri_targets(target_list, 0);
	gtk_drag_begin_with_coordinates(
		widget, target_list, GDK_ACTION_LINK, 1, event, start_x, start_y);
	gtk_target_list_unref(target_list);
	gdk_event_free(event);
}
static void
on_breadcrumb_drag_data_get(G_GNUC_UNUSED GtkWidget *widget,
	G_GNUC_UNUSED GdkDragContext *context, GtkSelectionData *selection_data,
	G_GNUC_UNUSED guint info, G_GNUC_UNUSED guint time_, gpointer user_data)
{
	GtkListBoxRow *row = GTK_LIST_BOX_ROW(user_data);
	GFile *location =
		g_object_get_qdata(G_OBJECT(row), fiv_sidebar_location_quark());
	gchar *uris[] = {g_file_get_uri(location), NULL};
	gtk_selection_data_set_uris(selection_data, uris);
	g_free(*uris);
}
static void
on_breadcrumb_drag_begin(G_GNUC_UNUSED GtkWidget *widget,
	GdkDragContext *context, gpointer user_data)
{
	gtk_drag_set_icon_name(context, "inode-directory-symbolic", 0, 0);
	gtk_places_sidebar_set_drop_targets_visible(user_data, TRUE, context);
}
static void
on_breadcrumb_drag_end(G_GNUC_UNUSED GtkWidget *widget,
	GdkDragContext *context, gpointer user_data)
{
	gtk_places_sidebar_set_drop_targets_visible(user_data, FALSE, context);
}
static gboolean
on_breadcrumb_button_press(GtkWidget *widget, GdkEventButton *event,
	G_GNUC_UNUSED gpointer user_data)
{
	if (!gdk_event_triggers_context_menu((const GdkEvent *) event))
		return GDK_EVENT_PROPAGATE;
	GFile *location =
		g_object_get_qdata(G_OBJECT(widget), fiv_sidebar_location_quark());
	gtk_menu_popup_at_pointer(fiv_context_menu_new(widget, location), NULL);
	return GDK_EVENT_STOP;
}
static gboolean
on_breadcrumb_popup_menu(GtkWidget *widget, G_GNUC_UNUSED gpointer user_data)
{
	GFile *location =
		g_object_get_qdata(G_OBJECT(widget), fiv_sidebar_location_quark());
	gtk_menu_popup_at_widget(fiv_context_menu_new(widget, location), widget,
		GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
	return TRUE;
}
static GtkWidget *
create_row(FivSidebar *self, GFile *file, const char *icon_name)
{
	GError *error = NULL;
	GFileInfo *info =
		g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
			G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error);
	if (!info) {
		g_debug("%s", error->message);
		g_error_free(error);
		return NULL;
	}
	const char *name = g_file_info_get_display_name(info);
	GtkWidget *rowbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
	GtkWidget *rowimage =
		gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_MENU);
	gtk_style_context_add_class(
		gtk_widget_get_style_context(rowimage), "sidebar-icon");
	gtk_container_add(GTK_CONTAINER(rowbox), rowimage);
	GtkWidget *rowlabel = gtk_label_new(name);
	gtk_label_set_ellipsize(GTK_LABEL(rowlabel), PANGO_ELLIPSIZE_END);
	gtk_widget_set_has_tooltip(rowlabel, TRUE);
	g_signal_connect(rowlabel, "query-tooltip",
		G_CALLBACK(on_rowlabel_query_tooltip), NULL);
	gtk_style_context_add_class(
		gtk_widget_get_style_context(rowlabel), "sidebar-label");
	gtk_container_add(GTK_CONTAINER(rowbox), rowlabel);
	// The revealer is primarily necessary to match Adwaita CSS rules,
	// but it conveniently also has its own GdkWindow to hook events on.
	GtkWidget *revealer = gtk_revealer_new();
	gtk_widget_add_events(
		revealer, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
	gtk_revealer_set_reveal_child(
		GTK_REVEALER(revealer), TRUE);
	gtk_revealer_set_transition_type(
		GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_NONE);
	gtk_container_add(GTK_CONTAINER(revealer), rowbox);
	GtkGesture *drag = gtk_gesture_drag_new(revealer);
	gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY);
	gtk_event_controller_set_propagation_phase(
		GTK_EVENT_CONTROLLER(drag), GTK_PHASE_BUBBLE);
	g_object_set_qdata_full(G_OBJECT(revealer),
		fiv_sidebar_drag_gesture_quark(),
		drag, (GDestroyNotify) g_object_unref);
	g_signal_connect(drag, "drag-begin",
		G_CALLBACK(on_breadcrumb_gesture_drag_begin), revealer);
	g_signal_connect(drag, "drag-update",
		G_CALLBACK(on_breadcrumb_gesture_drag_update), revealer);
	GtkWidget *row = gtk_list_box_row_new();
	g_object_set_qdata_full(G_OBJECT(row), fiv_sidebar_location_quark(),
		g_object_ref(file), (GDestroyNotify) g_object_unref);
	g_object_set_qdata_full(G_OBJECT(row), fiv_sidebar_self_quark(),
		g_object_ref(self), (GDestroyNotify) g_object_unref);
	g_signal_connect(row, "button-press-event",
		G_CALLBACK(on_breadcrumb_button_press), NULL);
	g_signal_connect(row, "popup-menu",
		G_CALLBACK(on_breadcrumb_popup_menu), NULL);
	// Drag signals need to be hooked to a widget with its own GdkWindow.
	g_signal_connect(revealer, "button-release-event",
		G_CALLBACK(on_breadcrumb_button_release), row);
	g_signal_connect(revealer, "drag-data-get",
		G_CALLBACK(on_breadcrumb_drag_data_get), row);
	g_signal_connect(revealer, "drag-begin",
		G_CALLBACK(on_breadcrumb_drag_begin), self->places);
	g_signal_connect(revealer, "drag-end",
		G_CALLBACK(on_breadcrumb_drag_end), self->places);
	gtk_container_add(GTK_CONTAINER(row), revealer);
	gtk_widget_show_all(row);
	g_object_unref(info);
	return row;
}
static void
on_update_task(GTask *task, G_GNUC_UNUSED gpointer source_object,
	G_GNUC_UNUSED gpointer task_data, G_GNUC_UNUSED GCancellable *cancellable)
{
	g_task_return_boolean(task, TRUE);
}
static void
on_update_task_done(GObject *source_object, G_GNUC_UNUSED GAsyncResult *res,
	G_GNUC_UNUSED gpointer user_data)
{
	FivSidebar *self = FIV_SIDEBAR(source_object);
	gtk_places_sidebar_set_location(
		self->places, fiv_io_model_get_location(self->model));
}
static void
reload_directories(FivSidebar *self)
{
	GFile *location = fiv_io_model_get_location(self->model);
	gtk_container_foreach(GTK_CONTAINER(self->listbox),
		(GtkCallback) gtk_widget_destroy, NULL);
	if (!location)
		return;
	GFile *iter = g_object_ref(location);
	GtkWidget *row = NULL;
	while (TRUE) {
		GFile *parent = g_file_get_parent(iter);
		g_object_unref(iter);
		if (!(iter = parent))
			break;
		if ((row = create_row(self, parent, "go-up-symbolic")))
			gtk_list_box_prepend(GTK_LIST_BOX(self->listbox), row);
	}
	// Other options are "folder-{visiting,open}-symbolic", though the former
	// is mildly inappropriate (means: open in another window).
	if ((row = create_row(self, location, "circle-filled-symbolic")))
		gtk_container_add(GTK_CONTAINER(self->listbox), row);
	gsize len = 0;
	FivIoModelEntry *const *subdirs =
		fiv_io_model_get_subdirs(self->model, &len);
	for (gsize i = 0; i < len; i++) {
		GFile *file = g_file_new_for_uri(subdirs[i]->uri);
		if ((row = create_row(self, file, "go-down-symbolic")))
			gtk_container_add(GTK_CONTAINER(self->listbox), row);
		g_object_unref(file);
	}
}
static void
on_model_subdirectories_changed(G_GNUC_UNUSED FivIoModel *model,
	FivIoModelEntry *old, FivIoModelEntry *new, gpointer user_data)
{
	FivSidebar *self = FIV_SIDEBAR(user_data);
	// TODO(p): Optimize: there's no need to update parent directories.
	if (!old || !new || strcmp(old->uri, new->uri))
		reload_directories(self);
}
static void
update_location(FivSidebar *self)
{
	GFile *location = fiv_io_model_get_location(self->model);
	GFile *collection = g_file_new_for_uri(FIV_COLLECTION_SCHEME ":/");
	gtk_places_sidebar_remove_shortcut(self->places, collection);
	if (location && g_file_has_uri_scheme(location, FIV_COLLECTION_SCHEME)) {
		// add_shortcut() asynchronously requests GFileInfo, and only fills in
		// the new row's "uri" data field once that's finished, resulting in
		// the immediate set_location() call below failing to find it.
		gtk_places_sidebar_add_shortcut(self->places, collection);
		// Queue up a callback using the same mechanism that GFile uses.
		GTask *task = g_task_new(self, NULL, on_update_task_done, NULL);
		g_task_set_name(task, __func__);
		g_task_set_priority(task, G_PRIORITY_LOW);
		g_task_run_in_thread(task, on_update_task);
		g_object_unref(task);
	}
	g_object_unref(collection);
	gtk_places_sidebar_set_location(self->places, location);
	reload_directories(self);
}
static void
on_open_breadcrumb(
	G_GNUC_UNUSED GtkListBox *listbox, GtkListBoxRow *row, gpointer user_data)
{
	FivSidebar *self = FIV_SIDEBAR(user_data);
	GFile *location =
		g_object_get_qdata(G_OBJECT(row), fiv_sidebar_location_quark());
	g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0,
		location, GTK_PLACES_OPEN_NORMAL);
}
static void
on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location,
	GtkPlacesOpenFlags flags, gpointer user_data)
{
	FivSidebar *self = FIV_SIDEBAR(user_data);
	g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, location, flags);
	// Deselect the item in GtkPlacesSidebar, if unsuccessful.
	update_location(self);
}
static void
complete_path(GFile *location, GtkListStore *model)
{
	// TODO(p): Do not enter directories unless followed by '/'.
	// This information has already been stripped from `location`.
	// TODO(p): Try out GFileCompleter.
	GFile *parent = G_FILE_TYPE_DIRECTORY ==
			g_file_query_file_type(location, G_FILE_QUERY_INFO_NONE, NULL)
		? g_object_ref(location)
		: g_file_get_parent(location);
	if (!parent)
		return;
	GFileEnumerator *enumerator = g_file_enumerate_children(parent,
		G_FILE_ATTRIBUTE_STANDARD_NAME
		"," G_FILE_ATTRIBUTE_STANDARD_TYPE
		"," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
		G_FILE_QUERY_INFO_NONE, NULL, NULL);
	if (!enumerator)
		goto fail_enumerator;
	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 (g_file_info_has_attribute(info,
				G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
			g_file_info_get_is_hidden(info))
			continue;
		gchar *parse_name = g_file_get_parse_name(child);
		if (!g_str_has_suffix(parse_name, G_DIR_SEPARATOR_S)) {
			gchar *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);
		g_free(parse_name);
	}
	g_object_unref(enumerator);
fail_enumerator:
	g_object_unref(parent);
}
static GFile *
resolve_location(FivSidebar *self, const char *text)
{
	// Relative paths produce invalid GFile objects with this function.
	// And even if they didn't, we have our own root for them.
	GFile *file = g_file_parse_name(text);
	if (g_file_peek_path(file))
		return file;
	// Neither branch looks like a particularly good solution.
	// Though in general, false positives are preferred over negatives.
#if GLIB_CHECK_VERSION(2, 66, 0)
	if (g_uri_is_valid(text, G_URI_FLAGS_PARSE_RELAXED, NULL))
		return file;
#else
	gchar *scheme = g_uri_parse_scheme(text);
	g_free(scheme);
	if (scheme)
		return file;
#endif
	GFile *absolute = g_file_get_child_for_display_name(
		fiv_io_model_get_location(self->model), text, NULL);
	if (!absolute)
		return file;
	g_object_unref(file);
	return absolute;
}
static void
on_enter_location_changed(GtkEntry *entry, gpointer user_data)
{
	FivSidebar *self = FIV_SIDEBAR(user_data);
	const char *text = gtk_entry_get_text(entry);
	GFile *location = resolve_location(self, text);
	// Don't touch the network anywhere around here, URIs are a no-no.
	GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(entry));
	if (!g_file_peek_path(location) || g_file_query_exists(location, NULL))
		gtk_style_context_remove_class(style, GTK_STYLE_CLASS_WARNING);
	else
		gtk_style_context_add_class(style, GTK_STYLE_CLASS_WARNING);
	// XXX: For some reason, this jumps around with longer lists.
	GtkEntryCompletion *completion = gtk_entry_get_completion(entry);
	GtkTreeModel *model = gtk_entry_completion_get_model(completion);
	gtk_list_store_clear(GTK_LIST_STORE(model));
	if (g_file_peek_path(location))
		complete_path(location, GTK_LIST_STORE(model));
	g_object_unref(location);
}
static void
on_show_enter_location(
	G_GNUC_UNUSED GtkPlacesSidebar *sidebar, G_GNUC_UNUSED gpointer user_data)
{
	FivSidebar *self = FIV_SIDEBAR(user_data);
	GtkWidget *dialog = gtk_dialog_new_with_buttons("Enter location",
		GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(self))),
		GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL |
			GTK_DIALOG_USE_HEADER_BAR,
		"_Open", GTK_RESPONSE_ACCEPT, "_Cancel", GTK_RESPONSE_CANCEL, NULL);
	GtkListStore *model = gtk_list_store_new(1, G_TYPE_STRING);
	gtk_tree_sortable_set_sort_column_id(
		GTK_TREE_SORTABLE(model), 0, GTK_SORT_ASCENDING);
	GtkEntryCompletion *completion = gtk_entry_completion_new();
	gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model));
	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(
		completion, (GtkEntryCompletionMatchFunc) gtk_true, NULL, NULL);
	g_object_unref(model);
	GtkWidget *entry = gtk_entry_new();
	gtk_entry_set_completion(GTK_ENTRY(entry), completion);
	gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
	g_signal_connect(entry, "changed",
		G_CALLBACK(on_enter_location_changed), self);
	GFile *location = fiv_io_model_get_location(self->model);
	if (location) {
		gchar *parse_name = g_file_get_parse_name(location);
		gtk_entry_set_text(GTK_ENTRY(entry), parse_name);
		g_free(parse_name);
	}
	// Can't have it ellipsized and word-wrapped at the same time.
	GtkWidget *protocols = gtk_label_new("");
	gtk_label_set_ellipsize(GTK_LABEL(protocols), PANGO_ELLIPSIZE_END);
	gtk_label_set_xalign(GTK_LABEL(protocols), 0);
	gchar *protos = g_strjoinv(
		", ", (gchar **) g_vfs_get_supported_uri_schemes(g_vfs_get_default()));
	gchar *label = g_strdup_printf("Available protocols: %s", protos);
	g_free(protos);
	gtk_label_set_markup(GTK_LABEL(protocols), label);
	g_free(label);
	GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
	g_object_set(content, "margin", 12, NULL);
	gtk_box_set_spacing(GTK_BOX(content), 6);
	gtk_container_add(GTK_CONTAINER(content), entry);
	gtk_container_add(GTK_CONTAINER(content), protocols);
	gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
	gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
	gtk_window_set_default_size(GTK_WINDOW(dialog), 800, -1);
	gtk_window_set_geometry_hints(GTK_WINDOW(dialog), NULL,
		&(GdkGeometry) {.max_width = G_MAXSHORT, .max_height = -1},
		GDK_HINT_MAX_SIZE);
	gtk_widget_show_all(dialog);
	if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
		const char *text = gtk_entry_get_text(GTK_ENTRY(entry));
		GFile *location = resolve_location(self, text);
		g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0,
			location, GTK_PLACES_OPEN_NORMAL);
		g_object_unref(location);
	}
	gtk_widget_destroy(dialog);
	g_object_unref(completion);
	// Deselect the item in GtkPlacesSidebar, if unsuccessful.
	update_location(self);
}
static void
fiv_sidebar_init(FivSidebar *self)
{
	// TODO(p): Transplant functionality from the shitty GtkPlacesSidebar.
	// We cannot reasonably place any new items within its own GtkListBox,
	// so we need to replicate the style hierarchy to some extent.
	GtkWidget *places = gtk_places_sidebar_new();
	self->places = GTK_PLACES_SIDEBAR(places);
	gtk_places_sidebar_set_show_recent(self->places, FALSE);
	gtk_places_sidebar_set_show_trash(self->places, FALSE);
	gtk_places_sidebar_set_open_flags(self->places,
		GTK_PLACES_OPEN_NORMAL | GTK_PLACES_OPEN_NEW_WINDOW);
	g_signal_connect(self->places, "open-location",
		G_CALLBACK(on_open_location), self);
	gint minimum_width = -1;
	gtk_widget_get_size_request(places, &minimum_width, NULL);
	gtk_widget_set_size_request(places, minimum_width, -1);
	gtk_places_sidebar_set_show_enter_location(self->places, TRUE);
	g_signal_connect(self->places, "show-enter-location",
		G_CALLBACK(on_show_enter_location), self);
	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->places),
		GTK_POLICY_NEVER, GTK_POLICY_NEVER);
	self->listbox = gtk_list_box_new();
	gtk_list_box_set_selection_mode(
		GTK_LIST_BOX(self->listbox), GTK_SELECTION_NONE);
	g_signal_connect(self->listbox, "row-activated",
		G_CALLBACK(on_open_breadcrumb), self);
	// Fill up what would otherwise be wasted space,
	// as it is in the examples of Nautilus and Thunar.
	GtkWidget *superbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
	gtk_container_add(
		GTK_CONTAINER(superbox), GTK_WIDGET(self->places));
	gtk_container_add(
		GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL));
	gtk_container_add(
		GTK_CONTAINER(superbox), self->listbox);
	gtk_container_add(GTK_CONTAINER(self), superbox);
	gtk_scrolled_window_set_policy(
		GTK_SCROLLED_WINDOW(self), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
	gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)),
		GTK_STYLE_CLASS_SIDEBAR);
	gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)),
		"fiv");
}
// --- Public interface --------------------------------------------------------
GtkWidget *
fiv_sidebar_new(FivIoModel *model)
{
	g_return_val_if_fail(FIV_IS_IO_MODEL(model), NULL);
	FivSidebar *self = g_object_new(FIV_TYPE_SIDEBAR, NULL);
	// This doesn't work from the init function.
	GtkWidget *sidebar_port = gtk_bin_get_child(GTK_BIN(self));
	gtk_container_set_focus_hadjustment(GTK_CONTAINER(sidebar_port),
		gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(self)));
	gtk_container_set_focus_vadjustment(GTK_CONTAINER(sidebar_port),
		gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(self)));
	self->model = g_object_ref(model);
	g_signal_connect_swapped(self->model, "reloaded",
		G_CALLBACK(update_location), self);
	g_signal_connect(self->model, "subdirectories-changed",
		G_CALLBACK(on_model_subdirectories_changed), self);
	return GTK_WIDGET(self);
}
void
fiv_sidebar_show_enter_location(FivSidebar *self)
{
	g_return_if_fail(FIV_IS_SIDEBAR(self));
	g_signal_emit_by_name(self->places, "show-enter-location");
}