forked from AuroraMiddleware/gtk
95d48ac2e8
The places view has questionable code for handling 'destroy', which seems to trigger use-after-free in some cases. Clean this up a bit by acknowledging the cancellation fully, rather than relying on the destroyed flag.
2545 lines
74 KiB
C
2545 lines
74 KiB
C
/* gtkplacesview.c
|
|
*
|
|
* Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <gio/gio.h>
|
|
#include <gio/gvfs.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include "gtkprivate.h"
|
|
#include "gtkintl.h"
|
|
#include "gtkmarshalers.h"
|
|
#include "gtkplacesviewprivate.h"
|
|
#include "gtkplacesviewrowprivate.h"
|
|
#include "gtktypebuiltins.h"
|
|
#include "gtkprivatetypebuiltins.h"
|
|
#include "gtkeventcontrollerkey.h"
|
|
#include "gtkpopovermenu.h"
|
|
|
|
/*
|
|
* SECTION:gtkplacesview
|
|
* @Short_description: Widget that displays persistent drives and manages mounted networks
|
|
* @Title: GtkPlacesView
|
|
* @See_also: #GtkFileChooser
|
|
*
|
|
* #GtkPlacesView is a widget that displays a list of persistent drives
|
|
* such as harddisk partitions and networks. #GtkPlacesView does not monitor
|
|
* removable devices.
|
|
*
|
|
* The places view displays drives and networks, and will automatically mount
|
|
* them when the user activates. Network addresses are stored even if they fail
|
|
* to connect. When the connection is successful, the connected network is
|
|
* shown at the network list.
|
|
*
|
|
* To make use of the places view, an application at least needs to connect
|
|
* to the #GtkPlacesView::open-location signal. This is emitted when the user
|
|
* selects a location to open in the view.
|
|
*/
|
|
|
|
struct _GtkPlacesViewClass
|
|
{
|
|
GtkBoxClass parent_class;
|
|
|
|
void (* open_location) (GtkPlacesView *view,
|
|
GFile *location,
|
|
GtkPlacesOpenFlags open_flags);
|
|
|
|
void (* show_error_message) (GtkPlacesSidebar *sidebar,
|
|
const char *primary,
|
|
const char *secondary);
|
|
};
|
|
|
|
struct _GtkPlacesView
|
|
{
|
|
GtkBox parent_instance;
|
|
|
|
GVolumeMonitor *volume_monitor;
|
|
GtkPlacesOpenFlags open_flags;
|
|
GtkPlacesOpenFlags current_open_flags;
|
|
|
|
GFile *server_list_file;
|
|
GFileMonitor *server_list_monitor;
|
|
GFileMonitor *network_monitor;
|
|
|
|
GCancellable *cancellable;
|
|
|
|
char *search_query;
|
|
|
|
GtkWidget *actionbar;
|
|
GtkWidget *address_entry;
|
|
GtkWidget *connect_button;
|
|
GtkWidget *listbox;
|
|
GtkWidget *popup_menu;
|
|
GtkWidget *recent_servers_listbox;
|
|
GtkWidget *recent_servers_popover;
|
|
GtkWidget *recent_servers_stack;
|
|
GtkWidget *stack;
|
|
GtkWidget *server_adresses_popover;
|
|
GtkWidget *available_protocols_grid;
|
|
GtkWidget *network_placeholder;
|
|
GtkWidget *network_placeholder_label;
|
|
|
|
GtkSizeGroup *path_size_group;
|
|
GtkSizeGroup *space_size_group;
|
|
|
|
GtkEntryCompletion *address_entry_completion;
|
|
GtkListStore *completion_store;
|
|
|
|
GCancellable *networks_fetching_cancellable;
|
|
|
|
GtkPlacesViewRow *row_for_action;
|
|
|
|
guint should_open_location : 1;
|
|
guint should_pulse_entry : 1;
|
|
guint entry_pulse_timeout_id;
|
|
guint connecting_to_server : 1;
|
|
guint mounting_volume : 1;
|
|
guint unmounting_mount : 1;
|
|
guint fetching_networks : 1;
|
|
guint loading : 1;
|
|
guint destroyed : 1;
|
|
};
|
|
|
|
static void mount_volume (GtkPlacesView *view,
|
|
GVolume *volume);
|
|
|
|
static void on_eject_button_clicked (GtkWidget *widget,
|
|
GtkPlacesViewRow *row);
|
|
|
|
static gboolean on_row_popup_menu (GtkWidget *widget,
|
|
GVariant *args,
|
|
gpointer user_data);
|
|
|
|
static void click_cb (GtkGesture *gesture,
|
|
int n_press,
|
|
double x,
|
|
double y,
|
|
gpointer user_data);
|
|
|
|
static void populate_servers (GtkPlacesView *view);
|
|
|
|
static gboolean gtk_places_view_get_fetching_networks (GtkPlacesView *view);
|
|
|
|
static void gtk_places_view_set_fetching_networks (GtkPlacesView *view,
|
|
gboolean fetching_networks);
|
|
|
|
static void gtk_places_view_set_loading (GtkPlacesView *view,
|
|
gboolean loading);
|
|
|
|
static void update_loading (GtkPlacesView *view);
|
|
|
|
G_DEFINE_TYPE (GtkPlacesView, gtk_places_view, GTK_TYPE_BOX)
|
|
|
|
/* GtkPlacesView properties & signals */
|
|
enum {
|
|
PROP_0,
|
|
PROP_OPEN_FLAGS,
|
|
PROP_FETCHING_NETWORKS,
|
|
PROP_LOADING,
|
|
LAST_PROP
|
|
};
|
|
|
|
enum {
|
|
OPEN_LOCATION,
|
|
SHOW_ERROR_MESSAGE,
|
|
LAST_SIGNAL
|
|
};
|
|
|
|
const char *unsupported_protocols [] =
|
|
{
|
|
"file", "afc", "obex", "http",
|
|
"trash", "burn", "computer",
|
|
"archive", "recent", "localtest",
|
|
NULL
|
|
};
|
|
|
|
static guint places_view_signals [LAST_SIGNAL] = { 0 };
|
|
static GParamSpec *properties [LAST_PROP];
|
|
|
|
static void
|
|
emit_open_location (GtkPlacesView *view,
|
|
GFile *location,
|
|
GtkPlacesOpenFlags open_flags)
|
|
{
|
|
if ((open_flags & view->open_flags) == 0)
|
|
open_flags = GTK_PLACES_OPEN_NORMAL;
|
|
|
|
g_signal_emit (view, places_view_signals[OPEN_LOCATION], 0, location, open_flags);
|
|
}
|
|
|
|
static void
|
|
emit_show_error_message (GtkPlacesView *view,
|
|
char *primary_message,
|
|
char *secondary_message)
|
|
{
|
|
g_signal_emit (view, places_view_signals[SHOW_ERROR_MESSAGE],
|
|
0, primary_message, secondary_message);
|
|
}
|
|
|
|
static void
|
|
server_file_changed_cb (GtkPlacesView *view)
|
|
{
|
|
populate_servers (view);
|
|
}
|
|
|
|
static GBookmarkFile *
|
|
server_list_load (GtkPlacesView *view)
|
|
{
|
|
GBookmarkFile *bookmarks;
|
|
GError *error = NULL;
|
|
char *datadir;
|
|
char *filename;
|
|
|
|
bookmarks = g_bookmark_file_new ();
|
|
datadir = g_build_filename (g_get_user_config_dir (), "gtk-4.0", NULL);
|
|
filename = g_build_filename (datadir, "servers", NULL);
|
|
|
|
g_mkdir_with_parents (datadir, 0700);
|
|
g_bookmark_file_load_from_file (bookmarks, filename, &error);
|
|
|
|
if (error)
|
|
{
|
|
if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
|
|
{
|
|
/* only warn if the file exists */
|
|
g_warning ("Unable to open server bookmarks: %s", error->message);
|
|
g_clear_pointer (&bookmarks, g_bookmark_file_free);
|
|
}
|
|
|
|
g_clear_error (&error);
|
|
}
|
|
|
|
/* Monitor the file in case it's modified outside this code */
|
|
if (!view->server_list_monitor)
|
|
{
|
|
view->server_list_file = g_file_new_for_path (filename);
|
|
|
|
if (view->server_list_file)
|
|
{
|
|
view->server_list_monitor = g_file_monitor_file (view->server_list_file,
|
|
G_FILE_MONITOR_NONE,
|
|
NULL,
|
|
&error);
|
|
|
|
if (error)
|
|
{
|
|
g_warning ("Cannot monitor server file: %s", error->message);
|
|
g_clear_error (&error);
|
|
}
|
|
else
|
|
{
|
|
g_signal_connect_swapped (view->server_list_monitor,
|
|
"changed",
|
|
G_CALLBACK (server_file_changed_cb),
|
|
view);
|
|
}
|
|
}
|
|
|
|
g_clear_object (&view->server_list_file);
|
|
}
|
|
|
|
g_free (datadir);
|
|
g_free (filename);
|
|
|
|
return bookmarks;
|
|
}
|
|
|
|
static void
|
|
server_list_save (GBookmarkFile *bookmarks)
|
|
{
|
|
char *filename;
|
|
|
|
filename = g_build_filename (g_get_user_config_dir (), "gtk-4.0", "servers", NULL);
|
|
g_bookmark_file_to_file (bookmarks, filename, NULL);
|
|
g_free (filename);
|
|
}
|
|
|
|
static void
|
|
server_list_add_server (GtkPlacesView *view,
|
|
GFile *file)
|
|
{
|
|
GBookmarkFile *bookmarks;
|
|
GFileInfo *info;
|
|
GError *error;
|
|
char *title;
|
|
char *uri;
|
|
GDateTime *now;
|
|
|
|
error = NULL;
|
|
bookmarks = server_list_load (view);
|
|
|
|
if (!bookmarks)
|
|
return;
|
|
|
|
uri = g_file_get_uri (file);
|
|
|
|
info = g_file_query_info (file,
|
|
G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
|
|
G_FILE_QUERY_INFO_NONE,
|
|
NULL,
|
|
&error);
|
|
title = g_file_info_get_attribute_as_string (info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
|
|
|
|
g_bookmark_file_set_title (bookmarks, uri, title);
|
|
now = g_date_time_new_now_utc ();
|
|
g_bookmark_file_set_visited_date_time (bookmarks, uri, now);
|
|
g_date_time_unref (now);
|
|
g_bookmark_file_add_application (bookmarks, uri, NULL, NULL);
|
|
|
|
server_list_save (bookmarks);
|
|
|
|
g_bookmark_file_free (bookmarks);
|
|
g_clear_object (&info);
|
|
g_free (title);
|
|
g_free (uri);
|
|
}
|
|
|
|
static void
|
|
server_list_remove_server (GtkPlacesView *view,
|
|
const char *uri)
|
|
{
|
|
GBookmarkFile *bookmarks;
|
|
|
|
bookmarks = server_list_load (view);
|
|
|
|
if (!bookmarks)
|
|
return;
|
|
|
|
g_bookmark_file_remove_item (bookmarks, uri, NULL);
|
|
server_list_save (bookmarks);
|
|
|
|
g_bookmark_file_free (bookmarks);
|
|
}
|
|
|
|
/* Returns a toplevel GtkWindow, or NULL if none */
|
|
static GtkWindow *
|
|
get_toplevel (GtkWidget *widget)
|
|
{
|
|
GtkWidget *toplevel;
|
|
|
|
toplevel = GTK_WIDGET (gtk_widget_get_root (widget));
|
|
if (GTK_IS_WINDOW (toplevel))
|
|
return GTK_WINDOW (toplevel);
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
set_busy_cursor (GtkPlacesView *view,
|
|
gboolean busy)
|
|
{
|
|
GtkWidget *widget;
|
|
GtkWindow *toplevel;
|
|
|
|
toplevel = get_toplevel (GTK_WIDGET (view));
|
|
widget = GTK_WIDGET (toplevel);
|
|
if (!toplevel || !gtk_widget_get_realized (widget))
|
|
return;
|
|
|
|
if (busy)
|
|
gtk_widget_set_cursor_from_name (widget, "progress");
|
|
else
|
|
gtk_widget_set_cursor (widget, NULL);
|
|
}
|
|
|
|
/* Activates the given row, with the given flags as parameter */
|
|
static void
|
|
activate_row (GtkPlacesView *view,
|
|
GtkPlacesViewRow *row,
|
|
GtkPlacesOpenFlags flags)
|
|
{
|
|
GVolume *volume;
|
|
GMount *mount;
|
|
GFile *file;
|
|
|
|
mount = gtk_places_view_row_get_mount (row);
|
|
volume = gtk_places_view_row_get_volume (row);
|
|
file = gtk_places_view_row_get_file (row);
|
|
|
|
if (file)
|
|
{
|
|
emit_open_location (view, file, flags);
|
|
}
|
|
else if (mount)
|
|
{
|
|
GFile *location = g_mount_get_default_location (mount);
|
|
|
|
emit_open_location (view, location, flags);
|
|
|
|
g_object_unref (location);
|
|
}
|
|
else if (volume && g_volume_can_mount (volume))
|
|
{
|
|
/*
|
|
* When the row is activated, the unmounted volume shall
|
|
* be mounted and opened right after.
|
|
*/
|
|
view->should_open_location = TRUE;
|
|
|
|
gtk_places_view_row_set_busy (row, TRUE);
|
|
mount_volume (view, volume);
|
|
}
|
|
}
|
|
|
|
static void update_places (GtkPlacesView *view);
|
|
|
|
static void
|
|
gtk_places_view_finalize (GObject *object)
|
|
{
|
|
GtkPlacesView *view = (GtkPlacesView *)object;
|
|
|
|
if (view->entry_pulse_timeout_id > 0)
|
|
g_source_remove (view->entry_pulse_timeout_id);
|
|
|
|
g_clear_pointer (&view->search_query, g_free);
|
|
g_clear_object (&view->server_list_file);
|
|
g_clear_object (&view->server_list_monitor);
|
|
g_clear_object (&view->volume_monitor);
|
|
g_clear_object (&view->network_monitor);
|
|
g_clear_object (&view->cancellable);
|
|
g_clear_object (&view->networks_fetching_cancellable);
|
|
g_clear_object (&view->path_size_group);
|
|
g_clear_object (&view->space_size_group);
|
|
|
|
G_OBJECT_CLASS (gtk_places_view_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
gtk_places_view_dispose (GObject *object)
|
|
{
|
|
GtkPlacesView *view = (GtkPlacesView *)object;
|
|
|
|
view->destroyed = 1;
|
|
|
|
g_signal_handlers_disconnect_by_func (view->volume_monitor, update_places, object);
|
|
|
|
if (view->network_monitor)
|
|
g_signal_handlers_disconnect_by_func (view->network_monitor, update_places, object);
|
|
|
|
if (view->server_list_monitor)
|
|
g_signal_handlers_disconnect_by_func (view->server_list_monitor, server_file_changed_cb, object);
|
|
|
|
g_cancellable_cancel (view->cancellable);
|
|
g_cancellable_cancel (view->networks_fetching_cancellable);
|
|
g_clear_pointer (&view->popup_menu, gtk_widget_unparent);
|
|
|
|
G_OBJECT_CLASS (gtk_places_view_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
gtk_places_view_get_property (GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
GtkPlacesView *self = GTK_PLACES_VIEW (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_LOADING:
|
|
g_value_set_boolean (value, gtk_places_view_get_loading (self));
|
|
break;
|
|
|
|
case PROP_OPEN_FLAGS:
|
|
g_value_set_flags (value, gtk_places_view_get_open_flags (self));
|
|
break;
|
|
|
|
case PROP_FETCHING_NETWORKS:
|
|
g_value_set_boolean (value, gtk_places_view_get_fetching_networks (self));
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
gtk_places_view_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
GtkPlacesView *self = GTK_PLACES_VIEW (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_OPEN_FLAGS:
|
|
gtk_places_view_set_open_flags (self, g_value_get_flags (value));
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
is_external_volume (GVolume *volume)
|
|
{
|
|
gboolean is_external;
|
|
GDrive *drive;
|
|
char *id;
|
|
|
|
drive = g_volume_get_drive (volume);
|
|
id = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_CLASS);
|
|
|
|
is_external = g_volume_can_eject (volume);
|
|
|
|
/* NULL volume identifier only happens on removable devices */
|
|
is_external |= !id;
|
|
|
|
if (drive)
|
|
is_external |= g_drive_is_removable (drive);
|
|
|
|
g_clear_object (&drive);
|
|
g_free (id);
|
|
|
|
return is_external;
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
char *uri;
|
|
GtkPlacesView *view;
|
|
} RemoveServerData;
|
|
|
|
static void
|
|
on_remove_server_button_clicked (RemoveServerData *data)
|
|
{
|
|
server_list_remove_server (data->view, data->uri);
|
|
|
|
populate_servers (data->view);
|
|
}
|
|
|
|
static void
|
|
populate_servers (GtkPlacesView *view)
|
|
{
|
|
GBookmarkFile *server_list;
|
|
GtkWidget *child;
|
|
char **uris;
|
|
gsize num_uris;
|
|
int i;
|
|
|
|
server_list = server_list_load (view);
|
|
|
|
if (!server_list)
|
|
return;
|
|
|
|
uris = g_bookmark_file_get_uris (server_list, &num_uris);
|
|
|
|
gtk_stack_set_visible_child_name (GTK_STACK (view->recent_servers_stack),
|
|
num_uris > 0 ? "list" : "empty");
|
|
|
|
if (!uris)
|
|
{
|
|
g_bookmark_file_free (server_list);
|
|
return;
|
|
}
|
|
|
|
/* clear previous items */
|
|
while ((child = gtk_widget_get_first_child (GTK_WIDGET (view->recent_servers_listbox))))
|
|
gtk_list_box_remove (GTK_LIST_BOX (view->listbox), child);
|
|
|
|
gtk_list_store_clear (view->completion_store);
|
|
|
|
for (i = 0; i < num_uris; i++)
|
|
{
|
|
RemoveServerData *data;
|
|
GtkTreeIter iter;
|
|
GtkWidget *row;
|
|
GtkWidget *grid;
|
|
GtkWidget *button;
|
|
GtkWidget *label;
|
|
char *name;
|
|
char *dup_uri;
|
|
|
|
name = g_bookmark_file_get_title (server_list, uris[i], NULL);
|
|
dup_uri = g_strdup (uris[i]);
|
|
|
|
/* add to the completion list */
|
|
gtk_list_store_append (view->completion_store, &iter);
|
|
gtk_list_store_set (view->completion_store,
|
|
&iter,
|
|
0, name,
|
|
1, uris[i],
|
|
-1);
|
|
|
|
/* add to the recent servers listbox */
|
|
row = gtk_list_box_row_new ();
|
|
|
|
grid = g_object_new (GTK_TYPE_GRID,
|
|
"orientation", GTK_ORIENTATION_VERTICAL,
|
|
NULL);
|
|
|
|
/* name of the connected uri, if any */
|
|
label = gtk_label_new (name);
|
|
gtk_widget_set_hexpand (label, TRUE);
|
|
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
|
|
gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
|
|
gtk_grid_attach (GTK_GRID (grid), label, 0, 0, 1, 1);
|
|
|
|
/* the uri itself */
|
|
label = gtk_label_new (uris[i]);
|
|
gtk_widget_set_hexpand (label, TRUE);
|
|
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
|
|
gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
|
|
gtk_widget_add_css_class (label, "dim-label");
|
|
gtk_grid_attach (GTK_GRID (grid), label, 0, 1, 1, 1);
|
|
|
|
/* remove button */
|
|
button = gtk_button_new_from_icon_name ("window-close-symbolic");
|
|
gtk_widget_set_halign (button, GTK_ALIGN_END);
|
|
gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
|
|
gtk_button_set_has_frame (GTK_BUTTON (button), FALSE);
|
|
gtk_widget_add_css_class (button, "sidebar-button");
|
|
gtk_grid_attach (GTK_GRID (grid), button, 1, 0, 1, 2);
|
|
|
|
gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), grid);
|
|
gtk_list_box_insert (GTK_LIST_BOX (view->recent_servers_listbox), row, -1);
|
|
|
|
/* custom data */
|
|
data = g_new0 (RemoveServerData, 1);
|
|
data->view = view;
|
|
data->uri = dup_uri;
|
|
|
|
g_object_set_data_full (G_OBJECT (row), "uri", dup_uri, g_free);
|
|
g_object_set_data_full (G_OBJECT (row), "remove-server-data", data, g_free);
|
|
|
|
g_signal_connect_swapped (button,
|
|
"clicked",
|
|
G_CALLBACK (on_remove_server_button_clicked),
|
|
data);
|
|
|
|
g_free (name);
|
|
}
|
|
|
|
g_strfreev (uris);
|
|
g_bookmark_file_free (server_list);
|
|
}
|
|
|
|
static void
|
|
update_view_mode (GtkPlacesView *view)
|
|
{
|
|
GtkWidget *child;
|
|
gboolean show_listbox;
|
|
|
|
show_listbox = FALSE;
|
|
|
|
/* drives */
|
|
for (child = gtk_widget_get_first_child (GTK_WIDGET (view->listbox));
|
|
child != NULL;
|
|
child = gtk_widget_get_next_sibling (child))
|
|
{
|
|
/* GtkListBox filter rows by changing their GtkWidget::child-visible property */
|
|
if (gtk_widget_get_child_visible (child))
|
|
{
|
|
show_listbox = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!show_listbox &&
|
|
view->search_query &&
|
|
view->search_query[0] != '\0')
|
|
{
|
|
gtk_stack_set_visible_child_name (GTK_STACK (view->stack), "empty-search");
|
|
}
|
|
else
|
|
{
|
|
gtk_stack_set_visible_child_name (GTK_STACK (view->stack), "browse");
|
|
}
|
|
}
|
|
|
|
static void
|
|
insert_row (GtkPlacesView *view,
|
|
GtkWidget *row,
|
|
gboolean is_network)
|
|
{
|
|
GtkEventController *controller;
|
|
GtkShortcutTrigger *trigger;
|
|
GtkShortcutAction *action;
|
|
GtkShortcut *shortcut;
|
|
GtkGesture *gesture;
|
|
|
|
g_object_set_data (G_OBJECT (row), "is-network", GINT_TO_POINTER (is_network));
|
|
|
|
controller = gtk_shortcut_controller_new ();
|
|
trigger = gtk_alternative_trigger_new (gtk_keyval_trigger_new (GDK_KEY_F10, GDK_SHIFT_MASK),
|
|
gtk_keyval_trigger_new (GDK_KEY_Menu, 0));
|
|
action = gtk_callback_action_new (on_row_popup_menu, row, NULL);
|
|
shortcut = gtk_shortcut_new (trigger, action);
|
|
gtk_shortcut_controller_add_shortcut (GTK_SHORTCUT_CONTROLLER (controller), shortcut);
|
|
gtk_widget_add_controller (GTK_WIDGET (row), controller);
|
|
|
|
gesture = gtk_gesture_click_new ();
|
|
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), GDK_BUTTON_SECONDARY);
|
|
g_signal_connect (gesture, "pressed", G_CALLBACK (click_cb), row);
|
|
gtk_widget_add_controller (row, GTK_EVENT_CONTROLLER (gesture));
|
|
|
|
g_signal_connect (gtk_places_view_row_get_eject_button (GTK_PLACES_VIEW_ROW (row)),
|
|
"clicked",
|
|
G_CALLBACK (on_eject_button_clicked),
|
|
row);
|
|
|
|
gtk_places_view_row_set_path_size_group (GTK_PLACES_VIEW_ROW (row), view->path_size_group);
|
|
gtk_places_view_row_set_space_size_group (GTK_PLACES_VIEW_ROW (row), view->space_size_group);
|
|
|
|
gtk_list_box_insert (GTK_LIST_BOX (view->listbox), row, -1);
|
|
}
|
|
|
|
static void
|
|
add_volume (GtkPlacesView *view,
|
|
GVolume *volume)
|
|
{
|
|
gboolean is_network;
|
|
GMount *mount;
|
|
GFile *root;
|
|
GIcon *icon;
|
|
char *identifier;
|
|
char *name;
|
|
char *path;
|
|
|
|
if (is_external_volume (volume))
|
|
return;
|
|
|
|
identifier = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_CLASS);
|
|
is_network = g_strcmp0 (identifier, "network") == 0;
|
|
|
|
mount = g_volume_get_mount (volume);
|
|
root = mount ? g_mount_get_default_location (mount) : NULL;
|
|
icon = g_volume_get_icon (volume);
|
|
name = g_volume_get_name (volume);
|
|
path = !is_network ? g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE) : NULL;
|
|
|
|
if (!mount || !g_mount_is_shadowed (mount))
|
|
{
|
|
GtkWidget *row;
|
|
|
|
row = g_object_new (GTK_TYPE_PLACES_VIEW_ROW,
|
|
"icon", icon,
|
|
"name", name,
|
|
"path", path ? path : "",
|
|
"volume", volume,
|
|
"mount", mount,
|
|
"file", NULL,
|
|
"is-network", is_network,
|
|
NULL);
|
|
|
|
insert_row (view, row, is_network);
|
|
}
|
|
|
|
g_clear_object (&root);
|
|
g_clear_object (&icon);
|
|
g_clear_object (&mount);
|
|
g_free (identifier);
|
|
g_free (name);
|
|
g_free (path);
|
|
}
|
|
|
|
static void
|
|
add_mount (GtkPlacesView *view,
|
|
GMount *mount)
|
|
{
|
|
gboolean is_network;
|
|
GFile *root;
|
|
GIcon *icon;
|
|
char *name;
|
|
char *path;
|
|
char *uri;
|
|
char *schema;
|
|
|
|
icon = g_mount_get_icon (mount);
|
|
name = g_mount_get_name (mount);
|
|
root = g_mount_get_default_location (mount);
|
|
path = root ? g_file_get_parse_name (root) : NULL;
|
|
uri = g_file_get_uri (root);
|
|
schema = g_uri_parse_scheme (uri);
|
|
is_network = g_strcmp0 (schema, "file") != 0;
|
|
|
|
if (is_network)
|
|
g_clear_pointer (&path, g_free);
|
|
|
|
if (!g_mount_is_shadowed (mount))
|
|
{
|
|
GtkWidget *row;
|
|
|
|
row = g_object_new (GTK_TYPE_PLACES_VIEW_ROW,
|
|
"icon", icon,
|
|
"name", name,
|
|
"path", path ? path : "",
|
|
"volume", NULL,
|
|
"mount", mount,
|
|
"file", NULL,
|
|
"is-network", is_network,
|
|
NULL);
|
|
|
|
insert_row (view, row, is_network);
|
|
}
|
|
|
|
g_clear_object (&root);
|
|
g_clear_object (&icon);
|
|
g_free (name);
|
|
g_free (path);
|
|
g_free (uri);
|
|
g_free (schema);
|
|
}
|
|
|
|
static void
|
|
add_drive (GtkPlacesView *view,
|
|
GDrive *drive)
|
|
{
|
|
GList *volumes;
|
|
GList *l;
|
|
|
|
volumes = g_drive_get_volumes (drive);
|
|
|
|
for (l = volumes; l != NULL; l = l->next)
|
|
add_volume (view, l->data);
|
|
|
|
g_list_free_full (volumes, g_object_unref);
|
|
}
|
|
|
|
static void
|
|
add_file (GtkPlacesView *view,
|
|
GFile *file,
|
|
GIcon *icon,
|
|
const char *display_name,
|
|
const char *path,
|
|
gboolean is_network)
|
|
{
|
|
GtkWidget *row;
|
|
row = g_object_new (GTK_TYPE_PLACES_VIEW_ROW,
|
|
"icon", icon,
|
|
"name", display_name,
|
|
"path", path,
|
|
"volume", NULL,
|
|
"mount", NULL,
|
|
"file", file,
|
|
"is_network", is_network,
|
|
NULL);
|
|
|
|
insert_row (view, row, is_network);
|
|
}
|
|
|
|
static gboolean
|
|
has_networks (GtkPlacesView *view)
|
|
{
|
|
GtkWidget *child;
|
|
gboolean has_network = FALSE;
|
|
|
|
for (child = gtk_widget_get_first_child (GTK_WIDGET (view->listbox));
|
|
child != NULL;
|
|
child = gtk_widget_get_next_sibling (child))
|
|
{
|
|
if (GPOINTER_TO_INT (g_object_get_data (G_OBJECT (child), "is-network")) &&
|
|
g_object_get_data (G_OBJECT (child), "is-placeholder") == NULL)
|
|
{
|
|
has_network = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return has_network;
|
|
}
|
|
|
|
static void
|
|
update_network_state (GtkPlacesView *view)
|
|
{
|
|
if (view->network_placeholder == NULL)
|
|
{
|
|
view->network_placeholder = gtk_list_box_row_new ();
|
|
view->network_placeholder_label = gtk_label_new ("");
|
|
gtk_label_set_xalign (GTK_LABEL (view->network_placeholder_label), 0.0);
|
|
gtk_widget_set_margin_start (view->network_placeholder_label, 12);
|
|
gtk_widget_set_margin_end (view->network_placeholder_label, 12);
|
|
gtk_widget_set_margin_top (view->network_placeholder_label, 6);
|
|
gtk_widget_set_margin_bottom (view->network_placeholder_label, 6);
|
|
gtk_widget_set_hexpand (view->network_placeholder_label, TRUE);
|
|
gtk_widget_set_sensitive (view->network_placeholder, FALSE);
|
|
gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (view->network_placeholder),
|
|
view->network_placeholder_label);
|
|
g_object_set_data (G_OBJECT (view->network_placeholder),
|
|
"is-network", GINT_TO_POINTER (TRUE));
|
|
/* mark the row as placeholder, so it always goes first */
|
|
g_object_set_data (G_OBJECT (view->network_placeholder),
|
|
"is-placeholder", GINT_TO_POINTER (TRUE));
|
|
gtk_list_box_insert (GTK_LIST_BOX (view->listbox), view->network_placeholder, -1);
|
|
}
|
|
|
|
if (gtk_places_view_get_fetching_networks (view))
|
|
{
|
|
/* only show a placeholder with a message if the list is empty.
|
|
* otherwise just show the spinner in the header */
|
|
if (!has_networks (view))
|
|
{
|
|
gtk_widget_show (view->network_placeholder);
|
|
gtk_label_set_text (GTK_LABEL (view->network_placeholder_label),
|
|
_("Searching for network locations"));
|
|
}
|
|
}
|
|
else if (!has_networks (view))
|
|
{
|
|
gtk_widget_show (view->network_placeholder);
|
|
gtk_label_set_text (GTK_LABEL (view->network_placeholder_label),
|
|
_("No network locations found"));
|
|
}
|
|
else
|
|
{
|
|
gtk_widget_hide (view->network_placeholder);
|
|
}
|
|
}
|
|
|
|
static void
|
|
monitor_network (GtkPlacesView *view)
|
|
{
|
|
GFile *network_file;
|
|
GError *error;
|
|
|
|
if (view->network_monitor)
|
|
return;
|
|
|
|
error = NULL;
|
|
network_file = g_file_new_for_uri ("network:///");
|
|
view->network_monitor = g_file_monitor (network_file,
|
|
G_FILE_MONITOR_NONE,
|
|
NULL,
|
|
&error);
|
|
|
|
g_clear_object (&network_file);
|
|
|
|
if (error)
|
|
{
|
|
g_warning ("Error monitoring network: %s", error->message);
|
|
g_clear_error (&error);
|
|
return;
|
|
}
|
|
|
|
g_signal_connect_swapped (view->network_monitor,
|
|
"changed",
|
|
G_CALLBACK (update_places),
|
|
view);
|
|
}
|
|
|
|
static void
|
|
populate_networks (GtkPlacesView *view,
|
|
GFileEnumerator *enumerator,
|
|
GList *detected_networks)
|
|
{
|
|
GList *l;
|
|
GFile *file;
|
|
GFile *activatable_file;
|
|
char *uri;
|
|
GFileType type;
|
|
GIcon *icon;
|
|
char *display_name;
|
|
|
|
for (l = detected_networks; l != NULL; l = l->next)
|
|
{
|
|
file = g_file_enumerator_get_child (enumerator, l->data);
|
|
type = g_file_info_get_file_type (l->data);
|
|
if (type == G_FILE_TYPE_SHORTCUT || type == G_FILE_TYPE_MOUNTABLE)
|
|
uri = g_file_info_get_attribute_as_string (l->data, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
|
|
else
|
|
uri = g_file_get_uri (file);
|
|
activatable_file = g_file_new_for_uri (uri);
|
|
display_name = g_file_info_get_attribute_as_string (l->data, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
|
|
icon = g_file_info_get_icon (l->data);
|
|
|
|
add_file (view, activatable_file, icon, display_name, NULL, TRUE);
|
|
|
|
g_free (uri);
|
|
g_free (display_name);
|
|
g_clear_object (&file);
|
|
g_clear_object (&activatable_file);
|
|
}
|
|
}
|
|
|
|
static void
|
|
network_enumeration_next_files_finished (GObject *source_object,
|
|
GAsyncResult *res,
|
|
gpointer user_data)
|
|
{
|
|
GtkPlacesView *view;
|
|
GList *detected_networks;
|
|
GError *error;
|
|
|
|
view = GTK_PLACES_VIEW (user_data);
|
|
error = NULL;
|
|
|
|
detected_networks = g_file_enumerator_next_files_finish (G_FILE_ENUMERATOR (source_object),
|
|
res, &error);
|
|
|
|
if (error)
|
|
{
|
|
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
|
|
{
|
|
g_clear_error (&error);
|
|
g_object_unref (view);
|
|
return;
|
|
}
|
|
|
|
g_warning ("Failed to fetch network locations: %s", error->message);
|
|
g_clear_error (&error);
|
|
}
|
|
else
|
|
{
|
|
gtk_places_view_set_fetching_networks (view, FALSE);
|
|
populate_networks (view, G_FILE_ENUMERATOR (source_object), detected_networks);
|
|
|
|
g_list_free_full (detected_networks, g_object_unref);
|
|
}
|
|
|
|
update_network_state (view);
|
|
monitor_network (view);
|
|
update_loading (view);
|
|
|
|
g_object_unref (view);
|
|
}
|
|
|
|
static void
|
|
network_enumeration_finished (GObject *source_object,
|
|
GAsyncResult *res,
|
|
gpointer user_data)
|
|
{
|
|
GtkPlacesView *view = GTK_PLACES_VIEW (user_data);
|
|
GFileEnumerator *enumerator;
|
|
GError *error;
|
|
|
|
error = NULL;
|
|
enumerator = g_file_enumerate_children_finish (G_FILE (source_object), res, &error);
|
|
|
|
if (error)
|
|
{
|
|
if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
|
|
!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
|
|
g_warning ("Failed to fetch network locations: %s", error->message);
|
|
|
|
g_clear_error (&error);
|
|
g_object_unref (view);
|
|
}
|
|
else
|
|
{
|
|
g_file_enumerator_next_files_async (enumerator,
|
|
G_MAXINT32,
|
|
G_PRIORITY_DEFAULT,
|
|
view->networks_fetching_cancellable,
|
|
network_enumeration_next_files_finished,
|
|
user_data);
|
|
g_object_unref (enumerator);
|
|
}
|
|
}
|
|
|
|
static void
|
|
fetch_networks (GtkPlacesView *view)
|
|
{
|
|
GFile *network_file;
|
|
const char * const *supported_uris;
|
|
gboolean found;
|
|
|
|
supported_uris = g_vfs_get_supported_uri_schemes (g_vfs_get_default ());
|
|
|
|
for (found = FALSE; !found && supported_uris && supported_uris[0]; supported_uris++)
|
|
if (g_strcmp0 (supported_uris[0], "network") == 0)
|
|
found = TRUE;
|
|
|
|
if (!found)
|
|
return;
|
|
|
|
network_file = g_file_new_for_uri ("network:///");
|
|
|
|
g_cancellable_cancel (view->networks_fetching_cancellable);
|
|
g_clear_object (&view->networks_fetching_cancellable);
|
|
view->networks_fetching_cancellable = g_cancellable_new ();
|
|
gtk_places_view_set_fetching_networks (view, TRUE);
|
|
update_network_state (view);
|
|
|
|
g_object_ref (view);
|
|
g_file_enumerate_children_async (network_file,
|
|
"standard::type,standard::target-uri,standard::name,standard::display-name,standard::icon",
|
|
G_FILE_QUERY_INFO_NONE,
|
|
G_PRIORITY_DEFAULT,
|
|
view->networks_fetching_cancellable,
|
|
network_enumeration_finished,
|
|
view);
|
|
|
|
g_clear_object (&network_file);
|
|
}
|
|
|
|
static void
|
|
update_places (GtkPlacesView *view)
|
|
{
|
|
GList *mounts;
|
|
GList *volumes;
|
|
GList *drives;
|
|
GList *l;
|
|
GIcon *icon;
|
|
GFile *file;
|
|
GtkWidget *child;
|
|
|
|
/* Clear all previously added items */
|
|
while ((child = gtk_widget_get_first_child (GTK_WIDGET (view->listbox))))
|
|
gtk_list_box_remove (GTK_LIST_BOX (view->listbox), child);
|
|
|
|
view->network_placeholder = NULL;
|
|
/* Inform clients that we started loading */
|
|
gtk_places_view_set_loading (view, TRUE);
|
|
|
|
/* Add "Computer" row */
|
|
file = g_file_new_for_path ("/");
|
|
icon = g_themed_icon_new_with_default_fallbacks ("drive-harddisk");
|
|
|
|
add_file (view, file, icon, _("Computer"), "/", FALSE);
|
|
|
|
g_clear_object (&file);
|
|
g_clear_object (&icon);
|
|
|
|
/* Add currently connected drives */
|
|
drives = g_volume_monitor_get_connected_drives (view->volume_monitor);
|
|
|
|
for (l = drives; l != NULL; l = l->next)
|
|
add_drive (view, l->data);
|
|
|
|
g_list_free_full (drives, g_object_unref);
|
|
|
|
/*
|
|
* Since all volumes with an associated GDrive were already added with
|
|
* add_drive before, add all volumes that aren't associated with a
|
|
* drive.
|
|
*/
|
|
volumes = g_volume_monitor_get_volumes (view->volume_monitor);
|
|
|
|
for (l = volumes; l != NULL; l = l->next)
|
|
{
|
|
GVolume *volume;
|
|
GDrive *drive;
|
|
|
|
volume = l->data;
|
|
drive = g_volume_get_drive (volume);
|
|
|
|
if (drive)
|
|
{
|
|
g_object_unref (drive);
|
|
continue;
|
|
}
|
|
|
|
add_volume (view, volume);
|
|
}
|
|
|
|
g_list_free_full (volumes, g_object_unref);
|
|
|
|
/*
|
|
* Now that all necessary drives and volumes were already added, add mounts
|
|
* that have no volume, such as /etc/mtab mounts, ftp, sftp, etc.
|
|
*/
|
|
mounts = g_volume_monitor_get_mounts (view->volume_monitor);
|
|
|
|
for (l = mounts; l != NULL; l = l->next)
|
|
{
|
|
GMount *mount;
|
|
GVolume *volume;
|
|
|
|
mount = l->data;
|
|
volume = g_mount_get_volume (mount);
|
|
|
|
if (volume)
|
|
{
|
|
g_object_unref (volume);
|
|
continue;
|
|
}
|
|
|
|
add_mount (view, mount);
|
|
}
|
|
|
|
g_list_free_full (mounts, g_object_unref);
|
|
|
|
/* load saved servers */
|
|
populate_servers (view);
|
|
|
|
/* fetch networks and add them asynchronously */
|
|
fetch_networks (view);
|
|
|
|
update_view_mode (view);
|
|
/* Check whether we still are in a loading state */
|
|
update_loading (view);
|
|
}
|
|
|
|
static void
|
|
server_mount_ready_cb (GObject *source_file,
|
|
GAsyncResult *res,
|
|
gpointer user_data)
|
|
{
|
|
GtkPlacesView *view = GTK_PLACES_VIEW (user_data);
|
|
gboolean should_show;
|
|
GError *error;
|
|
GFile *location;
|
|
|
|
location = G_FILE (source_file);
|
|
should_show = TRUE;
|
|
error = NULL;
|
|
|
|
g_file_mount_enclosing_volume_finish (location, res, &error);
|
|
if (error)
|
|
{
|
|
should_show = FALSE;
|
|
|
|
if (error->code == G_IO_ERROR_ALREADY_MOUNTED)
|
|
{
|
|
/*
|
|
* Already mounted volume is not a critical error
|
|
* and we can still continue with the operation.
|
|
*/
|
|
should_show = TRUE;
|
|
}
|
|
else if (error->domain != G_IO_ERROR ||
|
|
(error->code != G_IO_ERROR_CANCELLED &&
|
|
error->code != G_IO_ERROR_FAILED_HANDLED))
|
|
{
|
|
/* if it wasn't cancelled show a dialog */
|
|
emit_show_error_message (view, _("Unable to access location"), error->message);
|
|
}
|
|
|
|
/* The operation got cancelled by the user and or the error
|
|
has been handled already. */
|
|
g_clear_error (&error);
|
|
}
|
|
|
|
if (view->destroyed) {
|
|
g_object_unref (view);
|
|
return;
|
|
}
|
|
|
|
view->should_pulse_entry = FALSE;
|
|
|
|
/* Restore from Cancel to Connect */
|
|
gtk_button_set_label (GTK_BUTTON (view->connect_button), _("Con_nect"));
|
|
gtk_widget_set_sensitive (view->address_entry, TRUE);
|
|
view->connecting_to_server = FALSE;
|
|
|
|
if (should_show)
|
|
{
|
|
server_list_add_server (view, location);
|
|
|
|
/*
|
|
* Only clear the entry if it successfully connects to the server.
|
|
* Otherwise, the user would lost the typed address even if it fails
|
|
* to connect.
|
|
*/
|
|
gtk_editable_set_text (GTK_EDITABLE (view->address_entry), "");
|
|
|
|
if (view->should_open_location)
|
|
{
|
|
GMount *mount;
|
|
GFile *root;
|
|
|
|
mount = g_file_find_enclosing_mount (location, view->cancellable, NULL);
|
|
if (mount)
|
|
{
|
|
root = g_mount_get_default_location (mount);
|
|
|
|
emit_open_location (view, root, view->open_flags);
|
|
|
|
g_object_unref (root);
|
|
g_object_unref (mount);
|
|
}
|
|
}
|
|
}
|
|
|
|
update_places (view);
|
|
g_object_unref (view);
|
|
}
|
|
|
|
static void
|
|
volume_mount_ready_cb (GObject *source_volume,
|
|
GAsyncResult *res,
|
|
gpointer user_data)
|
|
{
|
|
GtkPlacesView *view = GTK_PLACES_VIEW (user_data);
|
|
gboolean should_show;
|
|
GVolume *volume;
|
|
GError *error;
|
|
|
|
volume = G_VOLUME (source_volume);
|
|
should_show = TRUE;
|
|
error = NULL;
|
|
|
|
g_volume_mount_finish (volume, res, &error);
|
|
|
|
if (error)
|
|
{
|
|
should_show = FALSE;
|
|
|
|
if (error->code == G_IO_ERROR_ALREADY_MOUNTED)
|
|
{
|
|
/*
|
|
* If the volume was already mounted, it's not a hard error
|
|
* and we can still continue with the operation.
|
|
*/
|
|
should_show = TRUE;
|
|
}
|
|
else if (error->domain != G_IO_ERROR ||
|
|
(error->code != G_IO_ERROR_CANCELLED &&
|
|
error->code != G_IO_ERROR_FAILED_HANDLED))
|
|
{
|
|
/* if it wasn't cancelled show a dialog */
|
|
emit_show_error_message (GTK_PLACES_VIEW (user_data), _("Unable to access location"), error->message);
|
|
should_show = FALSE;
|
|
}
|
|
|
|
/* The operation got cancelled by the user and or the error
|
|
has been handled already. */
|
|
g_clear_error (&error);
|
|
}
|
|
|
|
if (view->destroyed)
|
|
{
|
|
g_object_unref(view);
|
|
return;
|
|
}
|
|
|
|
view->mounting_volume = FALSE;
|
|
update_loading (view);
|
|
|
|
if (should_show)
|
|
{
|
|
GMount *mount;
|
|
GFile *root;
|
|
|
|
mount = g_volume_get_mount (volume);
|
|
root = g_mount_get_default_location (mount);
|
|
|
|
if (view->should_open_location)
|
|
emit_open_location (GTK_PLACES_VIEW (user_data), root, view->open_flags);
|
|
|
|
g_object_unref (mount);
|
|
g_object_unref (root);
|
|
}
|
|
|
|
update_places (view);
|
|
g_object_unref (view);
|
|
}
|
|
|
|
static void
|
|
unmount_ready_cb (GObject *source_mount,
|
|
GAsyncResult *res,
|
|
gpointer user_data)
|
|
{
|
|
GtkPlacesView *view;
|
|
GMount *mount;
|
|
GError *error;
|
|
|
|
view = GTK_PLACES_VIEW (user_data);
|
|
mount = G_MOUNT (source_mount);
|
|
error = NULL;
|
|
|
|
g_mount_unmount_with_operation_finish (mount, res, &error);
|
|
|
|
if (error)
|
|
{
|
|
if (error->domain != G_IO_ERROR ||
|
|
(error->code != G_IO_ERROR_CANCELLED &&
|
|
error->code != G_IO_ERROR_FAILED_HANDLED))
|
|
{
|
|
/* if it wasn't cancelled show a dialog */
|
|
emit_show_error_message (view, _("Unable to unmount volume"), error->message);
|
|
}
|
|
|
|
g_clear_error (&error);
|
|
}
|
|
|
|
if (view->destroyed) {
|
|
g_object_unref (view);
|
|
return;
|
|
}
|
|
|
|
view->unmounting_mount = FALSE;
|
|
update_loading (view);
|
|
|
|
g_object_unref (view);
|
|
}
|
|
|
|
static gboolean
|
|
pulse_entry_cb (gpointer user_data)
|
|
{
|
|
GtkPlacesView *view = GTK_PLACES_VIEW (user_data);
|
|
|
|
if (view->destroyed)
|
|
{
|
|
view->entry_pulse_timeout_id = 0;
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
else if (view->should_pulse_entry)
|
|
{
|
|
gtk_entry_progress_pulse (GTK_ENTRY (view->address_entry));
|
|
|
|
return G_SOURCE_CONTINUE;
|
|
}
|
|
else
|
|
{
|
|
gtk_entry_set_progress_pulse_step (GTK_ENTRY (view->address_entry), 0.0);
|
|
gtk_entry_set_progress_fraction (GTK_ENTRY (view->address_entry), 0.0);
|
|
view->entry_pulse_timeout_id = 0;
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
}
|
|
|
|
static void
|
|
unmount_mount (GtkPlacesView *view,
|
|
GMount *mount)
|
|
{
|
|
GMountOperation *operation;
|
|
GtkWidget *toplevel;
|
|
|
|
toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (view)));
|
|
|
|
g_cancellable_cancel (view->cancellable);
|
|
g_clear_object (&view->cancellable);
|
|
view->cancellable = g_cancellable_new ();
|
|
|
|
view->unmounting_mount = TRUE;
|
|
update_loading (view);
|
|
|
|
g_object_ref (view);
|
|
|
|
operation = gtk_mount_operation_new (GTK_WINDOW (toplevel));
|
|
g_mount_unmount_with_operation (mount,
|
|
0,
|
|
operation,
|
|
view->cancellable,
|
|
unmount_ready_cb,
|
|
view);
|
|
g_object_unref (operation);
|
|
}
|
|
|
|
static void
|
|
mount_server (GtkPlacesView *view,
|
|
GFile *location)
|
|
{
|
|
GMountOperation *operation;
|
|
GtkWidget *toplevel;
|
|
|
|
g_cancellable_cancel (view->cancellable);
|
|
g_clear_object (&view->cancellable);
|
|
/* User cliked when the operation was ongoing, so wanted to cancel it */
|
|
if (view->connecting_to_server)
|
|
return;
|
|
|
|
view->cancellable = g_cancellable_new ();
|
|
toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (view)));
|
|
operation = gtk_mount_operation_new (GTK_WINDOW (toplevel));
|
|
|
|
view->should_pulse_entry = TRUE;
|
|
gtk_entry_set_progress_pulse_step (GTK_ENTRY (view->address_entry), 0.1);
|
|
/* Allow to cancel the operation */
|
|
gtk_button_set_label (GTK_BUTTON (view->connect_button), _("Cance_l"));
|
|
gtk_widget_set_sensitive (view->address_entry, FALSE);
|
|
view->connecting_to_server = TRUE;
|
|
update_loading (view);
|
|
|
|
if (view->entry_pulse_timeout_id == 0)
|
|
view->entry_pulse_timeout_id = g_timeout_add (100, (GSourceFunc) pulse_entry_cb, view);
|
|
|
|
g_mount_operation_set_password_save (operation, G_PASSWORD_SAVE_FOR_SESSION);
|
|
|
|
/* make sure we keep the view around for as long as we are running */
|
|
g_object_ref (view);
|
|
|
|
g_file_mount_enclosing_volume (location,
|
|
0,
|
|
operation,
|
|
view->cancellable,
|
|
server_mount_ready_cb,
|
|
view);
|
|
|
|
/* unref operation here - g_file_mount_enclosing_volume() does ref for itself */
|
|
g_object_unref (operation);
|
|
}
|
|
|
|
static void
|
|
mount_volume (GtkPlacesView *view,
|
|
GVolume *volume)
|
|
{
|
|
GMountOperation *operation;
|
|
GtkWidget *toplevel;
|
|
|
|
toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (view)));
|
|
operation = gtk_mount_operation_new (GTK_WINDOW (toplevel));
|
|
|
|
g_cancellable_cancel (view->cancellable);
|
|
g_clear_object (&view->cancellable);
|
|
view->cancellable = g_cancellable_new ();
|
|
|
|
view->mounting_volume = TRUE;
|
|
update_loading (view);
|
|
|
|
g_mount_operation_set_password_save (operation, G_PASSWORD_SAVE_FOR_SESSION);
|
|
|
|
/* make sure we keep the view around for as long as we are running */
|
|
g_object_ref (view);
|
|
|
|
g_volume_mount (volume,
|
|
0,
|
|
operation,
|
|
view->cancellable,
|
|
volume_mount_ready_cb,
|
|
view);
|
|
|
|
/* unref operation here - g_file_mount_enclosing_volume() does ref for itself */
|
|
g_object_unref (operation);
|
|
}
|
|
|
|
static void
|
|
open_cb (GtkWidget *widget,
|
|
const char *action_name,
|
|
GVariant *parameter)
|
|
{
|
|
GtkPlacesView *view = GTK_PLACES_VIEW (widget);
|
|
GtkPlacesOpenFlags flags = GTK_PLACES_OPEN_NORMAL;
|
|
|
|
if (view->row_for_action == NULL)
|
|
return;
|
|
|
|
if (strcmp (action_name, "location.open") == 0)
|
|
flags = GTK_PLACES_OPEN_NORMAL;
|
|
else if (strcmp (action_name, "location.open-tab") == 0)
|
|
flags = GTK_PLACES_OPEN_NEW_TAB;
|
|
else if (strcmp (action_name, "location.open-window") == 0)
|
|
flags = GTK_PLACES_OPEN_NEW_WINDOW;
|
|
|
|
activate_row (view, view->row_for_action, flags);
|
|
}
|
|
|
|
static void
|
|
mount_cb (GtkWidget *widget,
|
|
const char *action_name,
|
|
GVariant *parameter)
|
|
{
|
|
GtkPlacesView *view = GTK_PLACES_VIEW (widget);
|
|
GVolume *volume;
|
|
|
|
if (view->row_for_action == NULL)
|
|
return;
|
|
|
|
volume = gtk_places_view_row_get_volume (view->row_for_action);
|
|
|
|
/*
|
|
* When the mount item is activated, it's expected that
|
|
* the volume only gets mounted, without opening it after
|
|
* the operation is complete.
|
|
*/
|
|
view->should_open_location = FALSE;
|
|
|
|
gtk_places_view_row_set_busy (view->row_for_action, TRUE);
|
|
mount_volume (view, volume);
|
|
}
|
|
|
|
static void
|
|
unmount_cb (GtkWidget *widget,
|
|
const char *action_name,
|
|
GVariant *parameter)
|
|
{
|
|
GtkPlacesView *view = GTK_PLACES_VIEW (widget);
|
|
GMount *mount;
|
|
|
|
if (view->row_for_action == NULL)
|
|
return;
|
|
|
|
mount = gtk_places_view_row_get_mount (view->row_for_action);
|
|
|
|
gtk_places_view_row_set_busy (view->row_for_action, TRUE);
|
|
|
|
unmount_mount (view, mount);
|
|
}
|
|
|
|
static void
|
|
attach_protocol_row_to_grid (GtkGrid *grid,
|
|
const char *protocol_name,
|
|
const char *protocol_prefix)
|
|
{
|
|
GtkWidget *name_label;
|
|
GtkWidget *prefix_label;
|
|
|
|
name_label = gtk_label_new (protocol_name);
|
|
gtk_widget_set_halign (name_label, GTK_ALIGN_START);
|
|
gtk_grid_attach_next_to (grid, name_label, NULL, GTK_POS_BOTTOM, 1, 1);
|
|
|
|
prefix_label = gtk_label_new (protocol_prefix);
|
|
gtk_widget_set_halign (prefix_label, GTK_ALIGN_START);
|
|
gtk_grid_attach_next_to (grid, prefix_label, name_label, GTK_POS_RIGHT, 1, 1);
|
|
}
|
|
|
|
static void
|
|
populate_available_protocols_grid (GtkGrid *grid)
|
|
{
|
|
const char * const *supported_protocols;
|
|
gboolean has_any = FALSE;
|
|
|
|
supported_protocols = g_vfs_get_supported_uri_schemes (g_vfs_get_default ());
|
|
|
|
if (g_strv_contains (supported_protocols, "afp"))
|
|
{
|
|
attach_protocol_row_to_grid (grid, _("AppleTalk"), "afp://");
|
|
has_any = TRUE;
|
|
}
|
|
|
|
if (g_strv_contains (supported_protocols, "ftp"))
|
|
{
|
|
attach_protocol_row_to_grid (grid, _("File Transfer Protocol"),
|
|
/* Translators: do not translate ftp:// and ftps:// */
|
|
_("ftp:// or ftps://"));
|
|
has_any = TRUE;
|
|
}
|
|
|
|
if (g_strv_contains (supported_protocols, "nfs"))
|
|
{
|
|
attach_protocol_row_to_grid (grid, _("Network File System"), "nfs://");
|
|
has_any = TRUE;
|
|
}
|
|
|
|
if (g_strv_contains (supported_protocols, "smb"))
|
|
{
|
|
attach_protocol_row_to_grid (grid, _("Samba"), "smb://");
|
|
has_any = TRUE;
|
|
}
|
|
|
|
if (g_strv_contains (supported_protocols, "ssh"))
|
|
{
|
|
attach_protocol_row_to_grid (grid, _("SSH File Transfer Protocol"),
|
|
/* Translators: do not translate sftp:// and ssh:// */
|
|
_("sftp:// or ssh://"));
|
|
has_any = TRUE;
|
|
}
|
|
|
|
if (g_strv_contains (supported_protocols, "dav"))
|
|
{
|
|
attach_protocol_row_to_grid (grid, _("WebDAV"),
|
|
/* Translators: do not translate dav:// and davs:// */
|
|
_("dav:// or davs://"));
|
|
has_any = TRUE;
|
|
}
|
|
|
|
if (!has_any)
|
|
gtk_widget_hide (GTK_WIDGET (grid));
|
|
}
|
|
|
|
static GMenuModel *
|
|
get_menu_model (void)
|
|
{
|
|
GMenu *menu;
|
|
GMenu *section;
|
|
GMenuItem *item;
|
|
|
|
menu = g_menu_new ();
|
|
section = g_menu_new ();
|
|
item = g_menu_item_new (_("_Open"), "location.open");
|
|
g_menu_append_item (section, item);
|
|
g_object_unref (item);
|
|
|
|
item = g_menu_item_new (_("Open in New _Tab"), "location.open-tab");
|
|
g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled");
|
|
g_menu_append_item (section, item);
|
|
g_object_unref (item);
|
|
|
|
item = g_menu_item_new (_("Open in New _Window"), "location.open-window");
|
|
g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled");
|
|
g_menu_append_item (section, item);
|
|
g_object_unref (item);
|
|
|
|
g_menu_append_section (menu, NULL, G_MENU_MODEL (section));
|
|
g_object_unref (section);
|
|
|
|
section = g_menu_new ();
|
|
item = g_menu_item_new (_("_Disconnect"), "location.disconnect");
|
|
g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled");
|
|
g_menu_append_item (section, item);
|
|
g_object_unref (item);
|
|
|
|
item = g_menu_item_new (_("_Unmount"), "location.unmount");
|
|
g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled");
|
|
g_menu_append_item (section, item);
|
|
g_object_unref (item);
|
|
|
|
|
|
item = g_menu_item_new (_("_Connect"), "location.connect");
|
|
g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled");
|
|
g_menu_append_item (section, item);
|
|
g_object_unref (item);
|
|
|
|
item = g_menu_item_new (_("_Mount"), "location.mount");
|
|
g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled");
|
|
g_menu_append_item (section, item);
|
|
g_object_unref (item);
|
|
|
|
g_menu_append_section (menu, NULL, G_MENU_MODEL (section));
|
|
g_object_unref (section);
|
|
|
|
return G_MENU_MODEL (menu);
|
|
}
|
|
|
|
static gboolean
|
|
on_row_popup_menu (GtkWidget *widget,
|
|
GVariant *args,
|
|
gpointer user_data)
|
|
{
|
|
GtkPlacesViewRow *row = GTK_PLACES_VIEW_ROW (widget);
|
|
GtkPlacesView *view;
|
|
GMount *mount;
|
|
GFile *file;
|
|
gboolean is_network;
|
|
|
|
view = GTK_PLACES_VIEW (gtk_widget_get_ancestor (GTK_WIDGET (row), GTK_TYPE_PLACES_VIEW));
|
|
|
|
mount = gtk_places_view_row_get_mount (row);
|
|
file = gtk_places_view_row_get_file (row);
|
|
is_network = gtk_places_view_row_get_is_network (row);
|
|
|
|
gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.disconnect",
|
|
!file && mount && is_network);
|
|
gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.unmount",
|
|
!file && mount && !is_network);
|
|
gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.connect",
|
|
!file && !mount && is_network);
|
|
gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.mount",
|
|
!file && !mount && !is_network);
|
|
|
|
if (!view->popup_menu)
|
|
{
|
|
GMenuModel *model = get_menu_model ();
|
|
|
|
view->popup_menu = gtk_popover_menu_new_from_model (model);
|
|
gtk_popover_set_position (GTK_POPOVER (view->popup_menu), GTK_POS_BOTTOM);
|
|
|
|
gtk_popover_set_has_arrow (GTK_POPOVER (view->popup_menu), FALSE);
|
|
gtk_widget_set_halign (view->popup_menu, GTK_ALIGN_CENTER);
|
|
|
|
g_object_unref (model);
|
|
}
|
|
|
|
if (view->row_for_action)
|
|
g_object_set_data (G_OBJECT (view->row_for_action), "menu", NULL);
|
|
|
|
g_object_ref (view->popup_menu);
|
|
gtk_widget_unparent (view->popup_menu);
|
|
gtk_widget_set_parent (view->popup_menu, GTK_WIDGET (row));
|
|
g_object_unref (view->popup_menu);
|
|
|
|
view->row_for_action = row;
|
|
if (view->row_for_action)
|
|
g_object_set_data (G_OBJECT (view->row_for_action), "menu", view->popup_menu);
|
|
|
|
gtk_popover_popup (GTK_POPOVER (view->popup_menu));
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
click_cb (GtkGesture *gesture,
|
|
int n_press,
|
|
double x,
|
|
double y,
|
|
gpointer user_data)
|
|
{
|
|
on_row_popup_menu (GTK_WIDGET (user_data), NULL, NULL);
|
|
}
|
|
|
|
static gboolean
|
|
on_key_press_event (GtkEventController *controller,
|
|
guint keyval,
|
|
guint keycode,
|
|
GdkModifierType state,
|
|
GtkPlacesView *view)
|
|
{
|
|
GdkModifierType modifiers;
|
|
|
|
modifiers = gtk_accelerator_get_default_mod_mask ();
|
|
|
|
if (keyval == GDK_KEY_Return ||
|
|
keyval == GDK_KEY_KP_Enter ||
|
|
keyval == GDK_KEY_ISO_Enter ||
|
|
keyval == GDK_KEY_space)
|
|
{
|
|
GtkWidget *focus_widget;
|
|
GtkWindow *toplevel;
|
|
|
|
view->current_open_flags = GTK_PLACES_OPEN_NORMAL;
|
|
toplevel = get_toplevel (GTK_WIDGET (view));
|
|
|
|
if (!toplevel)
|
|
return FALSE;
|
|
|
|
focus_widget = gtk_root_get_focus (GTK_ROOT (toplevel));
|
|
|
|
if (!GTK_IS_PLACES_VIEW_ROW (focus_widget))
|
|
return FALSE;
|
|
|
|
if ((state & modifiers) == GDK_SHIFT_MASK)
|
|
view->current_open_flags = GTK_PLACES_OPEN_NEW_TAB;
|
|
else if ((state & modifiers) == GDK_CONTROL_MASK)
|
|
view->current_open_flags = GTK_PLACES_OPEN_NEW_WINDOW;
|
|
|
|
activate_row (view, GTK_PLACES_VIEW_ROW (focus_widget), view->current_open_flags);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
on_middle_click_row_event (GtkGestureClick *gesture,
|
|
guint n_press,
|
|
double x,
|
|
double y,
|
|
GtkPlacesView *view)
|
|
{
|
|
GtkListBoxRow *row;
|
|
|
|
if (n_press != 1)
|
|
return;
|
|
|
|
row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (view->listbox), y);
|
|
if (row != NULL && gtk_widget_is_sensitive (GTK_WIDGET (row)))
|
|
activate_row (view, GTK_PLACES_VIEW_ROW (row), GTK_PLACES_OPEN_NEW_TAB);
|
|
}
|
|
|
|
|
|
static void
|
|
on_eject_button_clicked (GtkWidget *widget,
|
|
GtkPlacesViewRow *row)
|
|
{
|
|
if (row)
|
|
{
|
|
GtkWidget *view = gtk_widget_get_ancestor (GTK_WIDGET (row), GTK_TYPE_PLACES_VIEW);
|
|
|
|
unmount_mount (GTK_PLACES_VIEW (view), gtk_places_view_row_get_mount (row));
|
|
}
|
|
}
|
|
|
|
static void
|
|
on_connect_button_clicked (GtkPlacesView *view)
|
|
{
|
|
const char *uri;
|
|
GFile *file;
|
|
|
|
file = NULL;
|
|
|
|
/*
|
|
* Since the 'Connect' button is updated whenever the typed
|
|
* address changes, it is sufficient to check if it's sensitive
|
|
* or not, in order to determine if the given address is valid.
|
|
*/
|
|
if (!gtk_widget_get_sensitive (view->connect_button))
|
|
return;
|
|
|
|
uri = gtk_editable_get_text (GTK_EDITABLE (view->address_entry));
|
|
|
|
if (uri != NULL && uri[0] != '\0')
|
|
file = g_file_new_for_commandline_arg (uri);
|
|
|
|
if (file)
|
|
{
|
|
view->should_open_location = TRUE;
|
|
|
|
mount_server (view, file);
|
|
}
|
|
else
|
|
{
|
|
emit_show_error_message (view, _("Unable to get remote server location"), NULL);
|
|
}
|
|
}
|
|
|
|
static void
|
|
on_address_entry_text_changed (GtkPlacesView *view)
|
|
{
|
|
const char * const *supported_protocols;
|
|
char *address, *scheme;
|
|
gboolean supported;
|
|
|
|
supported = FALSE;
|
|
supported_protocols = g_vfs_get_supported_uri_schemes (g_vfs_get_default ());
|
|
address = g_strdup (gtk_editable_get_text (GTK_EDITABLE (view->address_entry)));
|
|
scheme = g_uri_parse_scheme (address);
|
|
|
|
if (!supported_protocols)
|
|
goto out;
|
|
|
|
if (!scheme)
|
|
goto out;
|
|
|
|
supported = g_strv_contains (supported_protocols, scheme) &&
|
|
!g_strv_contains (unsupported_protocols, scheme);
|
|
|
|
out:
|
|
gtk_widget_set_sensitive (view->connect_button, supported);
|
|
if (scheme && !supported)
|
|
gtk_widget_add_css_class (view->address_entry, "error");
|
|
else
|
|
gtk_widget_remove_css_class (view->address_entry, "error");
|
|
|
|
g_free (address);
|
|
g_free (scheme);
|
|
}
|
|
|
|
static void
|
|
on_address_entry_show_help_pressed (GtkPlacesView *view,
|
|
GtkEntryIconPosition icon_pos,
|
|
GtkEntry *entry)
|
|
{
|
|
GdkRectangle rect;
|
|
double x, y;
|
|
|
|
/* Setup the auxiliary popover's rectangle */
|
|
gtk_entry_get_icon_area (GTK_ENTRY (view->address_entry),
|
|
GTK_ENTRY_ICON_SECONDARY,
|
|
&rect);
|
|
gtk_widget_translate_coordinates (view->address_entry, GTK_WIDGET (view),
|
|
rect.x, rect.y, &x, &y);
|
|
|
|
rect.x = x;
|
|
rect.y = y;
|
|
gtk_popover_set_pointing_to (GTK_POPOVER (view->server_adresses_popover), &rect);
|
|
gtk_widget_set_visible (view->server_adresses_popover, TRUE);
|
|
}
|
|
|
|
static void
|
|
on_recent_servers_listbox_row_activated (GtkPlacesView *view,
|
|
GtkPlacesViewRow *row,
|
|
GtkWidget *listbox)
|
|
{
|
|
char *uri;
|
|
|
|
uri = g_object_get_data (G_OBJECT (row), "uri");
|
|
|
|
gtk_editable_set_text (GTK_EDITABLE (view->address_entry), uri);
|
|
|
|
gtk_widget_hide (view->recent_servers_popover);
|
|
}
|
|
|
|
static void
|
|
on_listbox_row_activated (GtkPlacesView *view,
|
|
GtkPlacesViewRow *row,
|
|
GtkWidget *listbox)
|
|
{
|
|
activate_row (view, row, view->current_open_flags);
|
|
}
|
|
|
|
static gboolean
|
|
listbox_filter_func (GtkListBoxRow *row,
|
|
gpointer user_data)
|
|
{
|
|
GtkPlacesView *view = GTK_PLACES_VIEW (user_data);
|
|
gboolean is_placeholder;
|
|
gboolean retval;
|
|
gboolean searching;
|
|
char *name;
|
|
char *path;
|
|
|
|
retval = FALSE;
|
|
searching = view->search_query && view->search_query[0] != '\0';
|
|
|
|
is_placeholder = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "is-placeholder"));
|
|
|
|
if (is_placeholder && searching)
|
|
return FALSE;
|
|
|
|
if (!searching)
|
|
return TRUE;
|
|
|
|
g_object_get (row,
|
|
"name", &name,
|
|
"path", &path,
|
|
NULL);
|
|
|
|
if (name)
|
|
{
|
|
char *lowercase_name = g_utf8_strdown (name, -1);
|
|
|
|
retval |= strstr (lowercase_name, view->search_query) != NULL;
|
|
|
|
g_free (lowercase_name);
|
|
}
|
|
|
|
if (path)
|
|
{
|
|
char *lowercase_path = g_utf8_strdown (path, -1);
|
|
|
|
retval |= strstr (lowercase_path, view->search_query) != NULL;
|
|
|
|
g_free (lowercase_path);
|
|
}
|
|
|
|
g_free (name);
|
|
g_free (path);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static void
|
|
listbox_header_func (GtkListBoxRow *row,
|
|
GtkListBoxRow *before,
|
|
gpointer user_data)
|
|
{
|
|
gboolean row_is_network;
|
|
char *text;
|
|
|
|
text = NULL;
|
|
row_is_network = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "is-network"));
|
|
|
|
if (!before)
|
|
{
|
|
text = g_strdup_printf ("<b>%s</b>", row_is_network ? _("Networks") : _("On This Computer"));
|
|
}
|
|
else
|
|
{
|
|
gboolean before_is_network;
|
|
|
|
before_is_network = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (before), "is-network"));
|
|
|
|
if (before_is_network != row_is_network)
|
|
text = g_strdup_printf ("<b>%s</b>", row_is_network ? _("Networks") : _("On This Computer"));
|
|
}
|
|
|
|
if (text)
|
|
{
|
|
GtkWidget *header;
|
|
GtkWidget *label;
|
|
GtkWidget *separator;
|
|
|
|
header = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
|
|
gtk_widget_set_margin_top (header, 6);
|
|
|
|
separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
|
|
|
|
label = g_object_new (GTK_TYPE_LABEL,
|
|
"use_markup", TRUE,
|
|
"margin-start", 12,
|
|
"label", text,
|
|
"xalign", 0.0f,
|
|
NULL);
|
|
if (row_is_network)
|
|
{
|
|
GtkWidget *header_name;
|
|
GtkWidget *network_header_spinner;
|
|
|
|
gtk_widget_set_margin_end (label, 6);
|
|
|
|
header_name = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
|
network_header_spinner = gtk_spinner_new ();
|
|
gtk_widget_set_margin_end (network_header_spinner, 12);
|
|
g_object_bind_property (GTK_PLACES_VIEW (user_data),
|
|
"fetching-networks",
|
|
network_header_spinner,
|
|
"spinning",
|
|
G_BINDING_SYNC_CREATE);
|
|
|
|
gtk_box_append (GTK_BOX (header_name), label);
|
|
gtk_box_append (GTK_BOX (header_name), network_header_spinner);
|
|
gtk_box_append (GTK_BOX (header), header_name);
|
|
}
|
|
else
|
|
{
|
|
gtk_widget_set_hexpand (label, TRUE);
|
|
gtk_widget_set_margin_end (label, 12);
|
|
gtk_box_append (GTK_BOX (header), label);
|
|
}
|
|
|
|
gtk_box_append (GTK_BOX (header), separator);
|
|
|
|
gtk_list_box_row_set_header (row, header);
|
|
|
|
g_free (text);
|
|
}
|
|
else
|
|
{
|
|
gtk_list_box_row_set_header (row, NULL);
|
|
}
|
|
}
|
|
|
|
static int
|
|
listbox_sort_func (GtkListBoxRow *row1,
|
|
GtkListBoxRow *row2,
|
|
gpointer user_data)
|
|
{
|
|
gboolean row1_is_network;
|
|
gboolean row2_is_network;
|
|
char *path1;
|
|
char *path2;
|
|
gboolean *is_placeholder1;
|
|
gboolean *is_placeholder2;
|
|
int retval;
|
|
|
|
row1_is_network = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row1), "is-network"));
|
|
row2_is_network = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row2), "is-network"));
|
|
|
|
retval = row1_is_network - row2_is_network;
|
|
|
|
if (retval != 0)
|
|
return retval;
|
|
|
|
is_placeholder1 = g_object_get_data (G_OBJECT (row1), "is-placeholder");
|
|
is_placeholder2 = g_object_get_data (G_OBJECT (row2), "is-placeholder");
|
|
|
|
/* we can't have two placeholders for the same section */
|
|
g_assert (!(is_placeholder1 != NULL && is_placeholder2 != NULL));
|
|
|
|
if (is_placeholder1)
|
|
return -1;
|
|
if (is_placeholder2)
|
|
return 1;
|
|
|
|
g_object_get (row1, "path", &path1, NULL);
|
|
g_object_get (row2, "path", &path2, NULL);
|
|
|
|
retval = g_utf8_collate (path1, path2);
|
|
|
|
g_free (path1);
|
|
g_free (path2);
|
|
|
|
return retval;
|
|
}
|
|
|
|
static void
|
|
gtk_places_view_constructed (GObject *object)
|
|
{
|
|
GtkPlacesView *view = GTK_PLACES_VIEW (object);
|
|
|
|
G_OBJECT_CLASS (gtk_places_view_parent_class)->constructed (object);
|
|
|
|
gtk_list_box_set_sort_func (GTK_LIST_BOX (view->listbox),
|
|
listbox_sort_func,
|
|
object,
|
|
NULL);
|
|
gtk_list_box_set_filter_func (GTK_LIST_BOX (view->listbox),
|
|
listbox_filter_func,
|
|
object,
|
|
NULL);
|
|
gtk_list_box_set_header_func (GTK_LIST_BOX (view->listbox),
|
|
listbox_header_func,
|
|
object,
|
|
NULL);
|
|
|
|
/* load drives */
|
|
update_places (view);
|
|
|
|
g_signal_connect_swapped (view->volume_monitor,
|
|
"mount-added",
|
|
G_CALLBACK (update_places),
|
|
object);
|
|
g_signal_connect_swapped (view->volume_monitor,
|
|
"mount-changed",
|
|
G_CALLBACK (update_places),
|
|
object);
|
|
g_signal_connect_swapped (view->volume_monitor,
|
|
"mount-removed",
|
|
G_CALLBACK (update_places),
|
|
object);
|
|
g_signal_connect_swapped (view->volume_monitor,
|
|
"volume-added",
|
|
G_CALLBACK (update_places),
|
|
object);
|
|
g_signal_connect_swapped (view->volume_monitor,
|
|
"volume-changed",
|
|
G_CALLBACK (update_places),
|
|
object);
|
|
g_signal_connect_swapped (view->volume_monitor,
|
|
"volume-removed",
|
|
G_CALLBACK (update_places),
|
|
object);
|
|
}
|
|
|
|
static void
|
|
gtk_places_view_map (GtkWidget *widget)
|
|
{
|
|
GtkPlacesView *view = GTK_PLACES_VIEW (widget);
|
|
|
|
gtk_editable_set_text (GTK_EDITABLE (view->address_entry), "");
|
|
|
|
GTK_WIDGET_CLASS (gtk_places_view_parent_class)->map (widget);
|
|
}
|
|
|
|
static void
|
|
gtk_places_view_class_init (GtkPlacesViewClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
|
|
|
|
object_class->finalize = gtk_places_view_finalize;
|
|
object_class->dispose = gtk_places_view_dispose;
|
|
object_class->constructed = gtk_places_view_constructed;
|
|
object_class->get_property = gtk_places_view_get_property;
|
|
object_class->set_property = gtk_places_view_set_property;
|
|
|
|
widget_class->map = gtk_places_view_map;
|
|
|
|
/*
|
|
* GtkPlacesView::open-location:
|
|
* @view: the object which received the signal.
|
|
* @location: (type Gio.File): #GFile to which the caller should switch.
|
|
* @open_flags: a single value from #GtkPlacesOpenFlags specifying how the @location
|
|
* should be opened.
|
|
*
|
|
* The places view emits this signal when the user selects a location
|
|
* in it. The calling application should display the contents of that
|
|
* location; for example, a file manager should show a list of files in
|
|
* the specified location.
|
|
*/
|
|
places_view_signals [OPEN_LOCATION] =
|
|
g_signal_new (I_("open-location"),
|
|
G_OBJECT_CLASS_TYPE (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkPlacesViewClass, open_location),
|
|
NULL, NULL,
|
|
_gtk_marshal_VOID__OBJECT_FLAGS,
|
|
G_TYPE_NONE, 2,
|
|
G_TYPE_OBJECT,
|
|
GTK_TYPE_PLACES_OPEN_FLAGS);
|
|
g_signal_set_va_marshaller (places_view_signals [OPEN_LOCATION],
|
|
G_TYPE_FROM_CLASS (object_class),
|
|
_gtk_marshal_VOID__OBJECT_FLAGSv);
|
|
|
|
/*
|
|
* GtkPlacesView::show-error-message:
|
|
* @view: the object which received the signal.
|
|
* @primary: primary message with a summary of the error to show.
|
|
* @secondary: secondary message with details of the error to show.
|
|
*
|
|
* The places view emits this signal when it needs the calling
|
|
* application to present an error message. Most of these messages
|
|
* refer to mounting or unmounting media, for example, when a drive
|
|
* cannot be started for some reason.
|
|
*/
|
|
places_view_signals [SHOW_ERROR_MESSAGE] =
|
|
g_signal_new (I_("show-error-message"),
|
|
G_OBJECT_CLASS_TYPE (object_class),
|
|
G_SIGNAL_RUN_FIRST,
|
|
G_STRUCT_OFFSET (GtkPlacesViewClass, show_error_message),
|
|
NULL, NULL,
|
|
_gtk_marshal_VOID__STRING_STRING,
|
|
G_TYPE_NONE, 2,
|
|
G_TYPE_STRING,
|
|
G_TYPE_STRING);
|
|
|
|
properties[PROP_LOADING] =
|
|
g_param_spec_boolean ("loading",
|
|
P_("Loading"),
|
|
P_("Whether the view is loading locations"),
|
|
FALSE,
|
|
GTK_PARAM_READABLE);
|
|
|
|
properties[PROP_FETCHING_NETWORKS] =
|
|
g_param_spec_boolean ("fetching-networks",
|
|
P_("Fetching networks"),
|
|
P_("Whether the view is fetching networks"),
|
|
FALSE,
|
|
GTK_PARAM_READABLE);
|
|
|
|
properties[PROP_OPEN_FLAGS] =
|
|
g_param_spec_flags ("open-flags",
|
|
P_("Open Flags"),
|
|
P_("Modes in which the calling application can open locations selected in the sidebar"),
|
|
GTK_TYPE_PLACES_OPEN_FLAGS,
|
|
GTK_PLACES_OPEN_NORMAL,
|
|
GTK_PARAM_READWRITE);
|
|
|
|
g_object_class_install_properties (object_class, LAST_PROP, properties);
|
|
|
|
/* Bind class to template */
|
|
gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/ui/gtkplacesview.ui");
|
|
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, actionbar);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, address_entry);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, address_entry_completion);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, completion_store);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, connect_button);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, listbox);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, recent_servers_listbox);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, recent_servers_popover);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, recent_servers_stack);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, stack);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, server_adresses_popover);
|
|
gtk_widget_class_bind_template_child (widget_class, GtkPlacesView, available_protocols_grid);
|
|
|
|
gtk_widget_class_bind_template_callback (widget_class, on_address_entry_text_changed);
|
|
gtk_widget_class_bind_template_callback (widget_class, on_address_entry_show_help_pressed);
|
|
gtk_widget_class_bind_template_callback (widget_class, on_connect_button_clicked);
|
|
gtk_widget_class_bind_template_callback (widget_class, on_listbox_row_activated);
|
|
gtk_widget_class_bind_template_callback (widget_class, on_recent_servers_listbox_row_activated);
|
|
|
|
/**
|
|
* GtkPlacesView|location.open:
|
|
*
|
|
* Opens the location in the current window.
|
|
*/
|
|
gtk_widget_class_install_action (widget_class, "location.open", NULL, open_cb);
|
|
|
|
/**
|
|
* GtkPlacesView|location.open-tab:
|
|
*
|
|
* Opens the location in a new tab.
|
|
*/
|
|
gtk_widget_class_install_action (widget_class, "location.open-tab", NULL, open_cb);
|
|
|
|
/**
|
|
* GtkPlacesView|location.open-window:
|
|
*
|
|
* Opens the location in a new window.
|
|
*/
|
|
gtk_widget_class_install_action (widget_class, "location.open-window", NULL, open_cb);
|
|
|
|
/**
|
|
* GtkPlacesView|location.mount:
|
|
*
|
|
* Mount the location.
|
|
*/
|
|
gtk_widget_class_install_action (widget_class, "location.mount", NULL, mount_cb);
|
|
|
|
/**
|
|
* GtkPlacesView|location.connect:
|
|
*
|
|
* Connect the location.
|
|
*/
|
|
gtk_widget_class_install_action (widget_class, "location.connect", NULL, mount_cb);
|
|
|
|
/**
|
|
* GtkPlacesView|location.unmount:
|
|
*
|
|
* Unmount the location.
|
|
*/
|
|
gtk_widget_class_install_action (widget_class, "location.unmount", NULL, unmount_cb);
|
|
|
|
/**
|
|
* GtkPlacesView|location.disconnect:
|
|
*
|
|
* Disconnect the location.
|
|
*/
|
|
gtk_widget_class_install_action (widget_class, "location.disconnect", NULL, unmount_cb);
|
|
|
|
gtk_widget_class_set_css_name (widget_class, I_("placesview"));
|
|
}
|
|
|
|
static void
|
|
gtk_places_view_init (GtkPlacesView *self)
|
|
{
|
|
GtkEventController *controller;
|
|
|
|
self->volume_monitor = g_volume_monitor_get ();
|
|
self->open_flags = GTK_PLACES_OPEN_NORMAL;
|
|
self->path_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
|
|
self->space_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
|
|
|
|
gtk_widget_action_set_enabled (GTK_WIDGET (self), "location.open-tab", FALSE);
|
|
gtk_widget_action_set_enabled (GTK_WIDGET (self), "location.open-window", FALSE);
|
|
|
|
gtk_widget_init_template (GTK_WIDGET (self));
|
|
|
|
gtk_widget_set_parent (self->server_adresses_popover, GTK_WIDGET (self));
|
|
controller = gtk_event_controller_key_new ();
|
|
g_signal_connect (controller, "key-pressed", G_CALLBACK (on_key_press_event), self);
|
|
gtk_widget_add_controller (GTK_WIDGET (self), controller);
|
|
|
|
/* We need an additional controller because GtkListBox only
|
|
* activates rows for GDK_BUTTON_PRIMARY clicks
|
|
*/
|
|
controller = (GtkEventController *) gtk_gesture_click_new ();
|
|
gtk_event_controller_set_propagation_phase (controller, GTK_PHASE_BUBBLE);
|
|
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), GDK_BUTTON_MIDDLE);
|
|
g_signal_connect (controller, "released",
|
|
G_CALLBACK (on_middle_click_row_event), self);
|
|
gtk_widget_add_controller (self->listbox, controller);
|
|
|
|
populate_available_protocols_grid (GTK_GRID (self->available_protocols_grid));
|
|
}
|
|
|
|
/*
|
|
* gtk_places_view_new:
|
|
*
|
|
* Creates a new #GtkPlacesView widget.
|
|
*
|
|
* The application should connect to at least the
|
|
* #GtkPlacesView::open-location signal to be notified
|
|
* when the user makes a selection in the view.
|
|
*
|
|
* Returns: a newly created #GtkPlacesView
|
|
*/
|
|
GtkWidget *
|
|
gtk_places_view_new (void)
|
|
{
|
|
return g_object_new (GTK_TYPE_PLACES_VIEW, NULL);
|
|
}
|
|
|
|
/*
|
|
* gtk_places_view_set_open_flags:
|
|
* @view: a #GtkPlacesView
|
|
* @flags: Bitmask of modes in which the calling application can open locations
|
|
*
|
|
* Sets the way in which the calling application can open new locations from
|
|
* the places view. For example, some applications only open locations
|
|
* “directly” into their main view, while others may support opening locations
|
|
* in a new notebook tab or a new window.
|
|
*
|
|
* This function is used to tell the places @view about the ways in which the
|
|
* application can open new locations, so that the view can display (or not)
|
|
* the “Open in new tab” and “Open in new window” menu items as appropriate.
|
|
*
|
|
* When the #GtkPlacesView::open-location signal is emitted, its flags
|
|
* argument will be set to one of the @flags that was passed in
|
|
* gtk_places_view_set_open_flags().
|
|
*
|
|
* Passing 0 for @flags will cause #GTK_PLACES_OPEN_NORMAL to always be sent
|
|
* to callbacks for the “open-location” signal.
|
|
*/
|
|
void
|
|
gtk_places_view_set_open_flags (GtkPlacesView *view,
|
|
GtkPlacesOpenFlags flags)
|
|
{
|
|
g_return_if_fail (GTK_IS_PLACES_VIEW (view));
|
|
|
|
if (view->open_flags == flags)
|
|
return;
|
|
|
|
view->open_flags = flags;
|
|
|
|
gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.open-tab",
|
|
(flags & GTK_PLACES_OPEN_NEW_TAB) != 0);
|
|
gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.open-window",
|
|
(flags & GTK_PLACES_OPEN_NEW_WINDOW) != 0);
|
|
|
|
g_object_notify_by_pspec (G_OBJECT (view), properties[PROP_OPEN_FLAGS]);
|
|
}
|
|
|
|
/*
|
|
* gtk_places_view_get_open_flags:
|
|
* @view: a #GtkPlacesSidebar
|
|
*
|
|
* Gets the open flags.
|
|
*
|
|
* Returns: the #GtkPlacesOpenFlags of @view
|
|
*/
|
|
GtkPlacesOpenFlags
|
|
gtk_places_view_get_open_flags (GtkPlacesView *view)
|
|
{
|
|
g_return_val_if_fail (GTK_IS_PLACES_VIEW (view), 0);
|
|
|
|
return view->open_flags;
|
|
}
|
|
|
|
/*
|
|
* gtk_places_view_get_search_query:
|
|
* @view: a #GtkPlacesView
|
|
*
|
|
* Retrieves the current search query from @view.
|
|
*
|
|
* Returns: (transfer none): the current search query.
|
|
*/
|
|
const char *
|
|
gtk_places_view_get_search_query (GtkPlacesView *view)
|
|
{
|
|
g_return_val_if_fail (GTK_IS_PLACES_VIEW (view), NULL);
|
|
|
|
return view->search_query;
|
|
}
|
|
|
|
/*
|
|
* gtk_places_view_set_search_query:
|
|
* @view: a #GtkPlacesView
|
|
* @query_text: the query, or NULL.
|
|
*
|
|
* Sets the search query of @view. The search is immediately performed
|
|
* once the query is set.
|
|
*/
|
|
void
|
|
gtk_places_view_set_search_query (GtkPlacesView *view,
|
|
const char *query_text)
|
|
{
|
|
g_return_if_fail (GTK_IS_PLACES_VIEW (view));
|
|
|
|
if (g_strcmp0 (view->search_query, query_text) != 0)
|
|
{
|
|
g_clear_pointer (&view->search_query, g_free);
|
|
view->search_query = g_utf8_strdown (query_text, -1);
|
|
|
|
gtk_list_box_invalidate_filter (GTK_LIST_BOX (view->listbox));
|
|
gtk_list_box_invalidate_headers (GTK_LIST_BOX (view->listbox));
|
|
|
|
update_view_mode (view);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* gtk_places_view_get_loading:
|
|
* @view: a #GtkPlacesView
|
|
*
|
|
* Returns %TRUE if the view is loading locations.
|
|
*/
|
|
gboolean
|
|
gtk_places_view_get_loading (GtkPlacesView *view)
|
|
{
|
|
g_return_val_if_fail (GTK_IS_PLACES_VIEW (view), FALSE);
|
|
|
|
return view->loading;
|
|
}
|
|
|
|
static void
|
|
update_loading (GtkPlacesView *view)
|
|
{
|
|
gboolean loading;
|
|
|
|
g_return_if_fail (GTK_IS_PLACES_VIEW (view));
|
|
|
|
loading = view->fetching_networks || view->connecting_to_server ||
|
|
view->mounting_volume || view->unmounting_mount;
|
|
|
|
set_busy_cursor (view, loading);
|
|
gtk_places_view_set_loading (view, loading);
|
|
}
|
|
|
|
static void
|
|
gtk_places_view_set_loading (GtkPlacesView *view,
|
|
gboolean loading)
|
|
{
|
|
g_return_if_fail (GTK_IS_PLACES_VIEW (view));
|
|
|
|
if (view->loading != loading)
|
|
{
|
|
view->loading = loading;
|
|
g_object_notify_by_pspec (G_OBJECT (view), properties [PROP_LOADING]);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
gtk_places_view_get_fetching_networks (GtkPlacesView *view)
|
|
{
|
|
g_return_val_if_fail (GTK_IS_PLACES_VIEW (view), FALSE);
|
|
|
|
return view->fetching_networks;
|
|
}
|
|
|
|
static void
|
|
gtk_places_view_set_fetching_networks (GtkPlacesView *view,
|
|
gboolean fetching_networks)
|
|
{
|
|
g_return_if_fail (GTK_IS_PLACES_VIEW (view));
|
|
|
|
if (view->fetching_networks != fetching_networks)
|
|
{
|
|
view->fetching_networks = fetching_networks;
|
|
g_object_notify_by_pspec (G_OBJECT (view), properties [PROP_FETCHING_NETWORKS]);
|
|
}
|
|
}
|