qubes-installer-qubes-os/anaconda/widgets/src/SpokeSelector.c
2013-01-24 01:45:53 +01:00

340 lines
14 KiB
C

/*
* Copyright (C) 2011 Red Hat, Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Author: Chris Lumens <clumens@redhat.com>
*/
#include <gdk/gdk.h>
#include <glib.h>
#include <pango/pango.h>
#include "SpokeSelector.h"
#include "intl.h"
/**
* SECTION: SpokeSelector
* @title: AnacondaSpokeSelector
* @short_description: A graphical way to enter a configuration spoke
*
* A #AnacondaSpokeSelector is a widget that is associated with a Spoke and
* is packed into a grid on a Hub. A Spoke allows the user to configure one
* piece of system, and the associated selector both displays the current
* configuration and allows for a place to click to do further configuration.
*
* Some Spokes can have their initial configuration guessed, while others
* (specifically storage) requires the user to do something. For those that
* the user has not entered, the selector may be set as incomplete. See
* #anaconda_spoke_selector_get_incomplete and #anaconda_spoke_selector_set_incomplete.
*
* As a #AnacondaSpokeSelector is a subclass of a #GtkEventBox, any signals
* may be caught. However ::button-press-event is the most important one and
* should be how control is transferred to a Spoke.
*/
enum {
PROP_ICON = 1,
PROP_STATUS,
PROP_TITLE,
};
#define DEFAULT_ICON "gtk-missing-image"
#define DEFAULT_STATUS N_("None")
#define DEFAULT_TITLE N_("New Selector")
struct _AnacondaSpokeSelectorPrivate {
gboolean is_incomplete;
GtkWidget *grid;
GtkWidget *icon, *incomplete_icon;
GtkWidget *title_label;
GtkWidget *status_label;
GdkCursor *cursor;
GtkCssProvider *status_provider;
};
G_DEFINE_TYPE(AnacondaSpokeSelector, anaconda_spoke_selector, GTK_TYPE_EVENT_BOX)
static void anaconda_spoke_selector_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
static void anaconda_spoke_selector_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
static void anaconda_spoke_selector_realize(GtkWidget *widget, gpointer user_data);
static void anaconda_spoke_selector_finalize(AnacondaSpokeSelector *spoke);
static gboolean anaconda_spoke_selector_focus_changed(GtkWidget *widget, GdkEventFocus *event, gpointer user_data);
static void anaconda_spoke_selector_class_init(AnacondaSpokeSelectorClass *klass) {
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->set_property = anaconda_spoke_selector_set_property;
object_class->get_property = anaconda_spoke_selector_get_property;
object_class->finalize = (GObjectFinalizeFunc) anaconda_spoke_selector_finalize;
/**
* AnacondaSpokeSelector:icon:
*
* The :icon string is the standard icon name for an icon to display
* beside this spoke's :title. It is strongly suggested that one of the
* "-symbolic" icons be used, as that is consistent with the style we
* are going for.
*
* Since: 1.0
*/
g_object_class_install_property(object_class,
PROP_ICON,
g_param_spec_string("icon",
P_("Icon"),
P_("Icon to appear next to the title"),
DEFAULT_ICON,
G_PARAM_READWRITE));
/**
* AnacondaSpokeSelector:status:
*
* The :status string is text displayed underneath the spoke's :title and
* also beside the :icon. This text very briefly describes what has been
* selected on the spoke associated with this selector. For instance, it
* might be set up to "English" for a language-related spoke.
*
* Since: 1.0
*/
g_object_class_install_property(object_class,
PROP_STATUS,
g_param_spec_string("status",
P_("Status"),
P_("Status display"),
DEFAULT_STATUS,
G_PARAM_READWRITE));
/**
* AnacondaSpokeSelector:title:
*
* The :title of this selector, which will be displayed large and bold
* beside the :icon.
*
* Since: 1.0
*/
g_object_class_install_property(object_class,
PROP_TITLE,
g_param_spec_string("title",
P_("Title"),
P_("The title of the spoke selector"),
DEFAULT_TITLE,
G_PARAM_READWRITE));
g_type_class_add_private(object_class, sizeof(AnacondaSpokeSelectorPrivate));
}
/**
* anaconda_spoke_selector_new:
*
* Creates a new #AnacondaSpokeSelector, which is a selectable display for a
* single spoke of an Anaconda hub. Many spokes may be put together into a
* grid, displaying everything that a user needs to do in one place.
*
* Returns: A new #AnacondaSpokeSelector.
*/
GtkWidget *anaconda_spoke_selector_new() {
return g_object_new(ANACONDA_TYPE_SPOKE_SELECTOR, NULL);
}
static void format_status_label(AnacondaSpokeSelector *spoke, const char *markup) {
gchar *escaped;
PangoAttrList *attrs;
attrs = pango_attr_list_new();
pango_attr_list_insert(attrs, pango_attr_style_new(PANGO_STYLE_ITALIC));
pango_attr_list_insert(attrs, pango_attr_scale_new(PANGO_SCALE_LARGE));
/* Ampersands (and maybe other characters) in status text need to be escaped. */
escaped = g_markup_escape_text(markup, -1);
gtk_label_set_markup(GTK_LABEL(spoke->priv->status_label), escaped);
gtk_label_set_attributes(GTK_LABEL(spoke->priv->status_label), attrs);
pango_attr_list_unref(attrs);
g_free(escaped);
}
static void anaconda_spoke_selector_init(AnacondaSpokeSelector *spoke) {
char *markup;
spoke->priv = G_TYPE_INSTANCE_GET_PRIVATE(spoke,
ANACONDA_TYPE_SPOKE_SELECTOR,
AnacondaSpokeSelectorPrivate);
/* Allow tabbing from one SpokeSelector to the next, and make sure it's
* selectable with the keyboard.
*/
gtk_widget_set_can_focus(GTK_WIDGET(spoke), TRUE);
gtk_widget_add_events(GTK_WIDGET(spoke), GDK_FOCUS_CHANGE_MASK|GDK_KEY_RELEASE_MASK);
g_signal_connect(spoke, "focus-in-event", G_CALLBACK(anaconda_spoke_selector_focus_changed), NULL);
g_signal_connect(spoke, "focus-out-event", G_CALLBACK(anaconda_spoke_selector_focus_changed), NULL);
/* Set "hand" cursor shape when over the selector */
spoke->priv->cursor = gdk_cursor_new(GDK_HAND2);
g_signal_connect(spoke, "realize", G_CALLBACK(anaconda_spoke_selector_realize), NULL);
/* Set property defaults. */
spoke->priv->is_incomplete = FALSE;
/* Create the grid. */
spoke->priv->grid = gtk_grid_new();
gtk_grid_set_column_spacing(GTK_GRID(spoke->priv->grid), 6);
/* Create the icons. */
spoke->priv->icon = gtk_image_new_from_stock(DEFAULT_ICON, GTK_ICON_SIZE_DIALOG);
gtk_image_set_pixel_size(GTK_IMAGE(spoke->priv->icon), 64);
gtk_widget_set_valign(spoke->priv->icon, GTK_ALIGN_CENTER);
gtk_misc_set_padding(GTK_MISC(spoke->priv->icon), 12, 0);
spoke->priv->incomplete_icon = gtk_image_new_from_icon_name("dialog-warning-symbolic", GTK_ICON_SIZE_MENU);
gtk_widget_set_no_show_all(GTK_WIDGET(spoke->priv->incomplete_icon), TRUE);
gtk_widget_set_visible(GTK_WIDGET(spoke->priv->incomplete_icon), FALSE);
gtk_widget_set_valign(spoke->priv->incomplete_icon, GTK_ALIGN_START);
gtk_widget_set_hexpand(spoke->priv->incomplete_icon, FALSE);
gtk_misc_set_alignment(GTK_MISC(spoke->priv->incomplete_icon), 0, 1);
/* Create the title label. */
spoke->priv->title_label = gtk_label_new(NULL);
markup = g_markup_printf_escaped("<span weight='bold' size='large'>%s</span>", _(DEFAULT_TITLE));
gtk_label_set_justify(GTK_LABEL(spoke->priv->title_label), GTK_JUSTIFY_LEFT);
gtk_label_set_markup(GTK_LABEL(spoke->priv->title_label), markup);
gtk_misc_set_alignment(GTK_MISC(spoke->priv->title_label), 0, 1);
gtk_widget_set_hexpand(GTK_WIDGET(spoke->priv->title_label), FALSE);
g_free(markup);
/* Create the status label. */
spoke->priv->status_label = gtk_label_new(NULL);
format_status_label(spoke, _(DEFAULT_STATUS));
gtk_label_set_justify(GTK_LABEL(spoke->priv->status_label), GTK_JUSTIFY_LEFT);
gtk_misc_set_alignment(GTK_MISC(spoke->priv->status_label), 0, 0);
gtk_label_set_ellipsize(GTK_LABEL(spoke->priv->status_label), PANGO_ELLIPSIZE_MIDDLE);
gtk_label_set_max_width_chars(GTK_LABEL(spoke->priv->status_label), 45);
gtk_widget_set_hexpand(GTK_WIDGET(spoke->priv->status_label), FALSE);
spoke->priv->status_provider = gtk_css_provider_new();
gtk_css_provider_load_from_data(spoke->priv->status_provider, "GtkLabel { color: @text_color }", -1, NULL);
/* Add everything to the grid, add the grid to the widget. */
gtk_grid_attach(GTK_GRID(spoke->priv->grid), spoke->priv->icon, 0, 0, 1, 2);
gtk_grid_attach(GTK_GRID(spoke->priv->grid), spoke->priv->title_label, 1, 0, 1, 1);
gtk_grid_attach(GTK_GRID(spoke->priv->grid), spoke->priv->incomplete_icon, 2, 0, 1, 1);
gtk_grid_attach(GTK_GRID(spoke->priv->grid), spoke->priv->status_label, 1, 1, 2, 1);
gtk_container_add(GTK_CONTAINER(spoke), spoke->priv->grid);
}
static void anaconda_spoke_selector_finalize(AnacondaSpokeSelector *spoke) {
g_object_unref(spoke->priv->cursor);
}
static void anaconda_spoke_selector_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) {
AnacondaSpokeSelector *widget = ANACONDA_SPOKE_SELECTOR(object);
AnacondaSpokeSelectorPrivate *priv = widget->priv;
switch(prop_id) {
case PROP_ICON:
g_value_set_object (value, (GObject *)priv->icon);
break;
case PROP_STATUS:
g_value_set_string (value, gtk_label_get_text(GTK_LABEL(priv->status_label)));
break;
case PROP_TITLE:
g_value_set_string (value, gtk_label_get_text(GTK_LABEL(priv->title_label)));
break;
}
}
static void anaconda_spoke_selector_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) {
AnacondaSpokeSelector *widget = ANACONDA_SPOKE_SELECTOR(object);
AnacondaSpokeSelectorPrivate *priv = widget->priv;
switch(prop_id) {
case PROP_ICON:
gtk_image_set_from_icon_name(GTK_IMAGE(priv->icon), g_value_get_string(value), GTK_ICON_SIZE_DIALOG);
gtk_image_set_pixel_size(GTK_IMAGE(priv->icon), 64);
gtk_widget_set_valign(priv->icon, GTK_ALIGN_START);
break;
case PROP_STATUS: {
GtkStyleContext *context = gtk_widget_get_style_context(GTK_WIDGET(priv->status_label));
format_status_label(widget, g_value_get_string(value));
if (gtk_widget_get_sensitive(GTK_WIDGET(widget)))
gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(priv->status_provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
else
gtk_style_context_remove_provider(context, GTK_STYLE_PROVIDER(priv->status_provider));
break;
}
case PROP_TITLE: {
char *markup = g_markup_printf_escaped("<span weight='bold' size='large'>%s</span>", g_value_get_string(value));
gtk_label_set_markup(GTK_LABEL(priv->title_label), markup);
g_free(markup);
break;
}
}
}
/**
* anaconda_spoke_selector_get_incomplete:
* @spoke: a #AnacondaSpokeSelector
*
* Returns whether or not this spoke has been completed.
*
* Returns: Whether @spoke has been completed by the user.
*
* Since: 1.0
*/
gboolean anaconda_spoke_selector_get_incomplete(AnacondaSpokeSelector *spoke) {
return spoke->priv->is_incomplete;
}
/**
* anaconda_spoke_selector_set_incomplete:
* @spoke: a #AnacondaSpokeSelector
* @is_incomplete: %TRUE if this spoke still needs to be visited.
*
* Specifies whether this spoke must still be visited by the user. If so, this
* means anaconda doesn't have enough information to continue and the user must
* take some action. A warning icon will be displayed alongside the spoke's
* icon, and the continue button will be disabled.
*
* Since: 1.0
*/
void anaconda_spoke_selector_set_incomplete(AnacondaSpokeSelector *spoke, gboolean is_incomplete) {
spoke->priv->is_incomplete = is_incomplete;
gtk_widget_set_visible(GTK_WIDGET(spoke->priv->incomplete_icon), is_incomplete);
}
static gboolean anaconda_spoke_selector_focus_changed(GtkWidget *widget, GdkEventFocus *event, gpointer user_data) {
GtkStateFlags new_state;
new_state = gtk_widget_get_state_flags(widget) & ~GTK_STATE_FOCUSED;
if (event->in)
new_state |= GTK_STATE_FOCUSED;
gtk_widget_set_state_flags(widget, new_state, TRUE);
return FALSE;
}
static void anaconda_spoke_selector_realize(GtkWidget *widget, gpointer user_data) {
AnacondaSpokeSelector *spoke_selector = ANACONDA_SPOKE_SELECTOR(widget);
gdk_window_set_cursor(gtk_widget_get_window(widget), spoke_selector->priv->cursor);
}