diff --git a/docs/xapp-docs.xml b/docs/xapp-docs.xml index e9bc7b0..9df733c 100644 --- a/docs/xapp-docs.xml +++ b/docs/xapp-docs.xml @@ -16,10 +16,13 @@ API reference + + + - + diff --git a/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-behavior-symbolic.svg b/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-behavior-symbolic.svg new file mode 100644 index 0000000..b73d6b3 --- /dev/null +++ b/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-behavior-symbolic.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-display-symbolic.svg b/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-display-symbolic.svg new file mode 100644 index 0000000..e53d94a --- /dev/null +++ b/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-display-symbolic.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-plugins-symbolic.svg b/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-plugins-symbolic.svg new file mode 100644 index 0000000..4284ae3 --- /dev/null +++ b/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-plugins-symbolic.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-preview-symbolic.svg b/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-preview-symbolic.svg new file mode 100644 index 0000000..cdd0f98 --- /dev/null +++ b/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-preview-symbolic.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-toolbar-symbolic.svg b/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-toolbar-symbolic.svg new file mode 100644 index 0000000..909174e --- /dev/null +++ b/files/usr/share/icons/hicolor/scalable/categories/xapp-prefs-toolbar-symbolic.svg @@ -0,0 +1,19 @@ + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + + + + diff --git a/libxapp/meson.build b/libxapp/meson.build index 8c071b5..706a632 100644 --- a/libxapp/meson.build +++ b/libxapp/meson.build @@ -11,27 +11,41 @@ xapp_headers = [ 'xapp-gtk-window.h', + 'xapp-icon-chooser-button.h', + 'xapp-icon-chooser-dialog.h', 'xapp-kbd-layout-controller.h', 'xapp-monitor-blanker.h', - 'xapp-preferences-window.h' + 'xapp-preferences-window.h', + 'xapp-stack-sidebar.h', ] xapp_sources = [ 'xapp-glade-catalog.c', 'xapp-gtk-window.c', + 'xapp-icon-chooser-button.c', + 'xapp-icon-chooser-dialog.c', 'xapp-kbd-layout-controller.c', 'xapp-monitor-blanker.c', 'xapp-preferences-window.c', + 'xapp-stack-sidebar.c', ] +xapp_enums = gnome.mkenums('xapp-enums', + sources : xapp_headers, + c_template : 'xapp-enums.c.template', + h_template : 'xapp-enums.h.template', + identifier_prefix : 'XApp', + symbol_prefix : 'xapp' +) + libxapp = library('xapp', - sources : xapp_headers + xapp_sources, + sources : xapp_headers + xapp_sources + xapp_enums, include_directories: [top_inc], version: meson.project_version(), soversion: '1', dependencies: libdeps, c_args: ['-Wno-declaration-after-statement'], - link_args: [ '-Wl,-Bsymbolic', '-Wl,-z,relro', '-Wl,-z,now', ], + link_args: [ '-Wl,-Bsymbolic', '-Wl,-z,relro', '-Wl,-z,now', '-lm'], install: true ) @@ -75,4 +89,4 @@ sources: gir[0], metadata_dirs: meson.current_source_dir(), install: true -) \ No newline at end of file +) diff --git a/libxapp/xapp-enums.c.template b/libxapp/xapp-enums.c.template new file mode 100644 index 0000000..d974436 --- /dev/null +++ b/libxapp/xapp-enums.c.template @@ -0,0 +1,44 @@ +/*** BEGIN file-header ***/ + +#include "config.h" +#include "xapp-enums.h" +#include "xapp-icon-chooser-dialog.h" + +#define C_ENUM(v) ((gint) v) +#define C_FLAGS(v) ((guint) v) + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* enumerations from "@basename@" */ + +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType +@enum_name@_get_type (void) +{ + static volatile gsize gtype_id = 0; + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { C_ENUM(@VALUENAME@), "@VALUENAME@", "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, NULL, NULL } + }; + if (g_once_init_enter (>ype_id)) { + GType new_type = g_@type@_register_static ("@EnumName@", values); + g_once_init_leave (>ype_id, new_type); + } + return (GType) gtype_id; +} + +/*** END value-tail ***/ + +/*** BEGIN file-tail ***/ + +/*** END file-tail ***/ diff --git a/libxapp/xapp-enums.h.template b/libxapp/xapp-enums.h.template new file mode 100644 index 0000000..f846052 --- /dev/null +++ b/libxapp/xapp-enums.h.template @@ -0,0 +1,24 @@ +/*** BEGIN file-header ***/ +#ifndef __XAPP_ENUMS_H__ +#define __XAPP_ENUMS_H__ + +#include + +G_BEGIN_DECLS +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* enumerations from "@basename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType @enum_name@_get_type (void); +#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) +/*** END value-header ***/ + +/*** BEGIN file-tail ***/ +G_END_DECLS + +#endif /* __XAPP_ENUMS_H__ */ +/*** END file-tail ***/ diff --git a/libxapp/xapp-glade-catalog.c b/libxapp/xapp-glade-catalog.c index 813ca48..59c3d3d 100644 --- a/libxapp/xapp-glade-catalog.c +++ b/libxapp/xapp-glade-catalog.c @@ -2,6 +2,8 @@ #include #include "xapp-gtk-window.h" +#include "xapp-icon-chooser-button.h" +#include "xapp-stack-sidebar.h" void xapp_glade_catalog_init (const gchar *catalog_name); @@ -10,4 +12,6 @@ xapp_glade_catalog_init (const gchar *catalog_name) { g_type_ensure (XAPP_TYPE_GTK_WINDOW); + g_type_ensure (XAPP_TYPE_ICON_CHOOSER_BUTTON); + g_type_ensure (XAPP_TYPE_STACK_SIDEBAR); } diff --git a/libxapp/xapp-glade-catalog.xml b/libxapp/xapp-glade-catalog.xml index 18f46c3..798e272 100644 --- a/libxapp/xapp-glade-catalog.xml +++ b/libxapp/xapp-glade-catalog.xml @@ -4,9 +4,15 @@ + + + + diff --git a/libxapp/xapp-gtk-window.c b/libxapp/xapp-gtk-window.c index 4a433be..75edfb9 100644 --- a/libxapp/xapp-gtk-window.c +++ b/libxapp/xapp-gtk-window.c @@ -60,13 +60,6 @@ guint progress; gboolean progress_pulse; } XAppGtkWindowPrivate; - -struct _XAppGtkWindow -{ - GtkWindow parent_object; - - XAppGtkWindowPrivate *priv; -}; G_DEFINE_TYPE_WITH_PRIVATE (XAppGtkWindow, xapp_gtk_window, GTK_TYPE_WINDOW) @@ -340,7 +333,7 @@ xapp_gtk_window_realize (GtkWidget *widget) { XAppGtkWindow *window = XAPP_GTK_WINDOW (widget); - XAppGtkWindowPrivate *priv = window->priv; + XAppGtkWindowPrivate *priv = xapp_gtk_window_get_instance_private (window); GTK_WIDGET_CLASS (xapp_gtk_window_parent_class)->realize (widget); @@ -358,7 +351,7 @@ xapp_gtk_window_finalize (GObject *object) { XAppGtkWindow *window = XAPP_GTK_WINDOW (object); - XAppGtkWindowPrivate *priv = window->priv; + XAppGtkWindowPrivate *priv = xapp_gtk_window_get_instance_private (window); clear_icon_strings (priv); @@ -369,10 +362,7 @@ xapp_gtk_window_init (XAppGtkWindow *window) { XAppGtkWindowPrivate *priv; - - window->priv = G_TYPE_INSTANCE_GET_PRIVATE (window, XAPP_TYPE_GTK_WINDOW, XAppGtkWindowPrivate); - - priv = window->priv; + priv = xapp_gtk_window_get_instance_private (window); priv->icon_name = NULL; priv->icon_path = NULL; @@ -421,7 +411,9 @@ { g_return_if_fail (XAPP_IS_GTK_WINDOW (window)); - set_icon_name_internal (GTK_WINDOW (window), window->priv, icon_name); + XAppGtkWindowPrivate *priv = xapp_gtk_window_get_instance_private (window); + + set_icon_name_internal (GTK_WINDOW (window), priv, icon_name); } /** @@ -443,7 +435,9 @@ { g_return_if_fail (XAPP_IS_GTK_WINDOW (window)); - set_icon_from_file_internal (GTK_WINDOW (window), window->priv, file_name, error); + XAppGtkWindowPrivate *priv = xapp_gtk_window_get_instance_private (window); + + set_icon_from_file_internal (GTK_WINDOW (window), priv, file_name, error); } /** @@ -469,7 +463,9 @@ { g_return_if_fail (XAPP_IS_GTK_WINDOW (window)); - set_progress_internal (GTK_WINDOW (window), window->priv, progress); + XAppGtkWindowPrivate *priv = xapp_gtk_window_get_instance_private (window); + + set_progress_internal (GTK_WINDOW (window), priv, progress); } /** @@ -494,7 +490,9 @@ g_return_if_fail (XAPP_IS_GTK_WINDOW (window)); g_return_if_fail (XAPP_IS_GTK_WINDOW (window)); - set_progress_pulse_internal (GTK_WINDOW (window), window->priv, pulse); + XAppGtkWindowPrivate *priv = xapp_gtk_window_get_instance_private (window); + + set_progress_pulse_internal (GTK_WINDOW (window), priv, pulse); } diff --git a/libxapp/xapp-gtk-window.h b/libxapp/xapp-gtk-window.h index 5df7e1a..9b2d75b 100644 --- a/libxapp/xapp-gtk-window.h +++ b/libxapp/xapp-gtk-window.h @@ -10,7 +10,14 @@ #define XAPP_TYPE_GTK_WINDOW (xapp_gtk_window_get_type ()) -G_DECLARE_FINAL_TYPE (XAppGtkWindow, xapp_gtk_window, XAPP, GTK_WINDOW, GtkWindow) +G_DECLARE_DERIVABLE_TYPE (XAppGtkWindow, xapp_gtk_window, XAPP, GTK_WINDOW, GtkWindow) + +struct _XAppGtkWindowClass +{ + GtkWindowClass parent_class; + + gpointer padding[12]; +}; /* Class */ GtkWidget *xapp_gtk_window_new (GtkWindowType type); diff --git a/libxapp/xapp-icon-chooser-button.c b/libxapp/xapp-icon-chooser-button.c new file mode 100644 index 0000000..2b06253 --- /dev/null +++ b/libxapp/xapp-icon-chooser-button.c @@ -0,0 +1,346 @@ +#include +#include "xapp-icon-chooser-button.h" +#include + +#define XAPP_BUTTON_ICON_SIZE_DEFAULT GTK_ICON_SIZE_DIALOG + +/** + * SECTION:xapp-icon-chooser-button + * @Short_description: A button for selecting an icon + * @Title: XAppIconChooserButton + * + * The XAppIconChooserButton creates a button so that + * the user can select an icon. When the button is clicked + * it will open an XAppIconChooserDialog. The currently + * selected icon will be displayed as the button image. + */ + +typedef struct +{ + GtkWidget *image; + XAppIconChooserDialog *dialog; + GtkIconSize icon_size; + gchar *icon_string; +} XAppIconChooserButtonPrivate; + +struct _XAppIconChooserButton +{ + GtkButton parent_instance; +}; + +enum +{ + PROP_0, + PROP_ICON_SIZE, + PROP_ICON, + N_PROPERTIES +}; + +static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, }; + +G_DEFINE_TYPE_WITH_PRIVATE (XAppIconChooserButton, xapp_icon_chooser_button, GTK_TYPE_BUTTON) + +static void +on_clicked (GtkButton *button) +{ + XAppIconChooserButtonPrivate *priv; + GtkResponseType response; + + priv = xapp_icon_chooser_button_get_instance_private (XAPP_ICON_CHOOSER_BUTTON (button)); + + gtk_window_set_transient_for (GTK_WINDOW (priv->dialog), GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (button)))); + + if (priv->icon_string == NULL) + { + response = xapp_icon_chooser_dialog_run (priv->dialog); + } + else + { + response = xapp_icon_chooser_dialog_run_with_icon (priv->dialog, priv->icon_string); + } + + if (response == GTK_RESPONSE_OK) + { + gchar *icon; + + icon = xapp_icon_chooser_dialog_get_icon_string (priv->dialog); + xapp_icon_chooser_button_set_icon (XAPP_ICON_CHOOSER_BUTTON (button), icon); + } +} + +void +xapp_icon_chooser_button_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + XAppIconChooserButton *button; + XAppIconChooserButtonPrivate *priv; + + button = XAPP_ICON_CHOOSER_BUTTON (object); + priv = xapp_icon_chooser_button_get_instance_private (button); + + switch (prop_id) + { + case PROP_ICON_SIZE: + g_value_set_enum (value, priv->icon_size); + break; + case PROP_ICON: + g_value_set_string (value, priv->icon_string); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +void +xapp_icon_chooser_button_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + XAppIconChooserButton *button; + XAppIconChooserButtonPrivate *priv; + + button = XAPP_ICON_CHOOSER_BUTTON (object); + priv = xapp_icon_chooser_button_get_instance_private (button); + + switch (prop_id) + { + case PROP_ICON_SIZE: + xapp_icon_chooser_button_set_icon_size (button, g_value_get_enum (value)); + break; + case PROP_ICON: + xapp_icon_chooser_button_set_icon (button, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +xapp_icon_chooser_button_init (XAppIconChooserButton *button) +{ + XAppIconChooserButtonPrivate *priv; + + priv = xapp_icon_chooser_button_get_instance_private (button); + + priv->image = gtk_image_new_from_icon_name ("unkown", XAPP_BUTTON_ICON_SIZE_DEFAULT); + gtk_button_set_image (GTK_BUTTON (button), priv->image); + + gtk_widget_set_hexpand (GTK_WIDGET (button), FALSE); + gtk_widget_set_vexpand (GTK_WIDGET (button), FALSE); + gtk_widget_set_halign (GTK_WIDGET (button), GTK_ALIGN_CENTER); + gtk_widget_set_valign (GTK_WIDGET (button), GTK_ALIGN_CENTER); + + xapp_icon_chooser_button_set_icon_size (button, -1); + + priv->dialog = xapp_icon_chooser_dialog_new (); +} + +static void +xapp_icon_chooser_button_class_init (XAppIconChooserButtonClass *klass) +{ + GtkBindingSet *binding_set; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkButtonClass *button_class = GTK_BUTTON_CLASS (klass); + + object_class->get_property = xapp_icon_chooser_button_get_property; + object_class->set_property = xapp_icon_chooser_button_set_property; + + button_class->clicked = on_clicked; + + /** + * XAppIconChooserButton:icon-size: + * + * The size to use when displaying the icon. + */ + obj_properties[PROP_ICON_SIZE] = + g_param_spec_enum ("icon-size", + _("Icon size"), + _("The preferred icon size."), + GTK_TYPE_ICON_SIZE, + GTK_ICON_SIZE_DND, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * XAppIconChooserButton:icon: + * + * The preferred size to use when looking up icons. This only works with icon names. + * Additionally, there is no guarantee that a selected icon name will exist in a + * particular size. + */ + obj_properties[PROP_ICON] = + g_param_spec_string ("icon", + _("Icon"), + _("The string representing the icon."), + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPERTIES, obj_properties); +} + +/** + * xapp_icon_chooser_button_new: + * + * Creates a new #XAppIconChooserButton and sets its icon to @icon. + * + * Returns: a newly created #XAppIconChooserButton + */ +XAppIconChooserButton * +xapp_icon_chooser_button_new (void) +{ + return g_object_new (XAPP_TYPE_ICON_CHOOSER_BUTTON, NULL); +} + +/** + * xapp_icon_chooser_button_new_with_size: + * @icon_size: the size of icon to use in the button, or NULL to use the default value. + * + * Creates a new #XAppIconChooserButton, and sets the sizes of the button image and the icons in + * the dialog. Note that xapp_icon_chooser_button_new_with_size (NULL, NULL) is the same as calling + * xapp_icon_chooser_button_new (). + * + * Returns: a newly created #XAppIconChooserButton + */ +XAppIconChooserButton * +xapp_icon_chooser_button_new_with_size (GtkIconSize icon_size) +{ + XAppIconChooserButton *button; + + button = g_object_new (XAPP_TYPE_ICON_CHOOSER_BUTTON, NULL); + + xapp_icon_chooser_button_set_icon_size (button, icon_size); + + return button; +} + +/** + * xapp_icon_chooser_button_set_icon_size: + * @button: a #XAppIconChooserButton + * @icon_size: the size of icon to use in the button, or -1 to use the default value. + * + * Sets the icon size used in the button. + */ +void +xapp_icon_chooser_button_set_icon_size (XAppIconChooserButton *button, + GtkIconSize icon_size) +{ + XAppIconChooserButtonPrivate *priv; + gint width, height; + gchar *icon; + + priv = xapp_icon_chooser_button_get_instance_private (button); + + if (icon_size == -1) + { + priv->icon_size = XAPP_BUTTON_ICON_SIZE_DEFAULT; + } + else + { + priv->icon_size = icon_size; + } + + gtk_icon_size_lookup (priv->icon_size, &width, &height); + gtk_image_set_pixel_size (GTK_IMAGE (priv->image), width); + + // We need to make sure the icon gets resized if it's a file path. Since + // this means regenerating the pixbuf anyway, it's easier to just call + // xapp_icon_chooser_button_set_icon, but we need to dup the string so + // it doens't get freed before it gets used. + icon = g_strdup(priv->icon_string); + xapp_icon_chooser_button_set_icon (button, icon); + g_free (icon); + + g_object_notify (G_OBJECT (button), "icon-size"); +} + +/** + * xapp_icon_chooser_button_get_icon: + * @button: a #XAppIconChooserButton + * + * Gets the icon from the #XAppIconChooserButton. + * + * returns: a string representing the icon. This may be an icon name or a file path. + */ +const gchar* +xapp_icon_chooser_button_get_icon (XAppIconChooserButton *button) +{ + XAppIconChooserButtonPrivate *priv; + + priv = xapp_icon_chooser_button_get_instance_private (button); + + return priv->icon_string; +} + +/** + * xapp_icon_chooser_button_set_icon: + * @button: a #XAppIconChooserButton + * @icon: (nullable): a string representing the icon to be set. This may be an icon name or a file path. + * + * Sets the icon on the #XAppIconChooserButton. + */ +void +xapp_icon_chooser_button_set_icon (XAppIconChooserButton *button, + const gchar *icon) +{ + XAppIconChooserButtonPrivate *priv; + const gchar *icon_string; + + priv = xapp_icon_chooser_button_get_instance_private (button); + + if (priv->icon_string != NULL) + { + g_free (priv->icon_string); + } + + if (icon == NULL) + { + priv->icon_string = NULL; + icon_string = "unkown"; + } + else + { + priv->icon_string = g_strdup (icon); + icon_string = icon; + } + + if (g_strrstr (icon_string, "/")) + { + GdkPixbuf *pixbuf; + gint width, height; + + gtk_icon_size_lookup (priv->icon_size, &width, &height); + + pixbuf = gdk_pixbuf_new_from_file_at_size (icon_string, width, height, NULL); + + gtk_image_set_from_pixbuf (GTK_IMAGE (priv->image), pixbuf); + } + else + { + gtk_image_set_from_icon_name (GTK_IMAGE (priv->image), icon_string, priv->icon_size); + } + + g_object_notify (G_OBJECT (button), "icon"); +} + +/** + * xapp_icon_chooser_button_get_dialog: + * @button: a #XAppIconChooserButton + * + * Gets a reference to the icon chooser dialog for the #XAppIconChooserButton. + * This is useful for setting properties on the dialog. + * + * Returns: (transfer none): the #XAppIconChooserDialog + */ +XAppIconChooserDialog * +xapp_icon_chooser_button_get_dialog (XAppIconChooserButton *button) +{ + XAppIconChooserButtonPrivate *priv; + + priv = xapp_icon_chooser_button_get_instance_private (button); + + return priv->dialog; +} diff --git a/libxapp/xapp-icon-chooser-button.h b/libxapp/xapp-icon-chooser-button.h new file mode 100644 index 0000000..3d8dfcc --- /dev/null +++ b/libxapp/xapp-icon-chooser-button.h @@ -0,0 +1,30 @@ +#ifndef _XAPP_ICON_CHOOSER_BUTTON_H_ +#define _XAPP_ICON_CHOOSER_BUTTON_H_ + +#include +#include +#include "xapp-icon-chooser-dialog.h" +#include "xapp-enums.h" + +G_BEGIN_DECLS + +#define XAPP_TYPE_ICON_CHOOSER_BUTTON (xapp_icon_chooser_button_get_type ()) + +G_DECLARE_FINAL_TYPE (XAppIconChooserButton, xapp_icon_chooser_button, XAPP, ICON_CHOOSER_BUTTON, GtkButton) + +XAppIconChooserButton * xapp_icon_chooser_button_new (void); + +XAppIconChooserButton * xapp_icon_chooser_button_new_with_size (GtkIconSize icon_size); + +void xapp_icon_chooser_button_set_icon_size (XAppIconChooserButton *button, + GtkIconSize icon_size); + +void xapp_icon_chooser_button_set_icon (XAppIconChooserButton *button, + const gchar *icon); + +const gchar* xapp_icon_chooser_button_get_icon (XAppIconChooserButton *button); +XAppIconChooserDialog * xapp_icon_chooser_button_get_dialog (XAppIconChooserButton *button); + +G_END_DECLS + +#endif /* _XAPP_ICON_CHOOSER_DIALOG_H_ */ diff --git a/libxapp/xapp-icon-chooser-dialog.c b/libxapp/xapp-icon-chooser-dialog.c new file mode 100644 index 0000000..64d171c --- /dev/null +++ b/libxapp/xapp-icon-chooser-dialog.c @@ -0,0 +1,1923 @@ +#include +#include +#include +#include "xapp-enums.h" +#include "xapp-icon-chooser-dialog.h" +#include "xapp-stack-sidebar.h" +#include +#include + +#define DEBUG_REFS 0 +#define DEBUG_ICON_THEME 0 + +/** + * SECTION:xapp-icon-chooser-dialog + * @Short_description: A dialog for selecting an icon + * @Title: XAppIconChooserDialog + * + * The XAppIconChooserDialog creates a dialog so that + * the user can select an icon. It provides the ability + * to browse by category, search by icon name, or select + * from a specific file. + */ + +typedef struct +{ + const gchar *name; /* This is a translation which doesn't get freed */ + GList *icons; + GList *iter; + GtkListStore *model; +} IconCategoryInfo; + +typedef struct +{ + GtkResponseType response; + XAppIconSize icon_size; + GtkListStore *category_list; + GtkListStore *search_icon_store; + GFileEnumerator *search_file_enumerator; + GCancellable *cancellable; + GList *full_icon_list; + GList *search_iter; + GHashTable *categories; + GHashTable *pixbufs_by_name; + GtkWidget *search_bar; + GtkWidget *icon_view; + GtkWidget *list_box; + GtkWidget *select_button; + GtkWidget *browse_button; + GtkWidget *action_area; + GtkWidget *loading_bar; + gchar *icon_string; + gchar *current_text; + gulong search_changed_id; + gboolean allow_paths; + IconCategoryInfo *current_category; +} XAppIconChooserDialogPrivate; + +struct _XAppIconChooserDialog +{ + XAppGtkWindow parent_instance; +}; + +typedef struct +{ + XAppIconChooserDialog *dialog; + GtkListStore *model; + IconCategoryInfo *category_info; + GCancellable *cancellable; + GdkPixbuf *pixbuf; + const gchar *name; + gboolean chunk_end; +} NamedIconInfoLoadCallbackInfo; + +typedef struct +{ + XAppIconChooserDialog *dialog; + GtkListStore *model; + GCancellable *cancellable; + GFileEnumerator *enumerator; + gchar *short_name; + gchar *long_name; + gboolean chunk_end; +} FileIconInfoLoadCallbackInfo; + +typedef struct +{ + const gchar *name; + const gchar *contexts[5]; +} IconCategoryDefinition; + +static IconCategoryDefinition categories[] = { + // Category name context names + { + N_("Actions"), { "Actions", NULL } + }, + { + N_("Applications"), { "Applications", "Apps", NULL } + }, + { + N_("Categories"), { "Categories", NULL } + }, + { + N_("Devices"), { "Devices", NULL } + }, + { + N_("Emblems"), { "Emblems", NULL } + }, + { + N_("Emoji"), { "Emotes", NULL } + }, + { + N_("Mime types"), { "MimeTypes", "Mimetypes", NULL } + }, + { + N_("Places"), { "Places", NULL } + }, + { + N_("Status"), { "Status", "Notifications", NULL } + }, + { + N_("Other"), { "Panel", NULL } + } +}; + +enum +{ + CLOSE, + SELECT, + LAST_SIGNAL +}; + +enum +{ + PROP_0, + PROP_ICON_SIZE, + PROP_ALLOW_PATHS, + N_PROPERTIES +}; + +enum +{ + COLUMN_DISPLAY_NAME, + COLUMN_FULL_NAME, + COLUMN_PIXBUF, +}; + +static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, }; + +static guint signals[LAST_SIGNAL] = {0, }; + +G_DEFINE_TYPE_WITH_PRIVATE (XAppIconChooserDialog, xapp_icon_chooser_dialog, XAPP_TYPE_GTK_WINDOW) + +static void on_category_selected (GtkListBox *list_box, + XAppIconChooserDialog *dialog); + +static void on_search_text_changed (GtkSearchEntry *entry, + XAppIconChooserDialog *dialog); + +static void on_icon_view_selection_changed (GtkIconView *icon_view, + gpointer user_data); + +static void on_icon_store_icons_added (GtkTreeModel *tree_model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data); + +static void on_browse_button_clicked (GtkButton *button, + gpointer user_data); + +static void on_select_button_clicked (GtkButton *button, + gpointer user_data); + +static void on_cancel_button_clicked (GtkButton *button, + gpointer user_data); + +static gboolean on_search_bar_key_pressed (GtkWidget *widget, + GdkEvent *event, + gpointer user_data); + +static gboolean on_delete_event (GtkWidget *widget, + GdkEventAny *event); + +static gboolean on_select_event (XAppIconChooserDialog *dialog, + GdkEventAny *event); + +static void on_icon_view_item_activated (GtkIconView *iconview, + GtkTreePath *path, + gpointer user_data); + +static void load_categories (XAppIconChooserDialog *dialog); + +static void load_icons_for_category (XAppIconChooserDialog *dialog, + IconCategoryInfo *category_info, + guint icon_size); + +static void search_path (XAppIconChooserDialog *dialog, + const gchar *path_string, + GtkListStore *icon_store); + +static void search_icon_name (XAppIconChooserDialog *dialog, + const gchar *name_string, + GtkListStore *icon_store); + +static gint list_box_sort (GtkListBoxRow *row1, + GtkListBoxRow *row2, + gpointer user_data); + +static gint search_model_sort (GtkTreeModel *model, + GtkTreeIter *a, + GtkTreeIter *b, + gpointer user_data); + +static void +free_category_info (IconCategoryInfo *category_info) +{ + g_list_free_full (category_info->icons, g_free); + + g_clear_object (&category_info->model); + + g_free (category_info); +} + +static void +free_file_info (FileIconInfoLoadCallbackInfo *file_info) +{ + g_object_unref (file_info->cancellable); + + g_object_unref (file_info->enumerator); + + g_free (file_info->short_name); + g_free (file_info->long_name); + + g_free (file_info); +} + +static void +free_named_info (NamedIconInfoLoadCallbackInfo *named_info) +{ + g_object_unref (named_info->cancellable); + + g_clear_object (&named_info->pixbuf); + + g_free (named_info); +} + +#if DEBUG_REFS +static void +on_cancellable_finalize (gpointer data, + GObject *object) +{ + g_printerr ("Cancellable Finalize: %p\n", object); +} + +static void +on_enumerator_finalize (gpointer data, + GObject *object) +{ + g_printerr ("Enumerator Finalize: %p\n", object); +} +#endif + +static void +on_enumerator_toggle_ref_called (gpointer data, + GObject *object, + gboolean is_last_ref) +{ + XAppIconChooserDialog *dialog; + XAppIconChooserDialogPrivate *priv; + + dialog = XAPP_ICON_CHOOSER_DIALOG (data); + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + if (is_last_ref) + { + g_object_remove_toggle_ref (object, + (GToggleNotify) on_enumerator_toggle_ref_called, + dialog); + + priv->search_file_enumerator = NULL; + } +} + +static void +clear_search_state (XAppIconChooserDialog *dialog) +{ + XAppIconChooserDialogPrivate *priv; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + + g_clear_object (&priv->search_file_enumerator); + + gtk_widget_hide (priv->loading_bar); + priv->search_iter = NULL; +} + +void +xapp_icon_chooser_dialog_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + XAppIconChooserDialog *dialog; + XAppIconChooserDialogPrivate *priv; + + dialog = XAPP_ICON_CHOOSER_DIALOG (object); + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + switch (prop_id) + { + case PROP_ICON_SIZE: + g_value_set_enum (value, priv->icon_size); + break; + case PROP_ALLOW_PATHS: + g_value_set_boolean (value, priv->allow_paths); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +void +xapp_icon_chooser_dialog_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + XAppIconChooserDialog *dialog; + XAppIconChooserDialogPrivate *priv; + + dialog = XAPP_ICON_CHOOSER_DIALOG (object); + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + switch (prop_id) + { + case PROP_ICON_SIZE: + priv->icon_size = g_value_get_enum (value); + break; + case PROP_ALLOW_PATHS: + priv->allow_paths = g_value_get_boolean (value); + if (priv->allow_paths) + { + gtk_widget_show (priv->browse_button); + gtk_widget_set_no_show_all (priv->browse_button, FALSE); + } + else + { + gtk_widget_hide (priv->browse_button); + gtk_widget_set_no_show_all (priv->browse_button, TRUE); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +xapp_icon_chooser_dialog_dispose (GObject *object) +{ + XAppIconChooserDialog *dialog; + XAppIconChooserDialogPrivate *priv; + + dialog = XAPP_ICON_CHOOSER_DIALOG (object); + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + if (priv->full_icon_list != NULL) + { + g_list_free_full (priv->full_icon_list, g_free); + priv->full_icon_list = NULL; + } + + if (priv->categories != NULL) + { + g_hash_table_destroy (priv->categories); + priv->categories = NULL; + } + + if (priv->pixbufs_by_name != NULL) + { + g_hash_table_destroy (priv->pixbufs_by_name); + priv->pixbufs_by_name = NULL; + } + + g_clear_pointer (&priv->icon_string, g_free); + g_clear_pointer (&priv->current_text, g_free); + g_clear_object (&priv->cancellable); + + G_OBJECT_CLASS (xapp_icon_chooser_dialog_parent_class)->dispose (object); +} + +static void +xapp_icon_chooser_dialog_init (XAppIconChooserDialog *dialog) +{ + XAppIconChooserDialogPrivate *priv; + GtkWidget *main_box; + GtkWidget *secondary_box; + GtkWidget *toolbar; + GtkWidget *overlay; + GtkWidget *spinner; + GtkWidget *spinner_label; + GtkWidget *loading_bar_box; + GtkToolItem *tool_item; + GtkWidget *toolbar_box; + GtkWidget *right_box; + GtkCellRenderer *renderer; + GtkTreeViewColumn *column; + GtkStyleContext *style; + GtkSizeGroup *button_size_group; + GtkWidget *cancel_button; + GtkWidget *button_area; + GtkWidget *selection; + GtkWidget *scrolled_window; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + priv->icon_size = XAPP_ICON_SIZE_32; + priv->categories = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) free_category_info); + + /* pixbufs_by_name will save pixbufs generated by icon name, so they can avoid the load_pixbuf call + * when they're reloaded (like re-selecting a previously selected category) */ + priv->pixbufs_by_name = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); + + priv->response = GTK_RESPONSE_NONE; + priv->icon_string = NULL; + priv->current_text = NULL; + priv->cancellable = NULL; + priv->allow_paths = TRUE; + priv->full_icon_list = NULL; + + priv->search_icon_store = gtk_list_store_new (3, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF); + + gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (priv->search_icon_store), + COLUMN_DISPLAY_NAME, + search_model_sort, + priv, + NULL); + gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (priv->search_icon_store), + COLUMN_DISPLAY_NAME, + GTK_SORT_ASCENDING); + + g_signal_connect (priv->search_icon_store, "row-inserted", + G_CALLBACK (on_icon_store_icons_added), dialog); + + gtk_window_set_default_size (GTK_WINDOW (dialog), 600, 450); + gtk_window_set_skip_taskbar_hint (GTK_WINDOW (dialog), TRUE); + gtk_window_set_type_hint (GTK_WINDOW (dialog), GDK_WINDOW_TYPE_HINT_DIALOG); + gtk_window_set_title (GTK_WINDOW (dialog), _("Choose an icon")); + + main_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add (GTK_CONTAINER (dialog), main_box); + + // toolbar + toolbar = gtk_toolbar_new (); + gtk_box_pack_start (GTK_BOX (main_box), toolbar, FALSE, FALSE, 0); + style = gtk_widget_get_style_context (toolbar); + gtk_style_context_add_class (style, "primary-toolbar"); + + tool_item = gtk_tool_item_new (); + gtk_toolbar_insert (GTK_TOOLBAR (toolbar), tool_item, 0); + gtk_tool_item_set_expand (tool_item, TRUE); + + toolbar_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_container_add (GTK_CONTAINER (tool_item), toolbar_box); + style = gtk_widget_get_style_context (GTK_WIDGET (toolbar_box)); + gtk_style_context_add_class (style, "linked"); + + priv->search_bar = gtk_search_entry_new (); + gtk_box_pack_start (GTK_BOX (toolbar_box), priv->search_bar, TRUE, TRUE, 0); + gtk_entry_set_placeholder_text (GTK_ENTRY (priv->search_bar), _("Search")); + + priv->search_changed_id = g_signal_connect (priv->search_bar, "search-changed", + G_CALLBACK (on_search_text_changed), dialog); + g_signal_connect (priv->search_bar, "key-press-event", + G_CALLBACK (on_search_bar_key_pressed), dialog); + + priv->browse_button = gtk_button_new_with_label (_("Browse")); + gtk_box_pack_start (GTK_BOX (toolbar_box), priv->browse_button, FALSE, FALSE, 0); + + g_signal_connect (priv->browse_button, "clicked", + G_CALLBACK (on_browse_button_clicked), dialog); + + secondary_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start (GTK_BOX (main_box), secondary_box, TRUE, TRUE, 0); + + // context list + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_box_pack_start (GTK_BOX (secondary_box), scrolled_window, FALSE, FALSE, 0); + + priv->list_box = gtk_list_box_new (); + gtk_container_add(GTK_CONTAINER (scrolled_window), GTK_WIDGET (priv->list_box)); + gtk_list_box_set_sort_func (GTK_LIST_BOX (priv->list_box), list_box_sort, NULL, NULL); + g_signal_connect (priv->list_box, "selected-rows-changed", + G_CALLBACK (on_category_selected), dialog); + + style = gtk_widget_get_style_context (GTK_WIDGET (scrolled_window)); + gtk_style_context_add_class (style, "sidebar"); + + right_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_box_pack_start (GTK_BOX (secondary_box), right_box, TRUE, TRUE, 0); + + overlay = gtk_overlay_new (); + gtk_box_pack_start (GTK_BOX (right_box), overlay, TRUE, TRUE, 0); + + // icon view + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_overlay_add_overlay (GTK_OVERLAY (overlay), scrolled_window); + gtk_widget_set_halign (scrolled_window, GTK_ALIGN_FILL); + gtk_widget_set_valign (scrolled_window, GTK_ALIGN_FILL); + + priv->loading_bar = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME (priv->loading_bar), GTK_SHADOW_NONE); + + gtk_style_context_add_class (gtk_widget_get_style_context (priv->loading_bar), + "background"); + + gtk_overlay_add_overlay (GTK_OVERLAY (overlay), priv->loading_bar); + gtk_widget_set_halign (priv->loading_bar, GTK_ALIGN_START); + gtk_widget_set_valign (priv->loading_bar, GTK_ALIGN_END); + gtk_widget_set_no_show_all (priv->loading_bar, TRUE); + + loading_bar_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_container_add (GTK_CONTAINER (priv->loading_bar), loading_bar_box); + g_object_set (loading_bar_box, + "margin", 4, + NULL); + + spinner = gtk_spinner_new (); + gtk_spinner_start (GTK_SPINNER (spinner)); + gtk_box_pack_start (GTK_BOX (loading_bar_box), spinner, FALSE, FALSE, 4); + + spinner_label = gtk_label_new (_("Loading...")); + gtk_box_pack_start (GTK_BOX (loading_bar_box), spinner_label, FALSE, FALSE, 4); + + gtk_widget_show_all (loading_bar_box); + + priv->icon_view = gtk_icon_view_new (); + gtk_container_add(GTK_CONTAINER (scrolled_window), GTK_WIDGET (priv->icon_view)); + + gtk_icon_view_set_pixbuf_column (GTK_ICON_VIEW (priv->icon_view), COLUMN_PIXBUF); + gtk_icon_view_set_text_column (GTK_ICON_VIEW (priv->icon_view), COLUMN_DISPLAY_NAME); + gtk_icon_view_set_tooltip_column (GTK_ICON_VIEW (priv->icon_view), COLUMN_FULL_NAME); + + g_signal_connect (priv->icon_view, "selection-changed", + G_CALLBACK (on_icon_view_selection_changed), dialog); + g_signal_connect (priv->icon_view, "item-activated", + G_CALLBACK (on_icon_view_item_activated), dialog); + + // buttons + button_area = gtk_action_bar_new (); + priv->action_area = button_area; + gtk_box_pack_start (GTK_BOX (main_box), button_area, FALSE, FALSE, 0); + + button_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + priv->select_button = gtk_button_new_with_label (_("Select")); + style = gtk_widget_get_style_context (GTK_WIDGET (priv->select_button)); + gtk_style_context_add_class (style, "text-button"); + gtk_size_group_add_widget (button_size_group, priv->select_button); + gtk_action_bar_pack_end (GTK_ACTION_BAR (button_area), priv->select_button); + + g_signal_connect (priv->select_button, "clicked", + G_CALLBACK (on_select_button_clicked), dialog); + + cancel_button = gtk_button_new_with_label (_("Cancel")); + style = gtk_widget_get_style_context (GTK_WIDGET (cancel_button)); + gtk_style_context_add_class (style, "text-button"); + gtk_size_group_add_widget (button_size_group, cancel_button); + gtk_action_bar_pack_end (GTK_ACTION_BAR (button_area), cancel_button); + + g_signal_connect (cancel_button, "clicked", + G_CALLBACK (on_cancel_button_clicked), dialog); + + load_categories (dialog); +} + +static void +xapp_icon_chooser_dialog_class_init (XAppIconChooserDialogClass *klass) +{ + GtkBindingSet *binding_set; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = xapp_icon_chooser_dialog_get_property; + object_class->set_property = xapp_icon_chooser_dialog_set_property; + object_class->dispose = xapp_icon_chooser_dialog_dispose; + + widget_class->delete_event = on_delete_event; + + /** + * XAppIconChooserDialog:icon-size: + * + * The preferred size to use when looking up icons. This only works with icon names. + * Additionally, there is no guarantee that a selected icon name will exist in a + * particular size. + */ + obj_properties[PROP_ICON_SIZE] = + g_param_spec_enum ("icon-size", + _("Icon size"), + _("The preferred icon size."), + XAPP_TYPE_ICON_SIZE, + XAPP_ICON_SIZE_32, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * XAppIconChooserDialog:allow-paths: + * + * Whether to allow paths to be searched and selected or only icon names. + */ + obj_properties[PROP_ALLOW_PATHS] = + g_param_spec_boolean ("allow-paths", + _("Allow Paths"), + _("Whether to allow paths."), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPERTIES, obj_properties); + + // keybinding signals + signals[CLOSE] = + g_signal_new ("close", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (GtkWidgetClass, delete_event), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + signals[SELECT] = + g_signal_new ("select", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + binding_set = gtk_binding_set_by_class (klass); + gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "close", 0); + gtk_binding_entry_add_signal (binding_set, GDK_KEY_Return, 0, "select", 0); + gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Enter, 0, "select", 0); + + gtk_widget_class_set_css_name (widget_class, "stacksidebar"); +} + +/** + * xapp_icon_chooser_dialog_new: + * + * Creates a new #XAppIconChooserDialog. + * + * Returns: a newly created #XAppIconChooserDialog + */ +XAppIconChooserDialog * +xapp_icon_chooser_dialog_new (void) +{ + return g_object_new (XAPP_TYPE_ICON_CHOOSER_DIALOG, NULL); +} + +/** + * xapp_icon_chooser_dialog_run: + * @dialog: a #XAppIconChooserDialog + * + * Shows the dialog and enters a separate main loop until an icon is chosen or the action is canceled. + * + * xapp_icon_chooser_dialog_run (), xapp_icon_chooser_dialog_run_with_icon(), and + * xapp_icon_chooser_dialog_run_with_category () may all be called multiple times. This is useful for + * applications which use this dialog multiple times, as it may improve performance for subsequent + * calls. + * + * Returns: GTK_RESPONSE_OK if the user selected an icon, or GTK_RESPONSE_CANCEL otherwise + */ +gint +xapp_icon_chooser_dialog_run (XAppIconChooserDialog *dialog) +{ + XAppIconChooserDialogPrivate *priv; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + gtk_widget_show_all (GTK_WIDGET (dialog)); + gtk_widget_grab_focus (priv->search_bar); + + gtk_main (); + + return priv->response; +} + +/** + * xapp_icon_chooser_dialog_run_with_icon: + * @dialog: a #XAppIconChooserDialog + * @icon: a string representing the icon that should be selected + * + * Like xapp_icon_chooser_dialog_run but selects the icon specified by @icon. This can be either an + * icon name or a path. Passing an icon string or path that doesn't exist is accepted, but it may show + * multiple results, or none at all. This behavior is useful if, for example, you wish to have the + * user select an image file from a particular directory. + * + * If the property allow_paths is FALSE, setting a path will yield no results when the dialog is opened. + * + * xapp_icon_chooser_dialog_run (), xapp_icon_chooser_dialog_run_with_icon(), and + * xapp_icon_chooser_dialog_run_with_category () may all be called multiple times. This is useful for + * applications which use this dialog multiple times, as it may improve performance for subsequent + * calls. + * + * Returns: GTK_RESPONSE_OK if the user selected an icon, or GTK_RESPONSE_CANCEL otherwise + */ +gint +xapp_icon_chooser_dialog_run_with_icon (XAppIconChooserDialog *dialog, + gchar *icon) +{ + XAppIconChooserDialogPrivate *priv; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + gtk_widget_show_all (GTK_WIDGET (dialog)); + gtk_entry_set_text (GTK_ENTRY (priv->search_bar), icon); + gtk_widget_grab_focus (priv->search_bar); + + gtk_main (); + + return priv->response; +} + +/** + * xapp_icon_chooser_dialog_run_with_category: + * @dialog: a #XAppIconChooserDialog + * + * Like xapp_icon_chooser_dialog_run but selects a particular category specified by @category. + * This is used when there is a particular category of icon that is more appropriate than the + * others. If the category does not exist, the first category in the list will be selected. To + * get a list of possible categories, use gtk_icon_theme_list_contexts (). + * + * xapp_icon_chooser_dialog_run (), xapp_icon_chooser_dialog_run_with_icon(), and + * xapp_icon_chooser_dialog_run_with_category () may all be called multiple times. This is useful for + * applications which use this dialog multiple times, as it may improve performance for subsequent + * calls. + * + * Returns: GTK_RESPONSE_OK if the user selected an icon, or GTK_RESPONSE_CANCEL otherwise + */ +gint +xapp_icon_chooser_dialog_run_with_category (XAppIconChooserDialog *dialog, + gchar *category) +{ + XAppIconChooserDialogPrivate *priv; + GList *children; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + gtk_widget_show_all (GTK_WIDGET (dialog)); + gtk_widget_grab_focus (priv->search_bar); + + children = gtk_container_get_children (GTK_CONTAINER (priv->list_box)); + for ( ; children; children = children->next) + { + GtkWidget *row; + GtkWidget *child; + const gchar *context; + + row = children->data; + child = gtk_bin_get_child (GTK_BIN (row)); + context = gtk_label_get_text (GTK_LABEL (child)); + if (g_strcmp0 (context, category) == 0) + { + gtk_list_box_select_row (GTK_LIST_BOX (priv->list_box), GTK_LIST_BOX_ROW (row)); + break; + } + } + + gtk_main (); + + return priv->response; +} + +/** + * xapp_icon_chooser_dialog_get_icon_string: + * @dialog: a #XAppIconChooserDialog + * + * Gets the currently selected icon from the dialog. If allow-paths is TRUE, this function may return + * either an icon name or a path depending on what the user selects. Otherwise it will only return an + * icon name. + * + * Returns: (transfer full): the string representation of the currently selected icon or NULL + * if no icon is selected. + */ +gchar * +xapp_icon_chooser_dialog_get_icon_string (XAppIconChooserDialog *dialog) +{ + XAppIconChooserDialogPrivate *priv; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + return g_strdup (priv->icon_string); +} + +static void +xapp_icon_chooser_dialog_close (XAppIconChooserDialog *dialog, + GtkResponseType response) +{ + XAppIconChooserDialogPrivate *priv; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + priv->response = response; + gtk_widget_hide (GTK_WIDGET (dialog)); + + gtk_main_quit (); +} + +static void +on_custom_button_clicked (GtkButton *button, + gpointer user_data) +{ + GtkResponseType response_id; + + response_id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "response-id")); + + xapp_icon_chooser_dialog_close (XAPP_ICON_CHOOSER_DIALOG (user_data), response_id); +} + +/** + * xapp_icon_chooser_dialog_add_button: + * @dialog: an #XAppIconChooserDialog + * @button: a #GtkButton to add + * @packing: the #GtkPackType to specify start or end packing to the action bar + * @response_id: the dialog response id to return when this button is clicked. + * + * Allows a button to be added to the #GtkActionBar of the dialog with a custom + * response id. + */ +void +xapp_icon_chooser_dialog_add_button (XAppIconChooserDialog *dialog, + GtkWidget *button, + GtkPackType packing, + GtkResponseType response_id) +{ + XAppIconChooserDialogPrivate *priv; + GtkWidget *action_area; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + g_signal_connect (button, + "clicked", + G_CALLBACK (on_custom_button_clicked), + dialog); + + /* This saves having to use a custom container for callback data. */ + g_object_set_data (G_OBJECT (button), + "response-id", GINT_TO_POINTER (response_id)); + + if (packing == GTK_PACK_START) + { + gtk_action_bar_pack_start (GTK_ACTION_BAR (priv->action_area), button); + } + else + { + gtk_action_bar_pack_start (GTK_ACTION_BAR (priv->action_area), button); + } +} + +static gint +list_box_sort (GtkListBoxRow *row1, + GtkListBoxRow *row2, + gpointer user_data) +{ + GtkWidget *item; + const gchar *label1; + const gchar *label2; + + item = gtk_bin_get_child (GTK_BIN (row1)); + label1 = gtk_label_get_text (GTK_LABEL (item)); + + item = gtk_bin_get_child (GTK_BIN (row2)); + label2 = gtk_label_get_text (GTK_LABEL (item)); + + return g_strcmp0 (label1, label2); +} + +static gint +search_model_sort (GtkTreeModel *model, + GtkTreeIter *a, + GtkTreeIter *b, + gpointer user_data) +{ + gchar *a_value; + gchar *b_value; + gchar *search_str; + gboolean a_starts_with; + gboolean b_starts_with; + gint ret; + + XAppIconChooserDialogPrivate *priv = (XAppIconChooserDialogPrivate *) user_data; + + search_str = priv->current_text; + + gtk_tree_model_get (model, a, COLUMN_DISPLAY_NAME, &a_value, -1); + gtk_tree_model_get (model, b, COLUMN_DISPLAY_NAME, &b_value, -1); + + ret = g_strcmp0 (a_value, b_value); + + if (search_str == NULL) + { + } + else + if (g_strcmp0 (a_value, search_str) == 0) + { + ret = -1; + } + else + if (g_strcmp0 (b_value, search_str) == 0) + { + ret = 1; + } + else + { + a_starts_with = g_str_has_prefix (a_value, search_str); + b_starts_with = g_str_has_prefix (b_value, search_str); + + if (a_starts_with && !b_starts_with) + { + ret = -1; + } + + if (!a_starts_with && b_starts_with) + { + ret = 1; + } + } + + g_free (a_value); + g_free (b_value); + + return ret; +} + +static void +load_categories (XAppIconChooserDialog *dialog) +{ + XAppIconChooserDialogPrivate *priv; + GtkListBoxRow *row; + GtkIconTheme *theme; + GList *contexts, *l; + gint i; + gint j; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + theme = gtk_icon_theme_get_default (); + contexts = gtk_icon_theme_list_contexts (theme); + + for (i = 0; i < G_N_ELEMENTS (categories); i++) + { + IconCategoryDefinition *category; + IconCategoryInfo *category_info; + GtkWidget *row; + GtkWidget *label; + GList *context_icons; + + category = &categories[i]; + + category_info = g_new0 (IconCategoryInfo, 1); + + category_info->name = _(category->name); + + for (j = 0; category->contexts[j] != NULL; j++) + { + GList *match; + + context_icons = gtk_icon_theme_list_icons (theme, category->contexts[j]); + category_info->icons = g_list_concat (category_info->icons, context_icons); + + match = g_list_find_custom (contexts, category->contexts[j], (GCompareFunc) g_strcmp0); + + if (match) + { + contexts = g_list_remove_link (contexts, match); + g_free (match->data); + g_list_free (match); + } + } + + /* Any contexts not consumed by categories should be added to the 'other' category */ + if (i == (G_N_ELEMENTS (categories) - 1) && g_list_length (contexts) > 0) + { + for (l = contexts; l != NULL; l = l->next) + { + +#if DEBUG_ICON_THEME + g_message ("Adding unused category to Other category: '%s'", (gchar *) l->data); +#endif + context_icons = gtk_icon_theme_list_icons (theme, (gchar *) l->data); + + category_info->icons = g_list_concat (category_info->icons, context_icons); + } + } + + if (g_list_length (category_info->icons) == 0) + { + free_category_info (category_info); + + continue; + } + + /* Add the list of icons for this category into our master search list */ + priv->full_icon_list = g_list_concat (priv->full_icon_list, + g_list_copy_deep (category_info->icons, (GCopyFunc) g_strdup, NULL)); + + category_info->model = gtk_list_store_new (3, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF); + g_signal_connect (category_info->model, "row-inserted", + G_CALLBACK (on_icon_store_icons_added), dialog); + + category_info->icons = g_list_sort (category_info->icons, (GCompareFunc) g_utf8_collate); + + row = gtk_list_box_row_new (); + label = gtk_label_new (category_info->name); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_widget_set_margin_start (GTK_WIDGET (label), 6); + gtk_widget_set_margin_end (GTK_WIDGET (label), 6); + + gtk_container_add (GTK_CONTAINER (row), label); + gtk_container_add (GTK_CONTAINER (priv->list_box), row); + + g_hash_table_insert (priv->categories, row, category_info); + } + + g_list_free_full (contexts, g_free); + + priv->full_icon_list = g_list_sort (priv->full_icon_list, (GCompareFunc) g_utf8_collate); + + row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (priv->list_box), 0); + gtk_list_box_select_row (GTK_LIST_BOX (priv->list_box), row); +} + +GdkPixbuf * +wrangle_pixbuf_size (GdkPixbuf *pixbuf, + gint icon_size) +{ + gint width, height, new_width, new_height; + GdkPixbuf *out_pixbuf; + + new_width = new_height = -1; + + width = gdk_pixbuf_get_width (pixbuf); + height = gdk_pixbuf_get_height (pixbuf); + + if ((width > height ? width : height) > icon_size) + { + if (width > icon_size) + { + new_width = icon_size; + new_height = floor (((float) height / width) * new_width); + } + else if (height > icon_size) + { + new_height = icon_size; + new_width = floor (((float) width / height) * new_height); + } + + out_pixbuf = gdk_pixbuf_scale_simple (pixbuf, + new_width, + new_height, + GDK_INTERP_BILINEAR); + } + else + { + out_pixbuf = g_object_ref (pixbuf); + } + + return out_pixbuf; +} + +static gboolean +load_next_file_search_chunk (gpointer user_data) +{ + XAppIconChooserDialogPrivate *priv; + FileIconInfoLoadCallbackInfo *callback_info; + + callback_info = (FileIconInfoLoadCallbackInfo*) user_data; + priv = xapp_icon_chooser_dialog_get_instance_private (callback_info->dialog); + + if (g_cancellable_is_cancelled (callback_info->cancellable)) + { + free_file_info (callback_info); + + return G_SOURCE_REMOVE; + } + + search_path (callback_info->dialog, + priv->current_text, + priv->search_icon_store); + + free_file_info (callback_info); + + return G_SOURCE_REMOVE; +} + +static gboolean +load_next_category_chunk (gpointer user_data) +{ + XAppIconChooserDialogPrivate *priv; + NamedIconInfoLoadCallbackInfo *callback_info; + + callback_info = (NamedIconInfoLoadCallbackInfo*) user_data; + priv = xapp_icon_chooser_dialog_get_instance_private (callback_info->dialog); + + if (g_cancellable_is_cancelled (callback_info->cancellable)) + { + free_named_info (callback_info); + + return G_SOURCE_REMOVE; + } + + load_icons_for_category (callback_info->dialog, + callback_info->category_info, + priv->icon_size); + + free_named_info (callback_info); + + return G_SOURCE_REMOVE; +} + +static gboolean +load_next_name_search_chunk (gpointer user_data) +{ + XAppIconChooserDialogPrivate *priv; + NamedIconInfoLoadCallbackInfo *callback_info; + + callback_info = (NamedIconInfoLoadCallbackInfo*) user_data; + priv = xapp_icon_chooser_dialog_get_instance_private (callback_info->dialog); + + if (g_cancellable_is_cancelled (callback_info->cancellable)) + { + free_named_info (callback_info); + + return G_SOURCE_REMOVE; + } + + search_icon_name (callback_info->dialog, + priv->current_text, + priv->search_icon_store); + + free_named_info (callback_info); + + return G_SOURCE_REMOVE; +} + +static void +finish_pixbuf_load_from_file (GObject *stream, + GAsyncResult *res, + gpointer *user_data) +{ + XAppIconChooserDialogPrivate *priv; + FileIconInfoLoadCallbackInfo *callback_info; + GdkPixbuf *pixbuf, *final_pixbuf; + GError *error = NULL; + GtkTreeIter iter; + + callback_info = (FileIconInfoLoadCallbackInfo *) user_data; + priv = xapp_icon_chooser_dialog_get_instance_private (callback_info->dialog); + + pixbuf = gdk_pixbuf_new_from_stream_finish (res, &error); + + g_input_stream_close (G_INPUT_STREAM (stream), NULL, NULL); + g_object_unref (stream); + + if (g_cancellable_is_cancelled (callback_info->cancellable)) + { + g_clear_object (&pixbuf); + free_file_info (callback_info); + + return; + } + + if (pixbuf == NULL) + { + if (error && (error->domain != G_IO_ERROR || error->code != G_IO_ERROR_CANCELLED)) + { + g_message ("%s\n", error->message); + } + + final_pixbuf = NULL; + g_clear_error (&error); + } + else + { + final_pixbuf = wrangle_pixbuf_size (pixbuf, priv->icon_size); + g_object_unref (pixbuf); + } + + if (final_pixbuf) + { + GtkTreeIter iter; + + gtk_list_store_append (callback_info->model, &iter); + gtk_list_store_set (callback_info->model, &iter, + COLUMN_DISPLAY_NAME, callback_info->short_name, + COLUMN_FULL_NAME, callback_info->long_name, + COLUMN_PIXBUF, final_pixbuf, + -1); + + g_object_unref (final_pixbuf); + } + + if (callback_info->chunk_end) + { + g_idle_add ((GSourceFunc) load_next_file_search_chunk, callback_info); + } + else + { + free_file_info (callback_info); + } + +} + +static void +add_named_entry (NamedIconInfoLoadCallbackInfo *callback_info, + GdkPixbuf *pixbuf) +{ + GtkTreeIter iter; + + gtk_list_store_append (callback_info->model, &iter); + gtk_list_store_set (callback_info->model, &iter, + COLUMN_DISPLAY_NAME, callback_info->name, + COLUMN_FULL_NAME, callback_info->name, + COLUMN_PIXBUF, pixbuf, + -1); +} + +static gboolean +add_named_entry_with_existing_pixbuf (gpointer user_data) +{ + NamedIconInfoLoadCallbackInfo *callback_info; + + callback_info = (NamedIconInfoLoadCallbackInfo*) user_data; + + if (g_cancellable_is_cancelled (callback_info->cancellable)) + { + free_named_info (callback_info); + + return G_SOURCE_REMOVE; + } + + /* Category results have a category_info attached. */ + if (callback_info->category_info) + { + add_named_entry (callback_info, callback_info->pixbuf); + + if (callback_info->chunk_end) + { + g_idle_add ((GSourceFunc) load_next_category_chunk, callback_info); + + return G_SOURCE_REMOVE; + } + } + /* Otherwise, it's a search result set */ + else + { + add_named_entry (callback_info, callback_info->pixbuf); + + if (callback_info->chunk_end) + { + g_idle_add ((GSourceFunc) load_next_name_search_chunk, callback_info); + + return G_SOURCE_REMOVE; + } + } + + free_named_info (callback_info); + + return G_SOURCE_REMOVE; +} + +static void +finish_pixbuf_load_from_name (GObject *info, + GAsyncResult *res, + gpointer *user_data) +{ + XAppIconChooserDialogPrivate *priv; + NamedIconInfoLoadCallbackInfo *callback_info; + GdkPixbuf *pixbuf; + GError *error = NULL; + + callback_info = (NamedIconInfoLoadCallbackInfo*) user_data; + priv = xapp_icon_chooser_dialog_get_instance_private (callback_info->dialog); + + pixbuf = gtk_icon_info_load_icon_finish (GTK_ICON_INFO (info), res, &error); + g_object_unref (info); + + if (g_cancellable_is_cancelled (callback_info->cancellable)) + { + g_clear_object (&pixbuf); + + free_named_info (callback_info); + + return; + } + + if (pixbuf == NULL) + { + if (error && (error->domain != G_IO_ERROR || error->code != G_IO_ERROR_CANCELLED)) + { + g_message ("%s\n", error->message); + } + + free_named_info (callback_info); + + g_clear_error (&error); + + return; + } + + /* Hash table 'takes' reference, we don't have to free pixbuf. + callback_info->name is already owned by priv->full_icon_list so + it needs to be copied */ + g_hash_table_insert (priv->pixbufs_by_name, + g_strdup (callback_info->name), + (gpointer) pixbuf); + + /* If there's a category_info, this is a category selection. */ + if (callback_info->category_info) + { + add_named_entry (callback_info, pixbuf); + + if (callback_info->chunk_end) + { + g_idle_add ((GSourceFunc) load_next_category_chunk, callback_info); + + return; + } + } + /* Otherwise, it's a search result set */ + else + { + add_named_entry (callback_info, pixbuf); + + if (callback_info->chunk_end) + { + g_idle_add ((GSourceFunc) load_next_name_search_chunk, callback_info); + + return; + } + } + + free_named_info (callback_info); +} + +#define CATEGORY_CHUNK_SIZE 500 + +static void +load_icons_for_category (XAppIconChooserDialog *dialog, + IconCategoryInfo *category_info, + guint icon_size) +{ + XAppIconChooserDialogPrivate *priv; + GtkIconTheme *theme; + gint chunk_count; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + theme = gtk_icon_theme_get_default (); + + for (chunk_count = 0; chunk_count < CATEGORY_CHUNK_SIZE; chunk_count++) + { + if (category_info->iter == NULL) + { + category_info->iter = category_info->icons; + } + else + { + category_info->iter = category_info->iter->next; + } + + if (category_info->iter) + { + GdkPixbuf *pixbuf; + NamedIconInfoLoadCallbackInfo *callback_info; + const gchar *name = category_info->iter->data; + + callback_info = g_new0 (NamedIconInfoLoadCallbackInfo, 1); + callback_info->dialog = dialog; + callback_info->category_info = category_info; + callback_info->model = category_info->model; + callback_info->cancellable = g_object_ref (priv->cancellable); + callback_info->name = name; + callback_info->chunk_end = (chunk_count == CATEGORY_CHUNK_SIZE - 1); + + pixbuf = g_hash_table_lookup (priv->pixbufs_by_name, name); + + if (pixbuf != NULL) + { + callback_info->pixbuf = g_object_ref (pixbuf); + g_idle_add ((GSourceFunc) add_named_entry_with_existing_pixbuf, callback_info); + } + else + { + GtkIconInfo *info; + + info = gtk_icon_theme_lookup_icon (theme, name, icon_size, GTK_ICON_LOOKUP_FORCE_SIZE); + gtk_icon_info_load_icon_async (info, NULL, (GAsyncReadyCallback) (finish_pixbuf_load_from_name), callback_info); + } + } + else + { + gtk_widget_hide (priv->loading_bar); + break; // Quit the count early, we're out of data + } + } +} + +static void +on_category_selected (GtkListBox *list_box, + XAppIconChooserDialog *dialog) +{ + XAppIconChooserDialogPrivate *priv; + GList *selection; + GtkWidget *selected; + IconCategoryInfo *category_info; + GtkTreePath *new_path; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + clear_search_state (dialog); + + selection = gtk_list_box_get_selected_rows (GTK_LIST_BOX (priv->list_box)); + + if (!selection) + { + return; + } + + gtk_widget_show (priv->loading_bar); + + g_signal_handler_block (priv->search_bar, priv->search_changed_id); + gtk_entry_set_text (GTK_ENTRY (priv->search_bar), ""); + g_signal_handler_unblock (priv->search_bar, priv->search_changed_id); + + selected = selection->data; + category_info = g_hash_table_lookup (priv->categories, selected); + + priv->cancellable = g_cancellable_new (); + +#if DEBUG_REFS + g_object_weak_ref (G_OBJECT (priv->cancellable), (GWeakNotify) on_cancellable_finalize, priv); +#endif + + priv->current_category = category_info; + + gtk_list_store_clear (GTK_LIST_STORE (category_info->model)); + gtk_icon_view_set_model (GTK_ICON_VIEW (priv->icon_view), GTK_TREE_MODEL (category_info->model)); + + load_icons_for_category (dialog, + category_info, + priv->icon_size); + + gtk_adjustment_set_value (gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (priv->icon_view)), 0.0); + + g_list_free (selection); +} + +#define SEARCH_CHUNK_SIZE 2 + +static void +search_path (XAppIconChooserDialog *dialog, + const gchar *path_string, + GtkListStore *icon_store) +{ + XAppIconChooserDialogPrivate *priv; + gchar *search_str = NULL; + GFile *dir; + GFileInfo *child_info = NULL;; + gint chunk_count; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + if (g_file_test (path_string, G_FILE_TEST_IS_DIR)) + { + dir = g_file_new_for_path (path_string); + } + else + { + GFile *file; + + file = g_file_new_for_path (path_string); + dir = g_file_get_parent (file); + search_str = g_file_get_basename (file); + g_object_unref (file); + } + + if (!g_file_query_exists (dir, NULL) || + g_file_query_file_type (dir, G_FILE_QUERY_INFO_NONE, NULL) != G_FILE_TYPE_DIRECTORY) + { + g_free (search_str); + g_object_unref (dir); + return; + } + + if (!priv->search_file_enumerator) + { + priv->search_file_enumerator = g_file_enumerate_children (dir, + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," + G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE "," + G_FILE_ATTRIBUTE_STANDARD_SIZE "," + G_FILE_ATTRIBUTE_STANDARD_NAME, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + + g_object_add_toggle_ref (G_OBJECT (priv->search_file_enumerator), + (GToggleNotify) on_enumerator_toggle_ref_called, + dialog); + +#if DEBUG_REFS + g_object_weak_ref (G_OBJECT (priv->search_file_enumerator), (GWeakNotify) on_enumerator_finalize, dialog); +#endif + } + + chunk_count = 0; + + while (chunk_count < SEARCH_CHUNK_SIZE) + { + const gchar *child_name; + gchar *child_path; + GFile *child; + GError *error = NULL; + + g_clear_object (&child_info); + child_info = g_file_enumerator_next_file (priv->search_file_enumerator, NULL, NULL); + + if (!child_info) + { + break; + } + + child_name = g_file_info_get_name (child_info); + child = g_file_enumerator_get_child (priv->search_file_enumerator, child_info); + child_path = g_file_get_path (child); + + if (search_str == NULL || g_str_has_prefix (child_name, search_str)) + { + priv->current_category = NULL; + + gchar *content_type; + gboolean uncertain; + + content_type = g_content_type_guess (child_name, NULL, 0, &uncertain); + + if (content_type && g_str_has_prefix (content_type, "image") && !uncertain) + { + GFileInputStream *stream; + + stream = g_file_read (child, NULL, &error); + + if (stream != NULL) + { + FileIconInfoLoadCallbackInfo *callback_info; + + callback_info = g_new0 (FileIconInfoLoadCallbackInfo, 1); + callback_info->dialog = dialog; + callback_info->model = icon_store; + callback_info->cancellable = g_object_ref (priv->cancellable); + callback_info->enumerator = g_object_ref (priv->search_file_enumerator); + callback_info->short_name = g_strdup (child_name); + callback_info->long_name = g_strdup (child_path); + callback_info->chunk_end = (chunk_count == SEARCH_CHUNK_SIZE - 1); + + gdk_pixbuf_new_from_stream_async (G_INPUT_STREAM (stream), + NULL, + (GAsyncReadyCallback) finish_pixbuf_load_from_file, + callback_info); + + chunk_count ++; + } + else + { + if (error) + { + g_message ("%s\n", error->message); + g_error_free (error); + } + } + } + + g_free (content_type); + } + + g_free (child_path); + g_object_unref (child); + } + + if (!child_info) + { + if (priv->search_file_enumerator) + { + g_object_unref (priv->search_file_enumerator); + } + + gtk_widget_hide (priv->loading_bar); + } + else + { + g_clear_object (&child_info); + } + + g_free (search_str); + g_object_unref (dir); +} + +static void +search_icon_name (XAppIconChooserDialog *dialog, + const gchar *name_string, + GtkListStore *icon_store) +{ + XAppIconChooserDialogPrivate *priv; + GtkIconTheme *theme; + GList *icons; + GtkIconInfo *info; + NamedIconInfoLoadCallbackInfo *callback_info; + gint chunk_count; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + theme = gtk_icon_theme_get_default (); + + icons = priv->full_icon_list; + + chunk_count = 0; + + while (chunk_count < SEARCH_CHUNK_SIZE) + { + if (priv->search_iter == NULL) + { + priv->search_iter = icons; + } + else + { + priv->search_iter = priv->search_iter->next; + } + + if (priv->search_iter) + { + priv->current_category = NULL; + + if (g_strrstr (priv->search_iter->data, name_string)) + { + GdkPixbuf *pixbuf; + NamedIconInfoLoadCallbackInfo *callback_info; + const gchar *name = priv->search_iter->data; + + callback_info = g_new0 (NamedIconInfoLoadCallbackInfo, 1); + callback_info->dialog = dialog; + callback_info->model = priv->search_icon_store; + callback_info->cancellable = g_object_ref (priv->cancellable); + callback_info->category_info = NULL; + callback_info->name = name; + callback_info->chunk_end = (chunk_count == SEARCH_CHUNK_SIZE - 1); + + pixbuf = g_hash_table_lookup (priv->pixbufs_by_name, name); + + if (pixbuf != NULL) + { + callback_info->pixbuf = g_object_ref (pixbuf); + g_idle_add ((GSourceFunc) add_named_entry_with_existing_pixbuf, callback_info); + } + else + { + GtkIconInfo *info; + + info = gtk_icon_theme_lookup_icon (theme, name, priv->icon_size, GTK_ICON_LOOKUP_FORCE_SIZE); + gtk_icon_info_load_icon_async (info, NULL, (GAsyncReadyCallback) (finish_pixbuf_load_from_name), callback_info); + } + + chunk_count++; + } + } + else + { + gtk_widget_hide (priv->loading_bar); + + break; // Quit the count early, we're out of data + } + } +} + +static void +on_search_text_changed (GtkSearchEntry *entry, + XAppIconChooserDialog *dialog) +{ + XAppIconChooserDialogPrivate *priv; + const gchar *search_text; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + /* The search cancellable is carried in search callback data. If the + * text changes, we cancel it here, our load callbacks will check the + * state, and react appropriately (like not adding results to the model). */ + clear_search_state (dialog); + + gtk_list_box_select_row (GTK_LIST_BOX (priv->list_box), NULL); + + priv->cancellable = g_cancellable_new (); + +#if DEBUG_REFS + g_object_weak_ref (G_OBJECT (priv->cancellable), (GWeakNotify) on_cancellable_finalize, dialog); +#endif + + search_text = gtk_entry_get_text (GTK_ENTRY (entry)); + + if (g_strcmp0 (search_text, "") == 0) + { + g_clear_pointer (&priv->current_text, g_free); + g_clear_pointer (&priv->icon_string, g_free); + + gtk_widget_hide (priv->loading_bar); + + gtk_list_store_clear (GTK_LIST_STORE (priv->search_icon_store)); + } + else + if (strlen (search_text) < 2) + { + return; + } + else + { + g_free (priv->current_text); + priv->current_text = g_strdup (search_text); + + gtk_widget_show (priv->loading_bar); + + gtk_list_store_clear (GTK_LIST_STORE (priv->search_icon_store)); + gtk_icon_view_set_model (GTK_ICON_VIEW (priv->icon_view), GTK_TREE_MODEL (priv->search_icon_store)); + if (g_strrstr (search_text, "/")) + { + if (priv->allow_paths) + { + search_path (dialog, search_text, priv->search_icon_store); + } + } + else + { + search_icon_name (dialog, search_text, priv->search_icon_store); + } + } +} + +static void +on_icon_view_selection_changed (GtkIconView *icon_view, + gpointer user_data) +{ + XAppIconChooserDialogPrivate *priv; + GList *selected_items; + gchar *icon_string = NULL; + + priv = xapp_icon_chooser_dialog_get_instance_private (user_data); + + selected_items = gtk_icon_view_get_selected_items (icon_view); + if (selected_items == NULL) + { + gtk_widget_set_sensitive (GTK_WIDGET (priv->select_button), FALSE); + } + else + { + GtkTreePath *tree_path; + GtkTreeModel *model; + GtkTreeIter iter; + + gtk_widget_set_sensitive (GTK_WIDGET (priv->select_button), TRUE); + + tree_path = selected_items->data; + model = gtk_icon_view_get_model (icon_view); + gtk_tree_model_get_iter (model, &iter, tree_path); + gtk_tree_model_get (model, &iter, COLUMN_FULL_NAME, &icon_string, -1); + } + + if (priv->icon_string != NULL) + { + g_free (priv->icon_string); + } + + priv->icon_string = icon_string; + + g_list_free_full (selected_items, (GDestroyNotify) gtk_tree_path_free); +} + +static void +on_icon_store_icons_added (GtkTreeModel *tree_model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + XAppIconChooserDialogPrivate *priv; + GtkTreePath *new_path; + + priv = xapp_icon_chooser_dialog_get_instance_private (user_data); + + if (tree_model != gtk_icon_view_get_model (GTK_ICON_VIEW (priv->icon_view))) { + return; + } + + new_path = gtk_tree_path_new_first (); + gtk_icon_view_select_path (GTK_ICON_VIEW (priv->icon_view), new_path); + + gtk_tree_path_free (new_path); +} + +static void +on_browse_button_clicked (GtkButton *button, + gpointer user_data) +{ + XAppIconChooserDialog *dialog = user_data; + XAppIconChooserDialogPrivate *priv; + GtkWidget *file_dialog; + const gchar *search_text; + GtkFileFilter *file_filter; + gint response; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + file_dialog = gtk_file_chooser_dialog_new (_("Select image file"), + GTK_WINDOW (dialog), + GTK_FILE_CHOOSER_ACTION_OPEN, + _("Cancel"), + GTK_RESPONSE_CANCEL, + _("Open"), + GTK_RESPONSE_ACCEPT, + NULL); + + search_text = gtk_entry_get_text (GTK_ENTRY (priv->search_bar)); + if (g_strrstr (search_text, "/")) + { + gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (file_dialog), search_text); + } + else + { + gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (file_dialog), "/usr/share/icons/"); + } + + file_filter = gtk_file_filter_new (); + gtk_file_filter_set_name (file_filter, _("Image")); + gtk_file_filter_add_pixbuf_formats (file_filter); + gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (file_dialog), file_filter); + + response = gtk_dialog_run (GTK_DIALOG (file_dialog)); + if (response == GTK_RESPONSE_ACCEPT) + { + gchar *filename; + + filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (file_dialog)); + gtk_entry_set_text (GTK_ENTRY (priv->search_bar), filename); + g_free (filename); + } + + gtk_widget_destroy (file_dialog); +} + +static void +on_select_button_clicked (GtkButton *button, + gpointer user_data) +{ + xapp_icon_chooser_dialog_close (XAPP_ICON_CHOOSER_DIALOG (user_data), GTK_RESPONSE_OK); +} + +static void +on_cancel_button_clicked (GtkButton *button, + gpointer user_data) +{ + xapp_icon_chooser_dialog_close (XAPP_ICON_CHOOSER_DIALOG (user_data), GTK_RESPONSE_CANCEL); +} + +static gboolean +on_delete_event (GtkWidget *widget, + GdkEventAny *event) +{ + xapp_icon_chooser_dialog_close (XAPP_ICON_CHOOSER_DIALOG (widget), GTK_RESPONSE_CANCEL); + + return TRUE; +} + +static gboolean +on_select_event (XAppIconChooserDialog *dialog, + GdkEventAny *event) +{ + XAppIconChooserDialogPrivate *priv; + + priv = xapp_icon_chooser_dialog_get_instance_private (dialog); + + if (priv->icon_string != NULL) + { + xapp_icon_chooser_dialog_close (dialog, GTK_RESPONSE_OK); + } +} + +static void +on_icon_view_item_activated (GtkIconView *iconview, + GtkTreePath *path, + gpointer user_data) +{ + xapp_icon_chooser_dialog_close (XAPP_ICON_CHOOSER_DIALOG (user_data), GTK_RESPONSE_OK); +} + +static gboolean +on_search_bar_key_pressed (GtkWidget *widget, + GdkEvent *event, + gpointer user_data) +{ + XAppIconChooserDialogPrivate *priv; + guint keyval; + + priv = xapp_icon_chooser_dialog_get_instance_private (user_data); + + gdk_event_get_keyval (event, &keyval); + switch (keyval) + { + case GDK_KEY_Escape: + xapp_icon_chooser_dialog_close (XAPP_ICON_CHOOSER_DIALOG (user_data), GTK_RESPONSE_CANCEL); + return TRUE; + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (priv->icon_string != NULL) + { + xapp_icon_chooser_dialog_close (XAPP_ICON_CHOOSER_DIALOG (user_data), GTK_RESPONSE_OK); + } + return TRUE; + } + + return FALSE; +} diff --git a/libxapp/xapp-icon-chooser-dialog.h b/libxapp/xapp-icon-chooser-dialog.h new file mode 100644 index 0000000..86df393 --- /dev/null +++ b/libxapp/xapp-icon-chooser-dialog.h @@ -0,0 +1,43 @@ +#ifndef _XAPP_ICON_CHOOSER_DIALOG_H_ +#define _XAPP_ICON_CHOOSER_DIALOG_H_ + +#include +#include + +#include "xapp-gtk-window.h" + +G_BEGIN_DECLS + +#define XAPP_TYPE_ICON_CHOOSER_DIALOG (xapp_icon_chooser_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (XAppIconChooserDialog, xapp_icon_chooser_dialog, XAPP, ICON_CHOOSER_DIALOG, XAppGtkWindow) + +typedef enum +{ + XAPP_ICON_SIZE_16 = 16, + XAPP_ICON_SIZE_22 = 22, + XAPP_ICON_SIZE_24 = 24, + XAPP_ICON_SIZE_32 = 32, + XAPP_ICON_SIZE_48 = 48, + XAPP_ICON_SIZE_96 = 96 +} XAppIconSize; + +XAppIconChooserDialog * xapp_icon_chooser_dialog_new (void); + +gint xapp_icon_chooser_dialog_run (XAppIconChooserDialog *dialog); + +gint xapp_icon_chooser_dialog_run_with_icon (XAppIconChooserDialog *dialog, + gchar *icon); + +gint xapp_icon_chooser_dialog_run_with_category (XAppIconChooserDialog *dialog, + gchar *category); + +gchar * xapp_icon_chooser_dialog_get_icon_string (XAppIconChooserDialog *dialog); + +void xapp_icon_chooser_dialog_add_button (XAppIconChooserDialog *dialog, + GtkWidget *button, + GtkPackType packing, + GtkResponseType response_id); +G_END_DECLS + +#endif /* _XAPP_ICON_CHOOSER_DIALOG_H_ */ diff --git a/libxapp/xapp-preferences-window.c b/libxapp/xapp-preferences-window.c index 1d31c6e..630c14f 100644 --- a/libxapp/xapp-preferences-window.c +++ b/libxapp/xapp-preferences-window.c @@ -1,5 +1,6 @@ #include #include "xapp-preferences-window.h" +#include "xapp-stack-sidebar.h" /** * SECTION:xapp-preferences-window @@ -14,12 +15,11 @@ typedef struct { - GtkWidget *stack; - GtkWidget *side_switcher; - GtkWidget *button_area; - GtkSizeGroup *button_size_group; + GtkWidget *stack; + XAppStackSidebar *side_switcher; + GtkWidget *button_area; - gint num_pages; + gint num_pages; } XAppPreferencesWindowPrivate; enum @@ -38,32 +38,41 @@ XAppPreferencesWindowPrivate *priv = xapp_preferences_window_get_instance_private (window); GtkWidget *main_box; GtkWidget *secondary_box; + GtkStyleContext *style_context; gtk_window_set_default_size (GTK_WINDOW (window), 600, 400); gtk_window_set_skip_taskbar_hint (GTK_WINDOW (window), TRUE); gtk_window_set_type_hint (GTK_WINDOW (window), GDK_WINDOW_TYPE_HINT_DIALOG); + gtk_container_set_border_width (GTK_CONTAINER (window), 5); main_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_container_set_border_width (GTK_CONTAINER (main_box), 5); gtk_container_add (GTK_CONTAINER (window), main_box); secondary_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_container_set_border_width (GTK_CONTAINER (secondary_box), 5); gtk_box_pack_start (GTK_BOX (main_box), secondary_box, TRUE, TRUE, 0); - priv->side_switcher = gtk_stack_sidebar_new (); - gtk_widget_set_size_request (priv->side_switcher, 100, -1); - gtk_box_pack_start (GTK_BOX (secondary_box), priv->side_switcher, FALSE, FALSE, 0); - gtk_widget_set_no_show_all (priv->side_switcher, TRUE); + style_context = gtk_widget_get_style_context (secondary_box); + gtk_style_context_add_class (style_context, GTK_STYLE_CLASS_FRAME); + + priv->side_switcher = xapp_stack_sidebar_new (); + gtk_widget_set_size_request (GTK_WIDGET (priv->side_switcher), 100, -1); + gtk_box_pack_start (GTK_BOX (secondary_box), GTK_WIDGET (priv->side_switcher), FALSE, FALSE, 0); + gtk_widget_set_no_show_all (GTK_WIDGET (priv->side_switcher), TRUE); priv->stack = gtk_stack_new (); gtk_stack_set_transition_type (GTK_STACK (priv->stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE); gtk_box_pack_start (GTK_BOX (secondary_box), priv->stack, TRUE, TRUE, 0); - gtk_stack_sidebar_set_stack (GTK_STACK_SIDEBAR (priv->side_switcher), GTK_STACK (priv->stack)); + xapp_stack_sidebar_set_stack (priv->side_switcher, GTK_STACK (priv->stack)); - priv->button_area = gtk_action_bar_new (); + style_context = gtk_widget_get_style_context (priv->stack); + gtk_style_context_add_class (style_context, GTK_STYLE_CLASS_VIEW); + + priv->button_area = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL); + gtk_container_set_border_width (GTK_CONTAINER (priv->button_area), 5); gtk_box_pack_start (GTK_BOX (main_box), priv->button_area, FALSE, FALSE, 0); gtk_widget_set_no_show_all (priv->button_area, TRUE); - - priv->button_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); /* Keep track of the number of pages so we can hide the stack switcher with < 2 */ priv->num_pages = 0; @@ -135,7 +144,7 @@ if (priv->num_pages > 1) { - gtk_widget_set_no_show_all (priv->side_switcher, FALSE); + gtk_widget_set_no_show_all (GTK_WIDGET (priv->side_switcher), FALSE); } } @@ -161,17 +170,13 @@ g_return_if_fail (XAPP_IS_PREFERENCES_WINDOW (window)); g_return_if_fail (GTK_IS_WIDGET (button)); - if (pack_type == GTK_PACK_START) + gtk_container_add (GTK_CONTAINER (priv->button_area), button); + + if (pack_type == GTK_PACK_END) { - gtk_action_bar_pack_start (GTK_ACTION_BAR (priv->button_area), button); - gtk_widget_set_margin_start (button, 6); + gtk_button_box_set_child_secondary (GTK_BUTTON_BOX (priv->button_area), button, TRUE); } - else if (pack_type == GTK_PACK_END) - { - gtk_action_bar_pack_end (GTK_ACTION_BAR (priv->button_area), button); - gtk_widget_set_margin_end (button, 6); - } - else + else if (pack_type != GTK_PACK_START) { return; } @@ -179,6 +184,5 @@ style_context = gtk_widget_get_style_context (button); gtk_style_context_add_class (style_context, "text-button"); - gtk_size_group_add_widget (priv->button_size_group, button); gtk_widget_set_no_show_all (priv->button_area, FALSE); -} \ No newline at end of file +} diff --git a/libxapp/xapp-stack-sidebar.c b/libxapp/xapp-stack-sidebar.c new file mode 100644 index 0000000..9aef46e --- /dev/null +++ b/libxapp/xapp-stack-sidebar.c @@ -0,0 +1,522 @@ +/* Based on gtkstacksidebar.c */ + +#include "xapp-stack-sidebar.h" + +/** + * SECTION:xapp-stack-sidebar + * @Title: XAppStackSidebar + * @Short_description: An automatic sidebar widget + * + * A XAppStackSidebar allows you to quickly and easily provide a + * consistent "sidebar" object for your user interface + * + * In order to use a XAppStackSidebar, you simply use a GtkStack to + * organize your UI flow, and add the sidebar to your sidebar area. You + * can use xapp_stack_sidebar_set_stack() to connect the #XAppStackSidebar + * to the #GtkStack. The #XAppStackSidebar is an extended version of the + * the #GtkStackSidebar that allows showing an icon in addition to the text. + * + * # CSS nodes + * + * XAppStackSidebar has a single CSS node with the name stacksidebar and + * style class .sidebar + * + * When circumstances require it, XAppStackSidebar adds the + * .needs-attention style class to the widgets representing the stack + * pages. + */ + + +struct _XAppStackSidebar +{ + GtkBin parent_instance; + + GtkListBox *list; + GtkStack *stack; + GHashTable *rows; + gboolean in_child_changed; +}; + +enum +{ + PROP_0, + PROP_STACK, + N_PROPERTIES +}; + +static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, }; + +G_DEFINE_TYPE (XAppStackSidebar, xapp_stack_sidebar, GTK_TYPE_BIN) + +static void +xapp_stack_sidebar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + XAppStackSidebar *sidebar = XAPP_STACK_SIDEBAR (object); + + switch (prop_id) + { + case PROP_STACK: + xapp_stack_sidebar_set_stack (XAPP_STACK_SIDEBAR (object), g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +xapp_stack_sidebar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + XAppStackSidebar *sidebar = XAPP_STACK_SIDEBAR (object); + + switch (prop_id) + { + case PROP_STACK: + g_value_set_object (value, sidebar->stack); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static gint +sort_list (GtkListBoxRow *row1, + GtkListBoxRow *row2, + gpointer userdata) +{ + XAppStackSidebar *sidebar = XAPP_STACK_SIDEBAR (userdata); + GtkWidget *item; + GtkWidget *widget; + gint left = 0; + gint right = 0; + + + if (row1) + { + item = gtk_bin_get_child (GTK_BIN (row1)); + widget = g_object_get_data (G_OBJECT (item), "stack-child"); + gtk_container_child_get (GTK_CONTAINER (sidebar->stack), widget, + "position", &left, + NULL); + } + + if (row2) + { + item = gtk_bin_get_child (GTK_BIN (row2)); + widget = g_object_get_data (G_OBJECT (item), "stack-child"); + gtk_container_child_get (GTK_CONTAINER (sidebar->stack), widget, + "position", &right, + NULL); + } + + if (left < right) + { + return -1; + } + + if (left == right) + { + return 0; + } + + return 1; +} + +static void +xapp_stack_sidebar_row_selected (GtkListBox *box, + GtkListBoxRow *row, + gpointer user_data) +{ + XAppStackSidebar *sidebar = XAPP_STACK_SIDEBAR (user_data); + GtkWidget *item; + GtkWidget *widget; + + if (sidebar->in_child_changed) + { + return; + } + + if (!row) + { + return; + } + + item = gtk_bin_get_child (GTK_BIN (row)); + widget = g_object_get_data (G_OBJECT (item), "stack-child"); + gtk_stack_set_visible_child (sidebar->stack, widget); +} + +static void +xapp_stack_sidebar_init (XAppStackSidebar *sidebar) +{ + GtkStyleContext *style; + GtkWidget *sw; + + sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + + gtk_container_add (GTK_CONTAINER (sidebar), sw); + + sidebar->list = GTK_LIST_BOX (gtk_list_box_new ()); + + gtk_container_add (GTK_CONTAINER (sw), GTK_WIDGET (sidebar->list)); + + gtk_list_box_set_sort_func (sidebar->list, sort_list, sidebar, NULL); + + g_signal_connect (sidebar->list, "row-selected", + G_CALLBACK (xapp_stack_sidebar_row_selected), sidebar); + + style = gtk_widget_get_style_context (GTK_WIDGET (sidebar)); + gtk_style_context_add_class (style, "sidebar"); + + gtk_widget_show_all (GTK_WIDGET (sidebar)); + + sidebar->rows = g_hash_table_new (NULL, NULL); +} + +static void +update_row (XAppStackSidebar *sidebar, + GtkWidget *widget, + GtkWidget *row) +{ + GList *children; + GList *list; + GtkWidget *item; + gchar *title; + gchar *icon_name; + gboolean needs_attention; + GtkStyleContext *context; + + gtk_container_child_get (GTK_CONTAINER (sidebar->stack), + widget, + "title", &title, + "icon-name", &icon_name, + "needs-attention", &needs_attention, + NULL); + + item = gtk_bin_get_child (GTK_BIN (row)); + + children = gtk_container_get_children (GTK_CONTAINER (item)); + for (list = children; list != NULL; list = list->next) + { + GtkWidget *child = list->data; + + if (GTK_IS_LABEL (child)) + { + gtk_label_set_text (GTK_LABEL (child), title); + } + else if (GTK_IS_IMAGE (child)) + { + gtk_image_set_from_icon_name (GTK_IMAGE (child), icon_name, GTK_ICON_SIZE_MENU); + } + } + + gtk_widget_set_visible (row, gtk_widget_get_visible (widget) && (title != NULL || icon_name != NULL)); + + context = gtk_widget_get_style_context (row); + if (needs_attention) + { + gtk_style_context_add_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION); + } + else + { + gtk_style_context_remove_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION); + } + + g_free (title); + g_free (icon_name); + g_list_free (children); +} + +static void +on_position_updated (GtkWidget *widget, + GParamSpec *pspec, + XAppStackSidebar *sidebar) +{ + gtk_list_box_invalidate_sort (sidebar->list); +} + +static void +on_child_updated (GtkWidget *widget, + GParamSpec *pspec, + XAppStackSidebar *sidebar) +{ + GtkWidget *row; + + row = g_hash_table_lookup (sidebar->rows, widget); + update_row (sidebar, widget, row); +} + +static void +add_child (GtkWidget *widget, + XAppStackSidebar *sidebar) +{ + GtkWidget *item; + GtkWidget *label; + GtkWidget *icon; + GtkWidget *row; + + /* Check we don't actually already know about this widget */ + if (g_hash_table_lookup (sidebar->rows, widget)) + { + return; + } + + /* Make a pretty item when we add children */ + item = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_margin_start (item, 6); + gtk_widget_set_margin_end (item, 6); + + icon = gtk_image_new (); + gtk_box_pack_start (GTK_BOX (item), icon, FALSE, FALSE, 0); + + label = gtk_label_new (""); + gtk_box_pack_start (GTK_BOX (item), label, FALSE, FALSE, 0); + + row = gtk_list_box_row_new (); + gtk_container_add (GTK_CONTAINER (row), item); + gtk_widget_show_all (item); + + update_row (sidebar, widget, row); + + /* Hook up events */ + g_signal_connect (widget, "child-notify::title", + G_CALLBACK (on_child_updated), sidebar); + g_signal_connect (widget, "child-notify::icon-name", + G_CALLBACK (on_child_updated), sidebar); + g_signal_connect (widget, "child-notify::needs-attention", + G_CALLBACK (on_child_updated), sidebar); + g_signal_connect (widget, "notify::visible", + G_CALLBACK (on_child_updated), sidebar); + g_signal_connect (widget, "child-notify::position", + G_CALLBACK (on_position_updated), sidebar); + + g_object_set_data (G_OBJECT (item), "stack-child", widget); + g_hash_table_insert (sidebar->rows, widget, row); + gtk_container_add (GTK_CONTAINER (sidebar->list), row); +} + +static void +remove_child (GtkWidget *widget, + XAppStackSidebar *sidebar) +{ + GtkWidget *row; + + row = g_hash_table_lookup (sidebar->rows, widget); + if (!row) + { + return; + } + + g_signal_handlers_disconnect_by_func (widget, on_child_updated, sidebar); + g_signal_handlers_disconnect_by_func (widget, on_position_updated, sidebar); + + gtk_container_remove (GTK_CONTAINER (sidebar->list), row); + g_hash_table_remove (sidebar->rows, widget); +} + +static void +populate_sidebar (XAppStackSidebar *sidebar) +{ + GtkWidget *widget; + GtkWidget *row; + + gtk_container_foreach (GTK_CONTAINER (sidebar->stack), (GtkCallback)add_child, sidebar); + + widget = gtk_stack_get_visible_child (sidebar->stack); + if (widget) + { + row = g_hash_table_lookup (sidebar->rows, widget); + gtk_list_box_select_row (sidebar->list, GTK_LIST_BOX_ROW (row)); + } +} + +static void +clear_sidebar (XAppStackSidebar *sidebar) +{ + gtk_container_foreach (GTK_CONTAINER (sidebar->stack), (GtkCallback)remove_child, sidebar); +} + +static void +on_child_changed (GtkWidget *widget, + GParamSpec *pspec, + XAppStackSidebar *sidebar) +{ + GtkWidget *child; + GtkWidget *row; + + child = gtk_stack_get_visible_child (GTK_STACK (widget)); + row = g_hash_table_lookup (sidebar->rows, child); + + if (row != NULL) + { + sidebar->in_child_changed = TRUE; + gtk_list_box_select_row (sidebar->list, GTK_LIST_BOX_ROW (row)); + sidebar->in_child_changed = FALSE; + } +} + +static void +on_stack_child_added (GtkContainer *container, + GtkWidget *widget, + XAppStackSidebar *sidebar) +{ + add_child (widget, sidebar); +} + +static void +on_stack_child_removed (GtkContainer *container, + GtkWidget *widget, + XAppStackSidebar *sidebar) +{ + remove_child (widget, sidebar); +} + +static void +disconnect_stack_signals (XAppStackSidebar *sidebar) +{ + g_signal_handlers_disconnect_by_func (sidebar->stack, on_stack_child_added, sidebar); + g_signal_handlers_disconnect_by_func (sidebar->stack, on_stack_child_removed, sidebar); + g_signal_handlers_disconnect_by_func (sidebar->stack, on_child_changed, sidebar); + g_signal_handlers_disconnect_by_func (sidebar->stack, disconnect_stack_signals, sidebar); +} + +static void +connect_stack_signals (XAppStackSidebar *sidebar) +{ + g_signal_connect_after (sidebar->stack, "add", + G_CALLBACK (on_stack_child_added), sidebar); + g_signal_connect_after (sidebar->stack, "remove", + G_CALLBACK (on_stack_child_removed), sidebar); + g_signal_connect (sidebar->stack, "notify::visible-child", + G_CALLBACK (on_child_changed), sidebar); + g_signal_connect_swapped (sidebar->stack, "destroy", + G_CALLBACK (disconnect_stack_signals), sidebar); +} + +static void +xapp_stack_sidebar_dispose (GObject *object) +{ + XAppStackSidebar *sidebar = XAPP_STACK_SIDEBAR (object); + + xapp_stack_sidebar_set_stack (sidebar, NULL); + + G_OBJECT_CLASS (xapp_stack_sidebar_parent_class)->dispose (object); +} + +static void +xapp_stack_sidebar_finalize (GObject *object) +{ + XAppStackSidebar *sidebar = XAPP_STACK_SIDEBAR (object); + + g_hash_table_destroy (sidebar->rows); + + G_OBJECT_CLASS (xapp_stack_sidebar_parent_class)->finalize (object); +} + +static void +xapp_stack_sidebar_class_init (XAppStackSidebarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = xapp_stack_sidebar_dispose; + object_class->finalize = xapp_stack_sidebar_finalize; + object_class->set_property = xapp_stack_sidebar_set_property; + object_class->get_property = xapp_stack_sidebar_get_property; + + obj_properties[PROP_STACK] = + g_param_spec_object ("stack", + "Stack", + "Associated stack for this XAppStackSidebar", + GTK_TYPE_STACK, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPERTIES, obj_properties); + + gtk_widget_class_set_css_name (widget_class, "stacksidebar"); +} + +/** + * xapp_stack_sidebar_new: + * + * Creates a new sidebar. + * + * Returns: the new #XAppStackSidebar + */ + +XAppStackSidebar * +xapp_stack_sidebar_new (void) +{ + return g_object_new (XAPP_TYPE_STACK_SIDEBAR, NULL); +} + +/** + * xapp_stack_sidebar_set_stack: + * @sidebar: a #XAppStackSidebar + * @stack: a #GtkStack + * + * Set the #GtkStack associated with this #XAppStackSidebar. + * + * The sidebar widget will automatically update according to the order + * (packing) and items within the given #GtkStack. + */ + +void +xapp_stack_sidebar_set_stack (XAppStackSidebar *sidebar, + GtkStack *stack) +{ + g_return_if_fail (XAPP_IS_STACK_SIDEBAR (sidebar)); + g_return_if_fail (GTK_IS_STACK (stack) || stack == NULL); + + if (sidebar->stack == stack) + { + return; + } + + if (sidebar->stack) + { + disconnect_stack_signals (sidebar); + clear_sidebar (sidebar); + g_clear_object (&sidebar->stack); + } + + if (stack) + { + sidebar->stack = g_object_ref (stack); + populate_sidebar (sidebar); + connect_stack_signals (sidebar); + } + + gtk_widget_queue_resize (GTK_WIDGET (sidebar)); + + g_object_notify (G_OBJECT (sidebar), "stack"); +} + +/** + * xapp_stack_sidebar_get_stack: + * @sidebar: a #XAppStackSidebar + * + * Retrieves the stack. + * See xapp_stack_sidebar_set_stack(). + * + * Returns: (nullable) (transfer none): the associated #GtkStack or + * %NULL if none has been set explicitly + */ + +GtkStack * +xapp_stack_sidebar_get_stack (XAppStackSidebar *sidebar) +{ + g_return_val_if_fail (XAPP_IS_STACK_SIDEBAR (sidebar), NULL); + + return GTK_STACK (sidebar->stack); +} diff --git a/libxapp/xapp-stack-sidebar.h b/libxapp/xapp-stack-sidebar.h new file mode 100644 index 0000000..7085543 --- /dev/null +++ b/libxapp/xapp-stack-sidebar.h @@ -0,0 +1,22 @@ +#ifndef _XAPP_STACK_SIDEBAR_H_ +#define _XAPP_STACK_SIDEBAR_H_ + +#include +#include + +G_BEGIN_DECLS + +#define XAPP_TYPE_STACK_SIDEBAR (xapp_stack_sidebar_get_type ()) + +G_DECLARE_FINAL_TYPE (XAppStackSidebar, xapp_stack_sidebar, XAPP, STACK_SIDEBAR, GtkBin) + +XAppStackSidebar *xapp_stack_sidebar_new (void); + +void xapp_stack_sidebar_set_stack (XAppStackSidebar *sidebar, + GtkStack *stack); + +GtkStack *xapp_stack_sidebar_get_stack (XAppStackSidebar *sidebar); + +G_END_DECLS + +#endif /*_XAPP_STACK_SIDEBAR_H_ */ diff --git a/makepot b/makepot new file mode 100755 index 0000000..69f9a22 --- /dev/null +++ b/makepot @@ -0,0 +1,4 @@ +#!/bin/bash + +xgettext --language=C --keyword=_ --keyword=N_ --output=xapp.pot libxapp/*.c + diff --git a/meson.build b/meson.build index d261c28..2625639 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('xapp', 'c', - version : '1.2.2' + version : '1.4.2' ) gnome = import('gnome') @@ -27,7 +27,7 @@ top_inc = include_directories('.') subdir('libxapp') -# subdir('po') +subdir('po') subdir('pygobject') subdir('files') subdir('schemas') diff --git a/po/LINGUAS b/po/LINGUAS index 527e861..3858edc 100644 --- a/po/LINGUAS +++ b/po/LINGUAS @@ -1 +1,2 @@ +es fr diff --git a/po/POTFILES b/po/POTFILES deleted file mode 100644 index b479918..0000000 --- a/po/POTFILES +++ /dev/null @@ -1,4 +0,0 @@ -libxapp/xapp-gtk-window.c -libxapp/xapp-kbd-layout-controller.c -libxapp/xapp-monitor-blanker.c -libxapp/xapp-preferences-window.c diff --git a/po/es.po b/po/es.po new file mode 100644 index 0000000..ba150bf --- /dev/null +++ b/po/es.po @@ -0,0 +1,44 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-09-25 13:17-0600\n" +"PO-Revision-Date: 2018-09-25 13:42-0600\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.6\n" +"Last-Translator: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Language: es\n" + +#: libxapp/xapp-icon-chooser-dialog.c:153 +msgid "Browse" +msgstr "Ramonear" + +#: libxapp/xapp-icon-chooser-dialog.c:192 +#: libxapp/xapp-icon-chooser-dialog.c:674 +msgid "Cancel" +msgstr "Cancelar" + +#: libxapp/xapp-icon-chooser-dialog.c:691 +msgid "Image" +msgstr "Archivos de imagen" + +#: libxapp/xapp-icon-chooser-dialog.c:676 +msgid "Open" +msgstr "Abrir" + +#: libxapp/xapp-icon-chooser-dialog.c:183 +msgid "Select" +msgstr "Elegir" + +#: libxapp/xapp-icon-chooser-dialog.c:671 +msgid "Select image file" +msgstr "Elegir un archivo de icono" diff --git a/po/fr.po b/po/fr.po index e69de29..e44662b 100644 --- a/po/fr.po +++ b/po/fr.po @@ -0,0 +1,44 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-09-25 13:17-0600\n" +"PO-Revision-Date: 2018-09-25 13:43-0600\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.6\n" +"Last-Translator: \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Language: fr\n" + +#: libxapp/xapp-icon-chooser-dialog.c:153 +msgid "Browse" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:192 +#: libxapp/xapp-icon-chooser-dialog.c:674 +msgid "Cancel" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:691 +msgid "Image" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:676 +msgid "Open" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:183 +msgid "Select" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:671 +msgid "Select image file" +msgstr "" diff --git a/pygobject/XApp.py b/pygobject/XApp.py index 80eb278..9544665 100644 --- a/pygobject/XApp.py +++ b/pygobject/XApp.py @@ -21,5 +21,15 @@ class GtkWindow(XApp.GtkWindow): pass +class GtkButton(XApp.IconChooserButton): + pass + +class GtkBin(XApp.StackSidebar): + pass + GtkWindow = override(GtkWindow) +GtkButton = override(GtkButton) +GtkBin = override(GtkBin) __all__.append('GtkWindow') +__all__.append('GtkButton') +__all__.append('GtkBin') diff --git a/test-scripts/xapp-icon-chooser-dialog b/test-scripts/xapp-icon-chooser-dialog new file mode 100755 index 0000000..d264553 --- /dev/null +++ b/test-scripts/xapp-icon-chooser-dialog @@ -0,0 +1,49 @@ +#!/usr/bin/python3 + +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('XApp', '1.0') + +from gi.repository import Gtk, XApp, Gdk + +import argparse +import signal +signal.signal(signal.SIGINT, signal.SIG_DFL) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + # if there are no arguments supplied, we should create a new launcher in the default location (usually ~/.local/share/applications/) + group.add_argument('-c', '--category', dest='category', metavar='CATEGORY', + help='The category to select when the dialog is opened.') + group.add_argument('-i', '--icon-string', dest='icon', metavar='ICON_STRING', + help='The icon to select when the dialog is opened. This can be an icon name or a path. ' + 'If the icon doesn\'t exist, it will not cause an error, but there may not be an icon ' + 'selected.') + parser.add_argument('-b', '--clipboard', dest='clipboard', action='store_true', + help='If this option is supplied, the result will be copied to the clipboard when an icon is ' + 'selected and the dialog closed.') + parser.add_argument('-p', '--disallow-paths', dest='paths', action='store_false', + help='causes the path.') + + args = parser.parse_args() + + dialog = XApp.IconChooserDialog(allow_paths=args.paths) + dialog.set_skip_taskbar_hint(False) + if args.category: + response = dialog.run_with_category(args.category) + elif args.icon: + response = dialog.run_with_icon(args.icon) + else: + response = dialog.run() + + if response == Gtk.ResponseType.OK: + string = dialog.get_icon_string() + print(string) + + if args.clipboard: + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + clipboard.set_text(string, -1) + clipboard.store() + else: + print('Dialog canceled') diff --git a/xapp.pot b/xapp.pot new file mode 100644 index 0000000..d9e0f11 --- /dev/null +++ b/xapp.pot @@ -0,0 +1,121 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-11-15 10:11+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: libxapp/xapp-icon-chooser-button.c:162 +#: libxapp/xapp-icon-chooser-dialog.c:601 +msgid "Icon size" +msgstr "" + +#: libxapp/xapp-icon-chooser-button.c:163 +#: libxapp/xapp-icon-chooser-dialog.c:602 +msgid "The preferred icon size." +msgstr "" + +#: libxapp/xapp-icon-chooser-button.c:177 +msgid "Icon" +msgstr "" + +#: libxapp/xapp-icon-chooser-button.c:178 +msgid "The string representing the icon." +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:93 +msgid "Actions" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:96 +msgid "Applications" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:99 +msgid "Categories" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:102 +msgid "Devices" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:105 +msgid "Emblems" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:108 +msgid "Emoji" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:111 +msgid "Mime types" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:114 +msgid "Places" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:117 +msgid "Status" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:120 +msgid "Other" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:449 +msgid "Choose an icon" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:471 +msgid "Search" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:478 +msgid "Browse" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:534 +msgid "Loading..." +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:558 +msgid "Select" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:567 +#: libxapp/xapp-icon-chooser-dialog.c:1787 +msgid "Cancel" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:614 +msgid "Allow Paths" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:615 +msgid "Whether to allow paths." +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:1784 +msgid "Select image file" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:1789 +msgid "Open" +msgstr "" + +#: libxapp/xapp-icon-chooser-dialog.c:1804 +msgid "Image" +msgstr ""