// SPDX-License-Identifier: GPL-2.0+
/* NetworkManager Connection editor -- Connection editor for NetworkManager
*
* Dan Williams <dcbw@redhat.com>
*
* Copyright 2008 - 2014 Red Hat, Inc.
*/
#include "nm-default.h"
#include <net/ethernet.h>
#include <netinet/ether.h>
#include <string.h>
#include <stdlib.h>
#include "ce-page.h"
G_DEFINE_ABSTRACT_TYPE (CEPage, ce_page, G_TYPE_OBJECT)
enum {
PROP_0,
PROP_CONNECTION,
PROP_PARENT_WINDOW,
LAST_PROP
};
enum {
CHANGED,
INITIALIZED,
NEW_EDITOR,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };
typedef struct {
int value;
const char *text;
} SpinMapping;
static gboolean
spin_output_with_mapping (GtkSpinButton *spin,
const SpinMapping *mapping)
{
int val;
gchar *buf = NULL;
val = gtk_spin_button_get_value_as_int (spin);
if (val == mapping->value)
buf = g_strdup (mapping->text);
else
buf = g_strdup_printf ("%d", val);
if (!nm_streq (buf, gtk_entry_get_text (GTK_ENTRY (spin))))
gtk_entry_set_text (GTK_ENTRY (spin), buf);
g_free (buf);
return TRUE;
}
static gint
spin_input_with_mapping (GtkSpinButton *spin,
gdouble *new_val,
const SpinMapping *mapping)
{
const gchar *buf;
buf = gtk_entry_get_text (GTK_ENTRY (spin));
if (nm_streq (buf, mapping->text)) {
*new_val = mapping->value;
return TRUE;
}
return FALSE;
}
static void
spin_set_mapping (GtkSpinButton *spin, int value, const char *text)
{
SpinMapping *mapping;
g_return_if_fail (!g_object_get_data (G_OBJECT (spin), "mapping"));
mapping = g_new (SpinMapping, 1);
*mapping = (SpinMapping) {
.value = value,
.text = text,
};
g_object_set_data_full (G_OBJECT (spin), "mapping", mapping, g_free);
g_signal_connect (spin, "output",
G_CALLBACK (spin_output_with_mapping),
mapping);
g_signal_connect (spin, "input",
G_CALLBACK (spin_input_with_mapping),
mapping);
}
void
ce_spin_automatic_val (GtkSpinButton *spin, int defvalue)
{
spin_set_mapping (spin, defvalue, _("automatic"));
}
void
ce_spin_default_val (GtkSpinButton *spin, int defvalue)
{
spin_set_mapping (spin, defvalue, _("default"));
}
void
ce_spin_off_val (GtkSpinButton *spin, int defvalue)
{
spin_set_mapping (spin, defvalue, _("off"));
}
int
ce_get_property_default (NMSetting *setting, const char *property_name)
{
GParamSpec *spec;
GValue value = { 0, };
g_return_val_if_fail (NM_IS_SETTING (setting), -1);
spec = g_object_class_find_property (G_OBJECT_GET_CLASS (setting), property_name);
g_return_val_if_fail (spec != NULL, -1);
g_value_init (&value, spec->value_type);
g_param_value_set_default (spec, &value);
if (G_VALUE_HOLDS_CHAR (&value))
return (int) g_value_get_schar (&value);
else if (G_VALUE_HOLDS_INT (&value))
return g_value_get_int (&value);
else if (G_VALUE_HOLDS_INT64 (&value))
return (int) g_value_get_int64 (&value);
else if (G_VALUE_HOLDS_LONG (&value))
return (int) g_value_get_long (&value);
else if (G_VALUE_HOLDS_UINT (&value))
return (int) g_value_get_uint (&value);
else if (G_VALUE_HOLDS_UINT64 (&value))
return (int) g_value_get_uint64 (&value);
else if (G_VALUE_HOLDS_ULONG (&value))
return (int) g_value_get_ulong (&value);
else if (G_VALUE_HOLDS_UCHAR (&value))
return (int) g_value_get_uchar (&value);
g_return_val_if_fail (FALSE, 0);
return 0;
}
gboolean
ce_page_validate (CEPage *self, NMConnection *connection, GError **error)
{
g_return_val_if_fail (CE_IS_PAGE (self), FALSE);
g_return_val_if_fail (NM_IS_CONNECTION (connection), FALSE);
if (CE_PAGE_GET_CLASS (self)->ce_page_validate_v) {
if (!CE_PAGE_GET_CLASS (self)->ce_page_validate_v (self, connection, error)) {
if (error && !*error)
g_set_error_literal (error, NMA_ERROR, NMA_ERROR_GENERIC, _("unspecified error"));
return FALSE;
}
}
return TRUE;
}
gboolean
ce_page_last_update (CEPage *self, NMConnection *connection, GError **error)
{
g_return_val_if_fail (CE_IS_PAGE (self), FALSE);
g_return_val_if_fail (NM_IS_CONNECTION (connection), FALSE);
if (CE_PAGE_GET_CLASS (self)->last_update)
return CE_PAGE_GET_CLASS (self)->last_update (self, connection, error);
return TRUE;
}
gboolean
ce_page_inter_page_change (CEPage *self)
{
gboolean ret = FALSE;
g_return_val_if_fail (CE_IS_PAGE (self), FALSE);
if (self->inter_page_change_running)
return FALSE;
self->inter_page_change_running = TRUE;
if (CE_PAGE_GET_CLASS (self)->inter_page_change)
ret = CE_PAGE_GET_CLASS (self)->inter_page_change (self);
self->inter_page_change_running = FALSE;
return ret;
}
static void
_set_active_combo_item (GtkComboBox *combo, const char *item,
const char *combo_item, int combo_idx)
{
GtkWidget *entry;
if (item) {
/* set active item */
gtk_combo_box_set_active (combo, combo_idx);
if (!combo_item)
gtk_combo_box_text_prepend_text (GTK_COMBO_BOX_TEXT (combo), item);
entry = gtk_bin_get_child (GTK_BIN (combo));
if (entry)
gtk_entry_set_text (GTK_ENTRY (entry), combo_item ? combo_item : item);
}
}
/* Combo box storing data in the form of "text1 (text2)" */
void
ce_page_setup_data_combo (CEPage *self, GtkComboBox *combo,
const char *data, char **list)
{
char **iter, *active_item = NULL;
int i, active_idx = -1;
int data_len;
if (data)
data_len = strlen (data);
else
data_len = -1;
for (iter = list, i = 0; iter && *iter; iter++, i++) {
gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), *iter);
if ( data
&& g_ascii_strncasecmp (*iter, data, data_len) == 0
&& ((*iter)[data_len] == '\0' || (*iter)[data_len] == ' ')) {
active_item = *iter;
active_idx = i;
}
}
_set_active_combo_item (combo, data, active_item, active_idx);
}
/* Combo box storing MAC addresses only */
void
ce_page_setup_mac_combo (CEPage *self, GtkComboBox *combo,
const char *mac, char **mac_list)
{
char **iter, *active_mac = NULL;
int i, active_idx = -1;
for (iter = mac_list, i = 0; iter && *iter; iter++, i++) {
gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), *iter);
if (mac && *iter && nm_utils_hwaddr_matches (mac, -1, *iter, -1)) {
active_mac = *iter;
active_idx = i;
}
}
_set_active_combo_item (combo, mac, active_mac, active_idx);
}
void
ce_page_setup_cloned_mac_combo (GtkComboBoxText *combo, const char *current)
{
GtkWidget *entry;
static const char *entries[][2] = { { "preserve", N_("Preserve") },
{ "permanent", N_("Permanent") },
{ "random", N_("Random") },
{ "stable", N_("Stable") } };
int i, active = -1;
gtk_widget_set_tooltip_text (GTK_WIDGET (combo),
_("The MAC address entered here will be used as hardware address for "
"the network device this connection is activated on. This feature is "
"known as MAC cloning or spoofing. Example: 00:11:22:33:44:55"));
gtk_combo_box_text_remove_all (combo);
for (i = 0; i < G_N_ELEMENTS (entries); i++) {
gtk_combo_box_text_append (combo, entries[i][0], _(entries[i][1]));
if (nm_streq0 (current, entries[i][0]))
active = i;
}
if (active != -1) {
gtk_combo_box_set_active (GTK_COMBO_BOX (combo), active);
} else if (current && current[0]) {
entry = gtk_bin_get_child (GTK_BIN (combo));
g_assert (entry);
gtk_entry_set_text (GTK_ENTRY (entry), current);
}
}
char *
ce_page_cloned_mac_get (GtkComboBoxText *combo)
{
const char *id;
id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (combo));
if (id)
return g_strdup (id);
return gtk_combo_box_text_get_active_text (combo);
}
static gboolean
mac_valid (const char *mac, int type, const char *property_name, GError **error)
{
if (mac && *mac) {
if (!nm_utils_hwaddr_valid (mac, nm_utils_hwaddr_len (type))) {
const char *addr_type;
addr_type = type == ARPHRD_ETHER ? _("MAC address") : _("HW address");
if (property_name) {
g_set_error (error, NMA_ERROR, NMA_ERROR_GENERIC,
_("invalid %s for %s (%s)"),
addr_type, property_name, mac);
} else {
g_set_error (error, NMA_ERROR, NMA_ERROR_GENERIC,
_("invalid %s (%s)"),
addr_type, mac);
}
return FALSE;
}
}
return TRUE;
}
gboolean
ce_page_cloned_mac_combo_valid (GtkComboBoxText *combo, int type, const char *property_name, GError **error)
{
gs_free char *text = NULL;
if (gtk_combo_box_get_active (GTK_COMBO_BOX (combo)) != -1)
return TRUE;
text = gtk_combo_box_text_get_active_text (combo);
return mac_valid (text,
type,
property_name,
error);
}
gboolean
ce_page_mac_entry_valid (GtkEntry *entry, int type, const char *property_name, GError **error)
{
g_return_val_if_fail (GTK_IS_ENTRY (entry), FALSE);
return mac_valid (gtk_entry_get_text (entry), type, property_name, error);
}
gboolean
ce_page_interface_name_valid (const char *iface, const char *property_name, GError **error)
{
if (iface && *iface) {
if (!nm_utils_is_valid_iface_name (iface, error)) {
if (property_name) {
g_prefix_error (error,
_("invalid interface-name for %s (%s): "),
property_name, iface);
} else {
g_prefix_error (error,
_("invalid interface-name (%s): "),
iface);
}
return FALSE;
}
}
return TRUE;
}
static char **
_get_device_list (CEPage *self,
GType device_type,
gboolean set_ifname,
const char *mac_property)
{
const GPtrArray *devices;
GPtrArray *interfaces;
int i;
g_return_val_if_fail (CE_IS_PAGE (self), NULL);
g_return_val_if_fail (set_ifname || mac_property, NULL);
if (!self->client)
return NULL;
interfaces = g_ptr_array_new ();
devices = nm_client_get_devices (self->client);
for (i = 0; i < devices->len; i++) {
NMDevice *dev = g_ptr_array_index (devices, i);
const char *ifname;
char *mac = NULL;
char *item;
if ( device_type != G_TYPE_NONE
&& !G_TYPE_CHECK_INSTANCE_TYPE (dev, device_type))
continue;
if (device_type == NM_TYPE_DEVICE_BT)
ifname = nm_device_bt_get_name (NM_DEVICE_BT (dev));
else
ifname = nm_device_get_iface (NM_DEVICE (dev));
if (mac_property)
g_object_get (G_OBJECT (dev), mac_property, &mac, NULL);
if (mac && !mac[0])
nm_clear_g_free (&mac);
if (set_ifname && mac_property)
item = g_strdup_printf ("%s%s%s%s", ifname, NM_PRINT_FMT_QUOTED (mac, " (", mac, ")", ""));
else
item = g_strdup (set_ifname ? ifname : mac);
if (item)
g_ptr_array_add (interfaces, item);
g_free (mac);
}
g_ptr_array_add (interfaces, NULL);
return (char **)g_ptr_array_free (interfaces, FALSE);
}
static gboolean
_device_entry_parse (const char *entry_text, char **first, char **second)
{
const char *sp, *left, *right;
if (!entry_text || !*entry_text) {
*first = NULL;
*second = NULL;
return TRUE;
}
sp = strstr (entry_text, " (");
if (sp) {
*first = g_strndup (entry_text, sp - entry_text);
left = sp + 1;
right = strchr (left, ')');
if (*left == '(' && right && right > left)
*second = g_strndup (left + 1, right - left - 1);
else {
*second = NULL;
return FALSE;
}
} else {
*first = g_strdup (entry_text);
*second = NULL;
}
return TRUE;
}
static gboolean
_device_entries_match (const char *ifname, const char *mac, const char *entry)
{
char *first, *second;
gboolean ifname_match = FALSE, mac_match = FALSE;
gboolean both;
if (!ifname && !mac)
return FALSE;
_device_entry_parse (entry, &first, &second);
both = first && second;
if ( ifname
&& ( !g_strcmp0 (ifname, first)
|| !g_strcmp0 (ifname, second)))
ifname_match = TRUE;
if ( mac
&& ( (first && nm_utils_hwaddr_matches (mac, -1, first, -1))
|| (second && nm_utils_hwaddr_matches (mac, -1, second, -1))))
mac_match = TRUE;
g_free (first);
g_free (second);
if (both)
return ifname_match && mac_match;
else {
if (ifname)
return ifname_match;
else
return mac_match;
}
}
/* Combo box storing ifname and/or MAC */
void
ce_page_setup_device_combo (CEPage *self,
GtkComboBox *combo,
GType device_type,
const char *ifname,
const char *mac,
const char *mac_property)
{
char **iter, *active_item = NULL;
int i, active_idx = -1;
char **device_list;
char *item;
device_list = _get_device_list (self, device_type, TRUE, mac_property);
if (ifname && mac)
item = g_strdup_printf ("%s (%s)", ifname, mac);
else if (!ifname && !mac)
item = NULL;
else
item = g_strdup (ifname ? ifname : mac);
for (iter = device_list, i = 0; iter && *iter; iter++, i++) {
gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), *iter);
if (_device_entries_match (ifname, mac, *iter)) {
active_item = *iter;
active_idx = i;
}
}
_set_active_combo_item (combo, item, active_item, active_idx);
g_free (item);
g_strfreev (device_list);
}
gboolean
ce_page_device_entry_get (GtkEntry *entry, int type, gboolean check_ifname,
char **ifname, char **mac, const char *device_name, GError **error)
{
gs_free char *first = NULL;
gs_free char *second = NULL;
const char *ifname_tmp = NULL, *mac_tmp = NULL;
const char *str;
g_return_val_if_fail (entry != NULL, FALSE);
g_return_val_if_fail (GTK_IS_ENTRY (entry), FALSE);
str = gtk_entry_get_text (entry);
if (!_device_entry_parse (str, &first, &second)) {
g_set_error_literal (error, NMA_ERROR, NMA_ERROR_GENERIC,
_("can’t parse device name"));
goto invalid;
}
if (first) {
if (nm_utils_hwaddr_valid (first, nm_utils_hwaddr_len (type)))
mac_tmp = first;
else if (!check_ifname || nm_utils_is_valid_iface_name (first, error))
ifname_tmp = first;
else
goto invalid;
}
if (second) {
if (nm_utils_hwaddr_valid (second, nm_utils_hwaddr_len (type))) {
if (!mac_tmp) {
mac_tmp = second;
} else {
g_set_error_literal (error, NMA_ERROR, NMA_ERROR_GENERIC,
_("invalid hardware address"));
goto invalid;
}
} else if (!check_ifname || nm_utils_is_valid_iface_name (second, error)) {
if (!ifname_tmp)
ifname_tmp = second;
else
goto invalid;
} else
goto invalid;
}
if (ifname)
*ifname = g_strdup (ifname_tmp);
if (mac)
*mac = g_strdup (mac_tmp);
return TRUE;
invalid:
if (error) {
g_prefix_error (error,
_("invalid %s (%s): "),
device_name ? device_name : _("device"),
str);
} else {
g_set_error (error, NMA_ERROR, NMA_ERROR_GENERIC,
_("invalid %s (%s) "),
device_name ? device_name : _("device"),
str);
}
return FALSE;
}
char *
ce_page_get_next_available_name (const GPtrArray *connections, const char *format)
{
GSList *names = NULL, *iter;
char *cname = NULL;
int i = 0;
for (i = 0; i < connections->len; i++) {
const char *id;
id = nm_connection_get_id (connections->pdata[i]);
g_assert (id);
names = g_slist_append (names, (gpointer) id);
}
/* Find the next available unique connection name */
for (i = 1; !cname && i < 10000; i++) {
char *temp;
gboolean found = FALSE;
NM_PRAGMA_WARNING_DISABLE("-Wformat-nonliteral")
temp = g_strdup_printf (format, i);
NM_PRAGMA_WARNING_REENABLE
for (iter = names; iter; iter = g_slist_next (iter)) {
if (!strcmp (iter->data, temp)) {
found = TRUE;
break;
}
}
if (!found)
cname = temp;
else
g_free (temp);
}
g_slist_free (names);
return cname;
}
void
ce_page_complete_init (CEPage *self,
const char *setting_name,
GVariant *secrets,
GError *error)
{
GError *update_error = NULL;
GVariant *setting_dict;
char *dbus_err;
g_return_if_fail (self != NULL);
g_return_if_fail (CE_IS_PAGE (self));
if (error) {
/* Ignore missing settings errors */
dbus_err = g_dbus_error_get_remote_error (error);
if ( g_strcmp0 (dbus_err, "org.freedesktop.NetworkManager.Settings.InvalidSetting") == 0
|| g_strcmp0 (dbus_err, "org.freedesktop.NetworkManager.Settings.Connection.SettingNotFound") == 0
|| g_strcmp0 (dbus_err, "org.freedesktop.NetworkManager.AgentManager.NoSecrets") == 0)
g_clear_error (&error);
g_free (dbus_err);
}
if (error) {
g_warning ("Couldn't fetch secrets: %s", error->message);
g_error_free (error);
goto out;
}
if (!setting_name || !secrets || g_variant_n_children (secrets) == 0) {
/* Success, no secrets */
goto out;
}
g_assert (setting_name);
g_assert (secrets);
setting_dict = g_variant_lookup_value (secrets, setting_name, NM_VARIANT_TYPE_SETTING);
if (!setting_dict) {
/* Success, no secrets */
goto out;
}
g_variant_unref (setting_dict);
/* Update the connection with the new secrets */
if (!nm_connection_update_secrets (self->connection,
setting_name,
secrets,
&update_error)) {
g_warning ("Couldn't update the secrets: %s", update_error->message);
g_error_free (update_error);
goto out;
}
out:
g_signal_emit (self, signals[INITIALIZED], 0, NULL);
}
static void
ce_page_init (CEPage *self)
{
self->builder = gtk_builder_new ();
}
static void
dispose (GObject *object)
{
CEPage *self = CE_PAGE (object);
g_clear_object (&self->page);
g_clear_object (&self->builder);
g_clear_object (&self->connection);
G_OBJECT_CLASS (ce_page_parent_class)->dispose (object);
}
static void
finalize (GObject *object)
{
CEPage *self = CE_PAGE (object);
g_free (self->title);
G_OBJECT_CLASS (ce_page_parent_class)->finalize (object);
}
GtkWidget *
ce_page_get_page (CEPage *self)
{
g_return_val_if_fail (CE_IS_PAGE (self), NULL);
return self->page;
}
const char *
ce_page_get_title (CEPage *self)
{
g_return_val_if_fail (CE_IS_PAGE (self), NULL);
return self->title;
}
void
ce_page_changed (CEPage *self)
{
g_return_if_fail (CE_IS_PAGE (self));
g_signal_emit (self, signals[CHANGED], 0);
}
NMConnectionEditor *
ce_page_new_editor (CEPage *self,
GtkWindow *parent_window,
NMConnection *connection)
{
NMConnectionEditor *editor;
g_return_val_if_fail (CE_IS_PAGE (self), NULL);
editor = nm_connection_editor_new (parent_window,
connection,
self->client);
if (!editor)
return NULL;
g_signal_emit (self, signals[NEW_EDITOR], 0, editor);
return editor;
}
static void
get_property (GObject *object, guint prop_id,
GValue *value, GParamSpec *pspec)
{
CEPage *self = CE_PAGE (object);
switch (prop_id) {
case PROP_CONNECTION:
g_value_set_object (value, self->connection);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
set_property (GObject *object, guint prop_id,
const GValue *value, GParamSpec *pspec)
{
CEPage *self = CE_PAGE (object);
switch (prop_id) {
case PROP_CONNECTION:
if (self->connection)
g_object_unref (self->connection);
self->connection = g_value_dup_object (value);
break;
case PROP_PARENT_WINDOW:
self->parent_window = g_value_get_pointer (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
ce_page_class_init (CEPageClass *page_class)
{
GObjectClass *object_class = G_OBJECT_CLASS (page_class);
/* virtual methods */
object_class->dispose = dispose;
object_class->finalize = finalize;
object_class->get_property = get_property;
object_class->set_property = set_property;
/* Properties */
g_object_class_install_property
(object_class, PROP_CONNECTION,
g_param_spec_object (CE_PAGE_CONNECTION,
"Connection",
"Connection",
NM_TYPE_CONNECTION,
G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
g_object_class_install_property
(object_class, PROP_PARENT_WINDOW,
g_param_spec_pointer (CE_PAGE_PARENT_WINDOW,
"Parent window",
"Parent window",
G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
/* Signals */
signals[CHANGED] =
g_signal_new ("changed",
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_FIRST,
0, NULL, NULL, NULL,
G_TYPE_NONE, 0);
signals[INITIALIZED] =
g_signal_new (CE_PAGE_INITIALIZED,
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_FIRST,
0, NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_POINTER);
signals[NEW_EDITOR] =
g_signal_new (CE_PAGE_NEW_EDITOR,
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_FIRST,
0, NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_POINTER);
}
void
ce_page_complete_connection (NMConnection *connection,
const char *format,
const char *ctype,
gboolean autoconnect,
NMClient *client)
{
NMSettingConnection *s_con;
char *id, *uuid;
const GPtrArray *connections;
s_con = nm_connection_get_setting_connection (connection);
if (!s_con) {
s_con = NM_SETTING_CONNECTION (nm_setting_connection_new ());
nm_connection_add_setting (connection, NM_SETTING (s_con));
}
if (!nm_setting_connection_get_id (s_con)) {
connections = nm_client_get_connections (client);
id = ce_page_get_next_available_name (connections, format);
g_object_set (s_con, NM_SETTING_CONNECTION_ID, id, NULL);
g_free (id);
}
uuid = nm_utils_uuid_generate ();
g_object_set (s_con,
NM_SETTING_CONNECTION_UUID, uuid,
NM_SETTING_CONNECTION_TYPE, ctype,
NM_SETTING_CONNECTION_AUTOCONNECT, autoconnect,
NULL);
g_free (uuid);
}
CEPage *
ce_page_new (GType page_type,
NMConnectionEditor *editor,
NMConnection *connection,
GtkWindow *parent_window,
NMClient *client,
const char *ui_resource,
const char *widget_name,
const char *title)
{
CEPage *self;
GError *error = NULL;
g_return_val_if_fail (title != NULL, NULL);
if (ui_resource)
g_return_val_if_fail (widget_name != NULL, NULL);
self = CE_PAGE (g_object_new (page_type,
CE_PAGE_CONNECTION, connection,
CE_PAGE_PARENT_WINDOW, parent_window,
NULL));
self->title = g_strdup (title);
self->client = client;
self->editor = editor;
if (ui_resource) {
if (!gtk_builder_add_from_resource (self->builder, ui_resource, &error)) {
g_warning ("Couldn't load builder resource: %s", error->message);
g_error_free (error);
g_object_unref (self);
return NULL;
}
self->page = GTK_WIDGET (gtk_builder_get_object (self->builder, widget_name));
if (!self->page) {
g_warning ("Couldn't load page widget '%s' from %s", widget_name, ui_resource);
g_object_unref (self);
return NULL;
}
g_object_ref_sink (self->page);
}
return self;
}