/* * gtkappchooserdialog.c: an app-chooser dialog * * Copyright (C) 2004 Novell, Inc. * Copyright (C) 2007, 2010 Red Hat, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with the Gnome Library; see the file COPYING.LIB. If not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * * Authors: Dave Camp * Alexander Larsson * Cosimo Cecchi */ /** * SECTION:gtkappchooserdialog * @Title: GtkAppChooserDialog * @Short_description: An application chooser dialog * * #GtkAppChooserDialog shows a #GtkAppChooserWidget inside a #GtkDialog. * * Note that #GtkAppChooserDialog does not have any interesting methods * of its own. Instead, you should get the embedded #GtkAppChooserWidget * using gtk_app_chooser_dialog_get_widget() and call its methods if * the generic #GtkAppChooser interface is not sufficient for your needs. * * To set the heading that is shown above the #GtkAppChooserWidget, * use gtk_app_chooser_dialog_set_heading(). */ #include "config.h" #include "gtkappchooserdialog.h" #include "gtkintl.h" #include "gtkappchooser.h" #include "gtkappchooseronline.h" #include "gtkappchooserprivate.h" #include "gtkappchooserprivate.h" #include "gtkmessagedialog.h" #include "gtklabel.h" #include "gtkbbox.h" #include "gtkbutton.h" #include "gtkmenuitem.h" #include "gtkstock.h" #include #include #include #define sure_string(s) ((const char *) ((s) != NULL ? (s) : "")) struct _GtkAppChooserDialogPrivate { char *content_type; GFile *gfile; char *heading; GtkWidget *label; GtkWidget *button; GtkWidget *online_button; GtkWidget *open_label; GtkWidget *app_chooser_widget; GtkWidget *show_more_button; GtkAppChooserOnline *online; GCancellable *online_cancellable; gboolean show_more_clicked; gboolean dismissed; }; enum { PROP_GFILE = 1, PROP_CONTENT_TYPE, PROP_HEADING }; static void gtk_app_chooser_dialog_iface_init (GtkAppChooserIface *iface); G_DEFINE_TYPE_WITH_CODE (GtkAppChooserDialog, gtk_app_chooser_dialog, GTK_TYPE_DIALOG, G_IMPLEMENT_INTERFACE (GTK_TYPE_APP_CHOOSER, gtk_app_chooser_dialog_iface_init)); static void show_error_dialog (const gchar *primary, const gchar *secondary, GtkWindow *parent) { GtkWidget *message_dialog; message_dialog = gtk_message_dialog_new (parent, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, NULL); g_object_set (message_dialog, "text", primary, "secondary-text", secondary, NULL); gtk_dialog_set_default_response (GTK_DIALOG (message_dialog), GTK_RESPONSE_OK); gtk_widget_show (message_dialog); g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL); } static void search_for_mimetype_ready_cb (GObject *source, GAsyncResult *res, gpointer user_data) { GtkAppChooserOnline *online = GTK_APP_CHOOSER_ONLINE (source); GtkAppChooserDialog *self = user_data; GError *error = NULL; gdk_threads_enter (); _gtk_app_chooser_online_search_for_mimetype_finish (online, res, &error); if (self->priv->dismissed) goto out; if (error != NULL && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { show_error_dialog (_("Failed to look for applications online"), error->message, GTK_WINDOW (self)); } else { gtk_widget_set_sensitive (self->priv->online_button, TRUE); gtk_app_chooser_refresh (GTK_APP_CHOOSER (self->priv->app_chooser_widget)); } out: g_clear_object (&self->priv->online_cancellable); g_clear_error (&error); g_object_unref (self); gdk_threads_leave (); } static void online_button_clicked_cb (GtkButton *b, gpointer user_data) { GtkAppChooserDialog *self = user_data; self->priv->online_cancellable = g_cancellable_new (); gtk_widget_set_sensitive (self->priv->online_button, FALSE); _gtk_app_chooser_online_search_for_mimetype_async (self->priv->online, self->priv->content_type, GTK_WINDOW (self), self->priv->online_cancellable, search_for_mimetype_ready_cb, g_object_ref (self)); } static void app_chooser_online_get_default_ready_cb (GObject *source, GAsyncResult *res, gpointer user_data) { GtkAppChooserDialog *self = user_data; gdk_threads_enter (); self->priv->online = _gtk_app_chooser_online_get_default_finish (source, res); if (self->priv->online != NULL && !self->priv->dismissed) { GtkWidget *action_area; action_area = gtk_dialog_get_action_area (GTK_DIALOG (self)); self->priv->online_button = gtk_button_new_with_label (_("Find applications online")); gtk_box_pack_start (GTK_BOX (action_area), self->priv->online_button, FALSE, FALSE, 0); gtk_button_box_set_child_secondary (GTK_BUTTON_BOX (action_area), self->priv->online_button, TRUE); g_signal_connect (self->priv->online_button, "clicked", G_CALLBACK (online_button_clicked_cb), self); if (!self->priv->content_type) gtk_widget_set_sensitive (self->priv->online_button, FALSE); gtk_widget_show (self->priv->online_button); } g_object_unref (self); gdk_threads_leave (); } static void ensure_online_button (GtkAppChooserDialog *self) { _gtk_app_chooser_online_get_default_async (app_chooser_online_get_default_ready_cb, g_object_ref (self)); } /* An application is valid if: * * 1) The file exists * 2) The user has permissions to run the file */ static gboolean check_application (GtkAppChooserDialog *self, GAppInfo **app_out) { const char *command; char *path = NULL; char **argv = NULL; int argc; GError *error = NULL; gint retval = TRUE; GAppInfo *info; command = NULL; info = gtk_app_chooser_get_app_info (GTK_APP_CHOOSER (self->priv->app_chooser_widget)); if (info == NULL) { *app_out = NULL; return FALSE; } command = g_app_info_get_executable (info); g_shell_parse_argv (command, &argc, &argv, &error); if (error) { show_error_dialog (_("Could not run application"), error->message, GTK_WINDOW (self)); g_error_free (error); retval = FALSE; goto cleanup; } path = g_find_program_in_path (argv[0]); if (!path) { char *error_message; error_message = g_strdup_printf (_("Could not find '%s'"), argv[0]); show_error_dialog (_("Could not find application"), error_message, GTK_WINDOW (self)); g_free (error_message); retval = FALSE; goto cleanup; } *app_out = info; cleanup: g_strfreev (argv); g_free (path); return retval; } static void add_or_find_application (GtkAppChooserDialog *self) { GAppInfo *app; app = gtk_app_chooser_get_app_info (GTK_APP_CHOOSER (self)); if (app) { /* we don't care about reporting errors here */ if (self->priv->content_type) g_app_info_set_as_last_used_for_type (app, self->priv->content_type, NULL); g_object_unref (app); } } static void cancel_and_clear_cancellable (GtkAppChooserDialog *self) { if (self->priv->online_cancellable != NULL) { g_cancellable_cancel (self->priv->online_cancellable); g_clear_object (&self->priv->online_cancellable); } } static void gtk_app_chooser_dialog_response (GtkDialog *dialog, gint response_id, gpointer user_data) { GtkAppChooserDialog *self = GTK_APP_CHOOSER_DIALOG (dialog); switch (response_id) { case GTK_RESPONSE_OK: add_or_find_application (self); break; case GTK_RESPONSE_CANCEL: case GTK_RESPONSE_DELETE_EVENT: cancel_and_clear_cancellable (self); self->priv->dismissed = TRUE; default : break; } } static void widget_application_selected_cb (GtkAppChooserWidget *widget, GAppInfo *app_info, gpointer user_data) { GtkAppChooserDialog *self = user_data; gtk_widget_set_sensitive (self->priv->button, TRUE); } static void widget_application_activated_cb (GtkAppChooserWidget *widget, GAppInfo *app_info, gpointer user_data) { GtkAppChooserDialog *self = user_data; gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_OK); } static char * get_extension (const char *basename) { char *p; p = strrchr (basename, '.'); if (p && *(p + 1) != '\0') return g_strdup (p + 1); return NULL; } static void set_dialog_properties (GtkAppChooserDialog *self) { gchar *label; gchar *name; gchar *extension; gchar *description; gchar *default_text; gchar *string; gboolean unknown; PangoFontDescription *font_desc; name = NULL; extension = NULL; label = NULL; description = NULL; unknown = TRUE; if (self->priv->gfile != NULL) { name = g_file_get_basename (self->priv->gfile); extension = get_extension (name); } if (self->priv->content_type) { description = g_content_type_get_description (self->priv->content_type); unknown = g_content_type_is_unknown (self->priv->content_type); } gtk_window_set_title (GTK_WINDOW (self), ""); if (name != NULL) { /* Translators: %s is a filename */ label = g_strdup_printf (_("Select an application to open \"%s\""), name); string = g_strdup_printf (_("No applications available to open \"%s\""), name); } else { /* Translators: %s is a file type description */ label = g_strdup_printf (_("Select an application for \"%s\" files"), unknown ? self->priv->content_type : description); string = g_strdup_printf (_("No applications available to open \"%s\" files"), unknown ? self->priv->content_type : description); } font_desc = pango_font_description_new (); pango_font_description_set_weight (font_desc, PANGO_WEIGHT_BOLD); gtk_widget_override_font (self->priv->label, font_desc); pango_font_description_free (font_desc); if (self->priv->heading != NULL) gtk_label_set_markup (GTK_LABEL (self->priv->label), self->priv->heading); else gtk_label_set_markup (GTK_LABEL (self->priv->label), label); default_text = g_strdup_printf ("%s\n%s", string, _("Click \"Show other applications\", for more options, or " "\"Find applications online\" to install a new application")); gtk_app_chooser_widget_set_default_text (GTK_APP_CHOOSER_WIDGET (self->priv->app_chooser_widget), default_text); g_free (label); g_free (name); g_free (extension); g_free (description); g_free (string); g_free (default_text); } static void show_more_button_clicked_cb (GtkButton *button, gpointer user_data) { GtkAppChooserDialog *self = user_data; g_object_set (self->priv->app_chooser_widget, "show-recommended", TRUE, "show-fallback", TRUE, "show-other", TRUE, NULL); gtk_widget_hide (self->priv->show_more_button); self->priv->show_more_clicked = TRUE; } static void widget_notify_for_button_cb (GObject *source, GParamSpec *pspec, gpointer user_data) { GtkAppChooserDialog *self = user_data; GtkAppChooserWidget *widget = GTK_APP_CHOOSER_WIDGET (source); gboolean should_hide; should_hide = gtk_app_chooser_widget_get_show_other (widget) || self->priv->show_more_clicked; if (should_hide) gtk_widget_hide (self->priv->show_more_button); } static void forget_menu_item_activate_cb (GtkMenuItem *item, gpointer user_data) { GtkAppChooserDialog *self = user_data; GAppInfo *info; info = gtk_app_chooser_get_app_info (GTK_APP_CHOOSER (self)); if (info != NULL) { g_app_info_remove_supports_type (info, self->priv->content_type, NULL); gtk_app_chooser_refresh (GTK_APP_CHOOSER (self)); g_object_unref (info); } } static GtkWidget * build_forget_menu_item (GtkAppChooserDialog *self) { GtkWidget *retval; retval = gtk_menu_item_new_with_label (_("Forget association")); gtk_widget_show (retval); g_signal_connect (retval, "activate", G_CALLBACK (forget_menu_item_activate_cb), self); return retval; } static void widget_populate_popup_cb (GtkAppChooserWidget *widget, GtkMenu *menu, GAppInfo *info, gpointer user_data) { GtkAppChooserDialog *self = user_data; GtkWidget *menu_item; if (g_app_info_can_remove_supports_type (info)) { menu_item = build_forget_menu_item (self); gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item); } } static void build_dialog_ui (GtkAppChooserDialog *self) { GtkWidget *vbox; GtkWidget *vbox2; GtkWidget *button, *w; GAppInfo *info; gtk_container_set_border_width (GTK_CONTAINER (self), 5); vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); gtk_container_set_border_width (GTK_CONTAINER (vbox), 5); gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (self))), vbox, TRUE, TRUE, 0); gtk_widget_show (vbox); vbox2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); gtk_box_pack_start (GTK_BOX (vbox), vbox2, TRUE, TRUE, 0); gtk_widget_show (vbox2); self->priv->label = gtk_label_new (""); gtk_widget_set_halign (self->priv->label, GTK_ALIGN_START); gtk_widget_set_valign (self->priv->label, GTK_ALIGN_CENTER); gtk_label_set_line_wrap (GTK_LABEL (self->priv->label), TRUE); gtk_box_pack_start (GTK_BOX (vbox2), self->priv->label, FALSE, FALSE, 0); gtk_widget_show (self->priv->label); self->priv->app_chooser_widget = gtk_app_chooser_widget_new (self->priv->content_type); gtk_box_pack_start (GTK_BOX (vbox2), self->priv->app_chooser_widget, TRUE, TRUE, 0); gtk_widget_show (self->priv->app_chooser_widget); g_signal_connect (self->priv->app_chooser_widget, "application-selected", G_CALLBACK (widget_application_selected_cb), self); g_signal_connect (self->priv->app_chooser_widget, "application-activated", G_CALLBACK (widget_application_activated_cb), self); g_signal_connect (self->priv->app_chooser_widget, "notify::show-other", G_CALLBACK (widget_notify_for_button_cb), self); g_signal_connect (self->priv->app_chooser_widget, "populate-popup", G_CALLBACK (widget_populate_popup_cb), self); button = gtk_button_new_with_label (_("Show other applications")); self->priv->show_more_button = button; w = gtk_image_new_from_stock (GTK_STOCK_ADD, GTK_ICON_SIZE_BUTTON); gtk_button_set_image (GTK_BUTTON (button), w); gtk_box_pack_start (GTK_BOX (self->priv->app_chooser_widget), button, FALSE, FALSE, 6); gtk_widget_show_all (button); g_signal_connect (button, "clicked", G_CALLBACK (show_more_button_clicked_cb), self); gtk_dialog_add_button (GTK_DIALOG (self), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL); self->priv->button = gtk_dialog_add_button (GTK_DIALOG (self), _("_Select"), GTK_RESPONSE_OK); info = gtk_app_chooser_get_app_info (GTK_APP_CHOOSER (self->priv->app_chooser_widget)); gtk_widget_set_sensitive (self->priv->button, info != NULL); if (info) g_object_unref (info); gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_OK); } static void set_gfile_and_content_type (GtkAppChooserDialog *self, GFile *file) { GFileInfo *info; if (file == NULL) return; self->priv->gfile = g_object_ref (file); info = g_file_query_info (self->priv->gfile, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, 0, NULL, NULL); self->priv->content_type = g_strdup (g_file_info_get_content_type (info)); g_object_unref (info); } static GAppInfo * gtk_app_chooser_dialog_get_app_info (GtkAppChooser *object) { GtkAppChooserDialog *self = GTK_APP_CHOOSER_DIALOG (object); GAppInfo *app = NULL; if (!check_application (self, &app)) return NULL; return app; } static void gtk_app_chooser_dialog_refresh (GtkAppChooser *object) { GtkAppChooserDialog *self = GTK_APP_CHOOSER_DIALOG (object); gtk_app_chooser_refresh (GTK_APP_CHOOSER (self->priv->app_chooser_widget)); } static void gtk_app_chooser_dialog_constructed (GObject *object) { GtkAppChooserDialog *self = GTK_APP_CHOOSER_DIALOG (object); if (G_OBJECT_CLASS (gtk_app_chooser_dialog_parent_class)->constructed != NULL) G_OBJECT_CLASS (gtk_app_chooser_dialog_parent_class)->constructed (object); build_dialog_ui (self); set_dialog_properties (self); ensure_online_button (self); } static void gtk_app_chooser_dialog_dispose (GObject *object) { GtkAppChooserDialog *self = GTK_APP_CHOOSER_DIALOG (object); g_clear_object (&self->priv->gfile); cancel_and_clear_cancellable (self); g_clear_object (&self->priv->online); G_OBJECT_CLASS (gtk_app_chooser_dialog_parent_class)->dispose (object); } static void gtk_app_chooser_dialog_finalize (GObject *object) { GtkAppChooserDialog *self = GTK_APP_CHOOSER_DIALOG (object); g_free (self->priv->content_type); G_OBJECT_CLASS (gtk_app_chooser_dialog_parent_class)->finalize (object); } static void gtk_app_chooser_dialog_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GtkAppChooserDialog *self = GTK_APP_CHOOSER_DIALOG (object); switch (property_id) { case PROP_GFILE: set_gfile_and_content_type (self, g_value_get_object (value)); break; case PROP_CONTENT_TYPE: /* don't try to override a value previously set with the GFile */ if (self->priv->content_type == NULL) self->priv->content_type = g_value_dup_string (value); break; case PROP_HEADING: gtk_app_chooser_dialog_set_heading (self, g_value_get_string (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gtk_app_chooser_dialog_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GtkAppChooserDialog *self = GTK_APP_CHOOSER_DIALOG (object); switch (property_id) { case PROP_GFILE: if (self->priv->gfile != NULL) g_value_set_object (value, self->priv->gfile); break; case PROP_CONTENT_TYPE: g_value_set_string (value, self->priv->content_type); break; case PROP_HEADING: g_value_set_string (value, self->priv->heading); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gtk_app_chooser_dialog_iface_init (GtkAppChooserIface *iface) { iface->get_app_info = gtk_app_chooser_dialog_get_app_info; iface->refresh = gtk_app_chooser_dialog_refresh; } static void gtk_app_chooser_dialog_class_init (GtkAppChooserDialogClass *klass) { GObjectClass *gobject_class; GParamSpec *pspec; gobject_class = G_OBJECT_CLASS (klass); gobject_class->dispose = gtk_app_chooser_dialog_dispose; gobject_class->finalize = gtk_app_chooser_dialog_finalize; gobject_class->set_property = gtk_app_chooser_dialog_set_property; gobject_class->get_property = gtk_app_chooser_dialog_get_property; gobject_class->constructed = gtk_app_chooser_dialog_constructed; g_object_class_override_property (gobject_class, PROP_CONTENT_TYPE, "content-type"); /** * GtkAppChooserDialog:gfile: * * The GFile used by the #GtkAppChooserDialog. * The dialog's #GtkAppChooserWidget content type will be guessed from the * file, if present. */ pspec = g_param_spec_object ("gfile", P_("GFile"), P_("The GFile used by the app chooser dialog"), G_TYPE_FILE, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (gobject_class, PROP_GFILE, pspec); /** * GtkAppChooserDialog:heading: * * The text to show at the top of the dialog. * The string may contain Pango markup. */ pspec = g_param_spec_string ("heading", P_("Heading"), P_("The text to show at the top of the dialog"), NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (gobject_class, PROP_HEADING, pspec); g_type_class_add_private (klass, sizeof (GtkAppChooserDialogPrivate)); } static void gtk_app_chooser_dialog_init (GtkAppChooserDialog *self) { self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GTK_TYPE_APP_CHOOSER_DIALOG, GtkAppChooserDialogPrivate); /* we can't override the class signal handler here, as it's a RUN_LAST; * we want our signal handler instead to be executed before any user code. */ g_signal_connect (self, "response", G_CALLBACK (gtk_app_chooser_dialog_response), NULL); } static void set_parent_and_flags (GtkWidget *dialog, GtkWindow *parent, GtkDialogFlags flags) { if (parent != NULL) gtk_window_set_transient_for (GTK_WINDOW (dialog), parent); if (flags & GTK_DIALOG_MODAL) gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); if (flags & GTK_DIALOG_DESTROY_WITH_PARENT) gtk_window_set_destroy_with_parent (GTK_WINDOW (dialog), TRUE); } /** * gtk_app_chooser_dialog_new: * @parent: (allow-none): a #GtkWindow, or %NULL * @flags: flags for this dialog * @file: a #GFile * * Creates a new #GtkAppChooserDialog for the provided #GFile, * to allow the user to select an application for it. * * Returns: a newly created #GtkAppChooserDialog * * Since: 3.0 **/ GtkWidget * gtk_app_chooser_dialog_new (GtkWindow *parent, GtkDialogFlags flags, GFile *file) { GtkWidget *retval; g_return_val_if_fail (G_IS_FILE (file), NULL); retval = g_object_new (GTK_TYPE_APP_CHOOSER_DIALOG, "gfile", file, NULL); set_parent_and_flags (retval, parent, flags); return retval; } /** * gtk_app_chooser_dialog_new_for_content_type: * @parent: (allow-none): a #GtkWindow, or %NULL * @flags: flags for this dialog * @content_type: a content type string * * Creates a new #GtkAppChooserDialog for the provided content type, * to allow the user to select an application for it. * * Returns: a newly created #GtkAppChooserDialog * * Since: 3.0 **/ GtkWidget * gtk_app_chooser_dialog_new_for_content_type (GtkWindow *parent, GtkDialogFlags flags, const gchar *content_type) { GtkWidget *retval; g_return_val_if_fail (content_type != NULL, NULL); retval = g_object_new (GTK_TYPE_APP_CHOOSER_DIALOG, "content-type", content_type, NULL); set_parent_and_flags (retval, parent, flags); return retval; } /** * gtk_app_chooser_dialog_get_widget: * @self: a #GtkAppChooserDialog * * Returns the #GtkAppChooserWidget of this dialog. * * Returns: (transfer none): the #GtkAppChooserWidget of @self * * Since: 3.0 */ GtkWidget * gtk_app_chooser_dialog_get_widget (GtkAppChooserDialog *self) { g_return_val_if_fail (GTK_IS_APP_CHOOSER_DIALOG (self), NULL); return self->priv->app_chooser_widget; } /** * gtk_app_chooser_dialog_set_heading: * @self: a #GtkAppChooserDialog * @heading: a string containing Pango markup * * Sets the text to display at the top of the dialog. * If the heading is not set, the dialog displays a default text. */ void gtk_app_chooser_dialog_set_heading (GtkAppChooserDialog *self, const gchar *heading) { g_return_if_fail (GTK_IS_APP_CHOOSER_DIALOG (self)); g_free (self->priv->heading); self->priv->heading = g_strdup (heading); if (self->priv->label && self->priv->heading) gtk_label_set_markup (GTK_LABEL (self->priv->label), self->priv->heading); g_object_notify (G_OBJECT (self), "heading"); } /** * gtk_app_chooser_dialog_get_heading: * @self: a #GtkAppChooserDialog * * Returns the text to display at the top of the dialog. * * Returns: the text to display at the top of the dialog, or %NULL, in which * case a default text is displayed */ const gchar * gtk_app_chooser_dialog_get_heading (GtkAppChooserDialog *self) { g_return_val_if_fail (GTK_IS_APP_CHOOSER_DIALOG (self), NULL); return self->priv->heading; }