/*
 * Copyright (c) 2014 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"
#include <glib/gi18n-lib.h>

#include "prop-editor.h"
#include "strv-editor.h"
#include "object-tree.h"

#include "gtkactionable.h"
#include "gtkadjustment.h"
#include "gtkapplicationwindow.h"
#include "gtkcelllayout.h"
#include "gtkcellrenderertext.h"
#include "gtkcolorbutton.h"
#include "gtkcolorchooser.h"
#include "gtkcolorchooserwidget.h"
#include "gtkcombobox.h"
#include "gtkfontchooser.h"
#include "gtkfontchooserwidget.h"
#include "gtkiconview.h"
#include "gtklabel.h"
#include "gtkpopover.h"
#include "gtkradiobutton.h"
#include "gtkscrolledwindow.h"
#include "gtkspinbutton.h"
#include "gtksettingsprivate.h"
#include "gtktogglebutton.h"
#include "gtkwidgetprivate.h"
#include "gtkcssnodeprivate.h"

struct _GtkInspectorPropEditorPrivate
{
  GObject *object;
  gchar *name;
  gboolean is_child_property;
  GtkWidget *editor;
};

enum
{
  PROP_0,
  PROP_OBJECT,
  PROP_NAME,
  PROP_IS_CHILD_PROPERTY
};

enum
{
  SHOW_OBJECT,
  N_SIGNALS
};

static guint signals[N_SIGNALS] = { 0 };

G_DEFINE_TYPE_WITH_PRIVATE (GtkInspectorPropEditor, gtk_inspector_prop_editor, GTK_TYPE_BOX);

static gboolean
is_child_property (GParamSpec *pspec)
{
  return g_param_spec_get_qdata (pspec, g_quark_from_string ("is-child-prop")) != NULL;
}

static GParamSpec *
mark_child_property (GParamSpec *pspec)
{
  if (pspec)
    g_param_spec_set_qdata (pspec, g_quark_from_string ("is-child-prop"), GINT_TO_POINTER (TRUE));
  return pspec;
}

static GParamSpec *
find_property (GtkInspectorPropEditor *editor)
{
  if (editor->priv->is_child_property)
    {
      GtkWidget *widget = GTK_WIDGET (editor->priv->object);
      GtkWidget *parent = gtk_widget_get_parent (widget);

      return mark_child_property (gtk_container_class_find_child_property (G_OBJECT_GET_CLASS (parent), editor->priv->name));
    }

  return g_object_class_find_property (G_OBJECT_GET_CLASS (editor->priv->object), editor->priv->name);
}

typedef struct
{
  gpointer instance;
  GObject *alive_object;
  gulong id;
} DisconnectData;

static void
disconnect_func (gpointer data)
{
  DisconnectData *dd = data;

  g_signal_handler_disconnect (dd->instance, dd->id);
}

static void
signal_removed (gpointer  data,
                GClosure *closure)
{
  DisconnectData *dd = data;

  g_object_steal_data (dd->alive_object, "alive-object-data");
  g_free (dd);
}

static void
g_object_connect_property (GObject    *object,
                           GParamSpec *spec,
                           GCallback   func,
                           gpointer    data,
                           GObject    *alive_object)
{
  GClosure *closure;
  gchar *with_detail;
  DisconnectData *dd;

  if (is_child_property (spec))
    with_detail = g_strconcat ("child-notify::", spec->name, NULL);
  else
    with_detail = g_strconcat ("notify::", spec->name, NULL);

  dd = g_new (DisconnectData, 1);

  closure = g_cclosure_new (func, data, NULL);
  g_closure_add_invalidate_notifier (closure, dd, signal_removed);
  dd->id = g_signal_connect_closure (object, with_detail, closure, FALSE);
  dd->instance = object;
  dd->alive_object = alive_object;

  g_object_set_data_full (G_OBJECT (alive_object), "alive-object-data",
                          dd, disconnect_func);

  g_free (with_detail);
}

static void
block_notify (GObject *editor)
{
  DisconnectData *dd = (DisconnectData *)g_object_get_data (editor, "alive-object-data");

  if (dd)
    g_signal_handler_block (dd->instance, dd->id);
}

static void
unblock_notify (GObject *editor)
{
  DisconnectData *dd = (DisconnectData *)g_object_get_data (editor, "alive-object-data");

  if (dd)
    g_signal_handler_unblock (dd->instance, dd->id);
}

typedef struct
{
  GObject *obj;
  GParamSpec *spec;
  gulong modified_id;
} ObjectProperty;

static void
free_object_property (ObjectProperty *p)
{
  g_free (p);
}

static void
connect_controller (GObject     *controller,
                    const gchar *signal,
                    GObject     *model,
                    GParamSpec  *spec,
                    GCallback    func)
{
  ObjectProperty *p;

  p = g_new (ObjectProperty, 1);
  p->obj = model;
  p->spec = spec;

  p->modified_id = g_signal_connect_data (controller, signal, func, p,
                                          (GClosureNotify)free_object_property, 0);
  g_object_set_data (controller, "object-property", p);
}

static void
block_controller (GObject *controller)
{
  ObjectProperty *p = g_object_get_data (controller, "object-property");

  if (p)
    g_signal_handler_block (controller, p->modified_id);
}

static void
unblock_controller (GObject *controller)
{
  ObjectProperty *p = g_object_get_data (controller, "object-property");

  if (p)
    g_signal_handler_unblock (controller, p->modified_id);
}

static void
get_property_value (GObject *object, GParamSpec *pspec, GValue *value)
{
  if (is_child_property (pspec))
    {
      GtkWidget *widget = GTK_WIDGET (object);
      GtkWidget *parent = gtk_widget_get_parent (widget);

      gtk_container_child_get_property (GTK_CONTAINER (parent),
                                        widget, pspec->name, value);
    }
  else
    g_object_get_property (object, pspec->name, value);
}

static void
set_property_value (GObject *object, GParamSpec *pspec, GValue *value)
{
  if (is_child_property (pspec))
    {
      GtkWidget *widget = GTK_WIDGET (object);
      GtkWidget *parent = gtk_widget_get_parent (widget);

      gtk_container_child_set_property (GTK_CONTAINER (parent),
                                        widget, pspec->name, value);
    }
  else
    g_object_set_property (object, pspec->name, value);
}

static void
notify_property (GObject *object, GParamSpec *pspec)
{
  if (is_child_property (pspec))
    {
      GtkWidget *widget = GTK_WIDGET (object);
      GtkWidget *parent = gtk_widget_get_parent (widget);

      gtk_container_child_notify (GTK_CONTAINER (parent), widget, pspec->name);
    }
  else
    g_object_notify (object, pspec->name);
}

static void
int_modified (GtkAdjustment *adj, ObjectProperty *p)
{
  GValue val = G_VALUE_INIT;
  
  g_value_init (&val, G_TYPE_INT);
  g_value_set_int (&val, (int) gtk_adjustment_get_value (adj));
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

static void
int_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkAdjustment *adj = GTK_ADJUSTMENT (data);
  GValue val = G_VALUE_INIT;

  g_value_init (&val, G_TYPE_INT);
  get_property_value (object, pspec, &val);

  if (g_value_get_int (&val) != (int)gtk_adjustment_get_value (adj))
    {
      block_controller (G_OBJECT (adj));
      gtk_adjustment_set_value (adj, g_value_get_int (&val));
      unblock_controller (G_OBJECT (adj));
    }

  g_value_unset (&val);
}
static void
uint_modified (GtkAdjustment *adj, ObjectProperty *p)
{
  GValue val = G_VALUE_INIT;
  
  g_value_init (&val, G_TYPE_UINT);
  g_value_set_uint (&val, (guint) gtk_adjustment_get_value (adj));
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

static void
uint_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkAdjustment *adj = GTK_ADJUSTMENT (data);
  GValue val = G_VALUE_INIT;

  g_value_init (&val, G_TYPE_UINT);
  get_property_value (object, pspec, &val);

  if (g_value_get_uint (&val) != (guint)gtk_adjustment_get_value (adj))
    {
      block_controller (G_OBJECT (adj));
      gtk_adjustment_set_value (adj, g_value_get_uint (&val));
      unblock_controller (G_OBJECT (adj));
    }

  g_value_unset (&val);
}

static void
float_modified (GtkAdjustment *adj, ObjectProperty *p)
{
  GValue val = G_VALUE_INIT;
  
  g_value_init (&val, G_TYPE_FLOAT);
  g_value_set_float (&val, (float) gtk_adjustment_get_value (adj));
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

static void
float_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkAdjustment *adj = GTK_ADJUSTMENT (data);
  GValue val = G_VALUE_INIT;

  g_value_init (&val, G_TYPE_FLOAT);
  get_property_value (object, pspec, &val);

  if (g_value_get_float (&val) != (float) gtk_adjustment_get_value (adj))
    {
      block_controller (G_OBJECT (adj));
      gtk_adjustment_set_value (adj, g_value_get_float (&val));
      unblock_controller (G_OBJECT (adj));
    }

  g_value_unset (&val);
}

static void
double_modified (GtkAdjustment *adj, ObjectProperty *p)
{
  GValue val = G_VALUE_INIT;
  
  g_value_init (&val, G_TYPE_DOUBLE);
  g_value_set_double (&val, gtk_adjustment_get_value (adj));
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

static void
double_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkAdjustment *adj = GTK_ADJUSTMENT (data);
  GValue val = G_VALUE_INIT;

  g_value_init (&val, G_TYPE_DOUBLE);
  get_property_value (object, pspec, &val);

  if (g_value_get_double (&val) != gtk_adjustment_get_value (adj))
    {
      block_controller (G_OBJECT (adj));
      gtk_adjustment_set_value (adj, g_value_get_double (&val));
      unblock_controller (G_OBJECT (adj));
    }

  g_value_unset (&val);
}

static void
string_modified (GtkEntry *entry, ObjectProperty *p)
{
  GValue val = G_VALUE_INIT;
  
  g_value_init (&val, G_TYPE_STRING);
  g_value_set_static_string (&val, gtk_entry_get_text (entry));
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

static void
intern_string_modified (GtkEntry *entry, ObjectProperty *p)
{
  const gchar *s;

  s = g_intern_string (gtk_entry_get_text (entry));
  if (g_str_equal (p->spec->name, "id"))
    gtk_css_node_set_id (GTK_CSS_NODE (p->obj), s);
  else if (g_str_equal (p->spec->name, "name"))
    gtk_css_node_set_name (GTK_CSS_NODE (p->obj), s);
}

static void
string_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkEntry *entry = GTK_ENTRY (data);
  GValue val = G_VALUE_INIT;
  const gchar *str;
  const gchar *text;

  g_value_init (&val, G_TYPE_STRING);
  get_property_value (object, pspec, &val);

  str = g_value_get_string (&val);
  if (str == NULL)
    str = "";
  text = gtk_entry_get_text (entry);
  if (g_strcmp0 (str, text) != 0)
    {
      block_controller (G_OBJECT (entry));
      gtk_entry_set_text (entry, str);
      unblock_controller (G_OBJECT (entry));
    }

  g_value_unset (&val);
}

static void
strv_modified (GtkInspectorStrvEditor *editor, ObjectProperty *p)
{
  GValue val = G_VALUE_INIT;
  gchar **strv;

  g_value_init (&val, G_TYPE_STRV);
  strv = gtk_inspector_strv_editor_get_strv (editor);
  g_value_take_boxed (&val, strv);
  block_notify (G_OBJECT (editor));
  set_property_value (p->obj, p->spec, &val);
  unblock_notify (G_OBJECT (editor));
  g_value_unset (&val);
}

static void
strv_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkInspectorStrvEditor *editor = data;
  GValue val = G_VALUE_INIT;
  gchar **strv;

  g_value_init (&val, G_TYPE_STRV);
  get_property_value (object, pspec, &val);

  strv = g_value_get_boxed (&val);
  block_controller (G_OBJECT (editor));
  gtk_inspector_strv_editor_set_strv (editor, strv);
  unblock_controller (G_OBJECT (editor));

  g_value_unset (&val);
}
static void
bool_modified (GtkToggleButton *tb, ObjectProperty *p)
{
  GValue val = G_VALUE_INIT;

  g_value_init (&val, G_TYPE_BOOLEAN);
  g_value_set_boolean (&val, gtk_toggle_button_get_active (tb));
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

static void
bool_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkToggleButton *tb = GTK_TOGGLE_BUTTON (data);
  GValue val = G_VALUE_INIT;

  g_value_init (&val, G_TYPE_BOOLEAN);
  get_property_value (object, pspec, &val);

  if (g_value_get_boolean (&val) != gtk_toggle_button_get_active (tb))
    {
      block_controller (G_OBJECT (tb));
      gtk_toggle_button_set_active (tb, g_value_get_boolean (&val));
      unblock_controller (G_OBJECT (tb));
    }

  gtk_button_set_label (GTK_BUTTON (tb),
                        g_value_get_boolean (&val) ? "TRUE" : "FALSE");

  g_value_unset (&val);
}

static void
enum_modified (GtkToggleButton *button, ObjectProperty *p)
{
  gint i;
  GEnumClass *eclass;
  GValue val = G_VALUE_INIT;

  if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
    return;

  eclass = G_ENUM_CLASS (g_type_class_peek (p->spec->value_type));
  i = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "index"));

  g_value_init (&val, p->spec->value_type);
  g_value_set_enum (&val, eclass->values[i].value);
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

static void
enum_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkWidget *viewport;
  GtkWidget *box;
  GList *children, *c;
  GValue val = G_VALUE_INIT;
  GEnumClass *eclass;
  gint i, j;

  eclass = G_ENUM_CLASS (g_type_class_peek (pspec->value_type));

  g_value_init (&val, pspec->value_type);
  get_property_value (object, pspec, &val);

  i = 0;
  while (i < eclass->n_values)
    {
      if (eclass->values[i].value == g_value_get_enum (&val))
        break;
      ++i;
    }
  g_value_unset (&val);

  viewport = gtk_bin_get_child (GTK_BIN (data));
  box = gtk_bin_get_child (GTK_BIN (viewport));
  children = gtk_container_get_children (GTK_CONTAINER (box));

  for (c = children; c; c = c->next)
    block_controller (G_OBJECT (c->data));

  for (c = children, j = 0; c; c = c->next, j++)
    {
      if (j == i)
        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (c->data), TRUE);
    }

  for (c = children; c; c = c->next)
    unblock_controller (G_OBJECT (c->data));
}

static void
flags_modified (GtkCheckButton *button, ObjectProperty *p)
{
  gboolean active;
  GFlagsClass *fclass;
  guint flags;
  gint i;
  GValue val = G_VALUE_INIT;

  active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button));
  i = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "index"));
  fclass = G_FLAGS_CLASS (g_type_class_peek (p->spec->value_type));

  g_value_init (&val, p->spec->value_type);
  get_property_value (p->obj, p->spec, &val);
  flags = g_value_get_flags (&val);
  if (active)
    flags |= fclass->values[i].value;
  else
    flags &= ~fclass->values[i].value;
  g_value_set_flags (&val, flags);
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

static void
flags_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GList *children, *c;
  GValue val = G_VALUE_INIT;
  GFlagsClass *fclass;
  guint flags;
  gint i;
  GtkWidget *viewport;
  GtkWidget *box;

  fclass = G_FLAGS_CLASS (g_type_class_peek (pspec->value_type));

  g_value_init (&val, pspec->value_type);
  get_property_value (object, pspec, &val);
  flags = g_value_get_flags (&val);
  g_value_unset (&val);

  viewport = gtk_bin_get_child (GTK_BIN (data));
  box = gtk_bin_get_child (GTK_BIN (viewport));
  children = gtk_container_get_children (GTK_CONTAINER (box));

  for (c = children; c; c = c->next)
    block_controller (G_OBJECT (c->data));

  for (c = children, i = 0; c; c = c->next, i++)
    gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (c->data),
                                  (fclass->values[i].value & flags) != 0);

  for (c = children; c; c = c->next)
    unblock_controller (G_OBJECT (c->data));

  g_list_free (children);
}

static gunichar
unichar_get_value (GtkEntry *entry)
{
  const gchar *text = gtk_entry_get_text (entry);

  if (text[0])
    return g_utf8_get_char (text);
  else
    return 0;
}

static void
unichar_modified (GtkEntry *entry, ObjectProperty *p)
{
  gunichar u = unichar_get_value (entry);
  GValue val = G_VALUE_INIT;

  g_value_init (&val, p->spec->value_type);
  g_value_set_uint (&val, u);
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}
static void
unichar_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkEntry *entry = GTK_ENTRY (data);
  gunichar new_val;
  gunichar old_val = unichar_get_value (entry);
  GValue val = G_VALUE_INIT;
  gchar buf[7];
  gint len;

  g_value_init (&val, pspec->value_type);
  get_property_value (object, pspec, &val);
  new_val = (gunichar)g_value_get_uint (&val);

  if (new_val != old_val)
    {
      if (!new_val)
        len = 0;
      else
        len = g_unichar_to_utf8 (new_val, buf);

      buf[len] = '\0';

      block_controller (G_OBJECT (entry));
      gtk_entry_set_text (entry, buf);
      unblock_controller (G_OBJECT (entry));
    }
}

static void
pointer_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkLabel *label = GTK_LABEL (data);
  gchar *str;
  gpointer ptr;

  g_object_get (object, pspec->name, &ptr, NULL);

  str = g_strdup_printf (_("Pointer: %p"), ptr);
  gtk_label_set_text (label, str);
  g_free (str);
}

static gchar *
object_label (GObject *obj, GParamSpec *pspec)
{
  const gchar *name;

  if (obj)
    name = g_type_name (G_TYPE_FROM_INSTANCE (obj));
  else if (pspec)
    name = g_type_name (G_PARAM_SPEC_VALUE_TYPE (pspec));
  else
    name = C_("type name", "Unknown");
  return g_strdup_printf (_("Object: %p (%s)"), obj, name);
}

static void
object_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkWidget *label, *button;
  gchar *str;
  GObject *obj;

  GList *children = gtk_container_get_children (GTK_CONTAINER (data));
  label = GTK_WIDGET (children->data);
  button = GTK_WIDGET (children->next->data);
  g_object_get (object, pspec->name, &obj, NULL);
  g_list_free (children);

  str = object_label (obj, pspec);

  gtk_label_set_text (GTK_LABEL (label), str);
  gtk_widget_set_sensitive (button, G_IS_OBJECT (obj));

  if (obj)
    g_object_unref (obj);

  g_free (str);
}

static void
object_properties (GtkInspectorPropEditor *editor)
{
  GObject *obj;

  g_object_get (editor->priv->object, editor->priv->name, &obj, NULL);
  if (G_IS_OBJECT (obj))
    g_signal_emit (editor, signals[SHOW_OBJECT], 0, obj, editor->priv->name, "properties");
}

static void
rgba_modified (GtkColorButton *cb, GParamSpec *ignored, ObjectProperty *p)
{
  GValue val = G_VALUE_INIT;

  g_value_init (&val, p->spec->value_type);
  g_object_get_property (G_OBJECT (cb), "rgba", &val);
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

static void
rgba_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkColorChooser *cb = GTK_COLOR_CHOOSER (data);
  GValue val = G_VALUE_INIT;
  GdkRGBA *color;
  GdkRGBA cb_color;

  g_value_init (&val, GDK_TYPE_RGBA);
  get_property_value (object, pspec, &val);

  color = g_value_get_boxed (&val);
  gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (cb), &cb_color);

  if (color != NULL && !gdk_rgba_equal (color, &cb_color))
    {
      block_controller (G_OBJECT (cb));
      gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (cb), color);
      unblock_controller (G_OBJECT (cb));
    }
 g_value_unset (&val);
}

static void
color_modified (GtkColorButton *cb, GParamSpec *ignored, ObjectProperty *p)
{
  GdkRGBA rgba;
  GdkColor color;
  GValue val = G_VALUE_INIT;

  gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (cb), &rgba);
  color.red = 65535 * rgba.red;
  color.green = 65535 * rgba.green;
  color.blue = 65535 * rgba.blue;

  g_value_init (&val, p->spec->value_type);
  g_value_set_boxed (&val, &color);
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

G_GNUC_BEGIN_IGNORE_DEPRECATIONS
static void
color_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkColorChooser *cb = GTK_COLOR_CHOOSER (data);
  GValue val = G_VALUE_INIT;
  GdkColor *color;
  GdkRGBA rgba;

  g_value_init (&val, GDK_TYPE_COLOR);
  get_property_value (object, pspec, &val);
  color = g_value_get_boxed (&val);
  rgba.red = color->red / 65535.0;
  rgba.green = color->green / 65535.0;
  rgba.blue = color->blue / 65535.0;
  rgba.alpha = 1.0;

  if (g_value_get_boxed (&val))
    {
      block_controller (G_OBJECT (cb));
      gtk_color_chooser_set_rgba (cb, &rgba);
      unblock_controller (G_OBJECT (cb));
    }

  g_value_unset (&val);
}
G_GNUC_END_IGNORE_DEPRECATIONS

static void
font_modified (GtkFontChooser *fb, GParamSpec *pspec, ObjectProperty *p)
{
  GValue val = G_VALUE_INIT;

  g_value_init (&val, PANGO_TYPE_FONT_DESCRIPTION);
  g_object_get_property (G_OBJECT (fb), "font-desc", &val);
  set_property_value (p->obj, p->spec, &val);
  g_value_unset (&val);
}

static void
font_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkFontChooser *fb = GTK_FONT_CHOOSER (data);
  GValue val = G_VALUE_INIT;
  const PangoFontDescription *font_desc;
  PangoFontDescription *fb_font_desc;

  g_value_init (&val, PANGO_TYPE_FONT_DESCRIPTION);
  get_property_value (object, pspec, &val);

  font_desc = g_value_get_boxed (&val);
  fb_font_desc = gtk_font_chooser_get_font_desc (fb);

  if (font_desc == NULL ||
      (fb_font_desc != NULL &&
       !pango_font_description_equal (fb_font_desc, font_desc)))
    {
      block_controller (G_OBJECT (fb));
      gtk_font_chooser_set_font_desc (fb, font_desc);
      unblock_controller (G_OBJECT (fb));
    }

  g_value_unset (&val);
  pango_font_description_free (fb_font_desc);
}

static GtkWidget *
property_editor (GObject                *object,
                 GParamSpec             *spec,
                 GtkInspectorPropEditor *editor)
{
  GtkWidget *prop_edit;
  GtkAdjustment *adj;
  gchar *msg;
  GType type = G_PARAM_SPEC_TYPE (spec);

  if (type == G_TYPE_PARAM_INT)
    {
      adj = gtk_adjustment_new (G_PARAM_SPEC_INT (spec)->default_value,
                                G_PARAM_SPEC_INT (spec)->minimum,
                                G_PARAM_SPEC_INT (spec)->maximum,
                                1,
                                MAX ((G_PARAM_SPEC_INT (spec)->maximum - G_PARAM_SPEC_INT (spec)->minimum) / 10, 1),
                                0.0);

      prop_edit = gtk_spin_button_new (adj, 1.0, 0);

      g_object_connect_property (object, spec, G_CALLBACK (int_changed), adj, G_OBJECT (adj)); 

      connect_controller (G_OBJECT (adj), "value_changed",
                          object, spec, G_CALLBACK (int_modified));
    }
  else if (type == G_TYPE_PARAM_UINT)
    {
      adj = gtk_adjustment_new (G_PARAM_SPEC_UINT (spec)->default_value,
                                G_PARAM_SPEC_UINT (spec)->minimum,
                                G_PARAM_SPEC_UINT (spec)->maximum,
                                1,
                                MAX ((G_PARAM_SPEC_UINT (spec)->maximum - G_PARAM_SPEC_UINT (spec)->minimum) / 10, 1),
                                0.0);

      prop_edit = gtk_spin_button_new (adj, 1.0, 0);

      g_object_connect_property (object, spec,
                                 G_CALLBACK (uint_changed),
                                 adj, G_OBJECT (adj));

      connect_controller (G_OBJECT (adj), "value_changed",
                          object, spec, G_CALLBACK (uint_modified));
    }
  else if (type == G_TYPE_PARAM_FLOAT)
    {
      adj = gtk_adjustment_new (G_PARAM_SPEC_FLOAT (spec)->default_value,
                                G_PARAM_SPEC_FLOAT (spec)->minimum,
                                G_PARAM_SPEC_FLOAT (spec)->maximum,
                                0.1,
                                MAX ((G_PARAM_SPEC_FLOAT (spec)->maximum - G_PARAM_SPEC_FLOAT (spec)->minimum) / 10, 0.1),
                                0.0);

      prop_edit = gtk_spin_button_new (adj, 0.1, 2);

      g_object_connect_property (object, spec,
                                 G_CALLBACK (float_changed),
                                 adj, G_OBJECT (adj));

      connect_controller (G_OBJECT (adj), "value_changed",
                          object, spec, G_CALLBACK (float_modified));
    }
  else if (type == G_TYPE_PARAM_DOUBLE)
    {
      adj = gtk_adjustment_new (G_PARAM_SPEC_DOUBLE (spec)->default_value,
                                G_PARAM_SPEC_DOUBLE (spec)->minimum,
                                G_PARAM_SPEC_DOUBLE (spec)->maximum,
                                0.1,
                                1.0,
                                0.0);

      prop_edit = gtk_spin_button_new (adj, 0.1, 2);

      g_object_connect_property (object, spec,
                                 G_CALLBACK (double_changed),
                                 adj, G_OBJECT (adj));

      connect_controller (G_OBJECT (adj), "value_changed",
                          object, spec, G_CALLBACK (double_modified));
    }
  else if (type == G_TYPE_PARAM_STRING)
    {
      prop_edit = gtk_entry_new ();

      g_object_connect_property (object, spec,
                                 G_CALLBACK (string_changed),
                                 prop_edit, G_OBJECT (prop_edit));

      if (GTK_IS_CSS_NODE (object))
        connect_controller (G_OBJECT (prop_edit), "changed",
                            object, spec, G_CALLBACK (intern_string_modified));
      else
        connect_controller (G_OBJECT (prop_edit), "changed",
                            object, spec, G_CALLBACK (string_modified));
    }
  else if (type == G_TYPE_PARAM_BOOLEAN)
    {
      prop_edit = gtk_toggle_button_new_with_label ("");

      g_object_connect_property (object, spec,
                                 G_CALLBACK (bool_changed),
                                 prop_edit, G_OBJECT (prop_edit));

      connect_controller (G_OBJECT (prop_edit), "toggled",
                          object, spec, G_CALLBACK (bool_modified));
    }
  else if (type == G_TYPE_PARAM_ENUM)
    {
      {
        GtkWidget *box;
        GEnumClass *eclass;
        GtkWidget *first;
        gint j;

        prop_edit = gtk_scrolled_window_new (NULL, NULL);
        g_object_set (prop_edit,
                      "expand", TRUE,
                      "hscrollbar-policy", GTK_POLICY_NEVER,
                      "vscrollbar-policy", GTK_POLICY_NEVER,
                      NULL);
        box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
        gtk_widget_show (box);
        gtk_container_add (GTK_CONTAINER (prop_edit), box);

        eclass = G_ENUM_CLASS (g_type_class_ref (spec->value_type));

        j = 0;
        first = NULL;
        while (j < eclass->n_values)
          {
            GtkWidget *b;

            b = gtk_radio_button_new_with_label_from_widget ((GtkRadioButton*)first, eclass->values[j].value_name);
            if (first == NULL)
              first = b;
            g_object_set_data (G_OBJECT (b), "index", GINT_TO_POINTER (j));
            gtk_widget_show (b);
            gtk_box_pack_start (GTK_BOX (box), b, FALSE, FALSE, 0);
            connect_controller (G_OBJECT (b), "toggled",
                                object, spec, G_CALLBACK (enum_modified));
            ++j;
          }

        if (j >= 10)
          g_object_set (prop_edit, "vscrollbar-policy", GTK_POLICY_AUTOMATIC, NULL);

        g_type_class_unref (eclass);

        g_object_connect_property (object, spec,
                                   G_CALLBACK (enum_changed),
                                   prop_edit, G_OBJECT (prop_edit));
      }
    }
  else if (type == G_TYPE_PARAM_FLAGS)
    {
      {
        GtkWidget *box;
        GFlagsClass *fclass;
        gint j;

        prop_edit = gtk_scrolled_window_new (NULL, NULL);
        g_object_set (prop_edit,
                      "expand", TRUE,
                      "hscrollbar-policy", GTK_POLICY_NEVER,
                      "vscrollbar-policy", GTK_POLICY_NEVER,
                      NULL);
        box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
        gtk_widget_show (box);
        gtk_container_add (GTK_CONTAINER (prop_edit), box);

        fclass = G_FLAGS_CLASS (g_type_class_ref (spec->value_type));

        for (j = 0; j < fclass->n_values; j++)
          {
            GtkWidget *b;

            b = gtk_check_button_new_with_label (fclass->values[j].value_name);
            g_object_set_data (G_OBJECT (b), "index", GINT_TO_POINTER (j));
            gtk_widget_show (b);
            gtk_box_pack_start (GTK_BOX (box), b, FALSE, FALSE, 0);
            connect_controller (G_OBJECT (b), "toggled",
                                object, spec, G_CALLBACK (flags_modified));
          }

        if (j >= 10)
          g_object_set (prop_edit, "vscrollbar-policy", GTK_POLICY_AUTOMATIC, NULL);

        g_type_class_unref (fclass);

        g_object_connect_property (object, spec,
                                   G_CALLBACK (flags_changed),
                                   prop_edit, G_OBJECT (prop_edit));
      }
    }
  else if (type == G_TYPE_PARAM_UNICHAR)
    {
      prop_edit = gtk_entry_new ();
      gtk_entry_set_max_length (GTK_ENTRY (prop_edit), 1);

      g_object_connect_property (object, spec,
                                 G_CALLBACK (unichar_changed),
                                 prop_edit, G_OBJECT (prop_edit));

      connect_controller (G_OBJECT (prop_edit), "changed",
                          object, spec, G_CALLBACK (unichar_modified));
    }
  else if (type == G_TYPE_PARAM_POINTER)
    {
      prop_edit = gtk_label_new ("");

      g_object_connect_property (object, spec,
                                 G_CALLBACK (pointer_changed),
                                 prop_edit, G_OBJECT (prop_edit));
    }
  else if (type == G_TYPE_PARAM_OBJECT)
    {
      GtkWidget *label, *button;

      prop_edit = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);

      label = gtk_label_new ("");
      button = gtk_button_new_with_label (_("Properties"));
      g_signal_connect_swapped (button, "clicked",
                                G_CALLBACK (object_properties),
                                editor);
      gtk_container_add (GTK_CONTAINER (prop_edit), label);
      gtk_container_add (GTK_CONTAINER (prop_edit), button);
      gtk_widget_show (label);
      gtk_widget_show (button);

      g_object_connect_property (object, spec,
                                 G_CALLBACK (object_changed),
                                 prop_edit, G_OBJECT (label));
    }
  else if (type == G_TYPE_PARAM_BOXED &&
           G_PARAM_SPEC_VALUE_TYPE (spec) == GDK_TYPE_RGBA)
    {
      prop_edit = gtk_color_chooser_widget_new ();
      gtk_color_chooser_set_use_alpha (GTK_COLOR_CHOOSER (prop_edit), TRUE);

      g_object_connect_property (object, spec,
                                 G_CALLBACK (rgba_changed),
                                 prop_edit, G_OBJECT (prop_edit));

      connect_controller (G_OBJECT (prop_edit), "notify::rgba",
                          object, spec, G_CALLBACK (rgba_modified));
    }
  else if (type == G_TYPE_PARAM_BOXED &&
           G_PARAM_SPEC_VALUE_TYPE (spec) == g_type_from_name ("GdkColor"))
    {
      prop_edit = gtk_color_chooser_widget_new ();
      gtk_color_chooser_set_use_alpha (GTK_COLOR_CHOOSER (prop_edit), FALSE);

      g_object_connect_property (object, spec,
                                 G_CALLBACK (color_changed),
                                 prop_edit, G_OBJECT (prop_edit));

      connect_controller (G_OBJECT (prop_edit), "notify::rgba",
                          object, spec, G_CALLBACK (color_modified));
    }
  else if (type == G_TYPE_PARAM_BOXED &&
           G_PARAM_SPEC_VALUE_TYPE (spec) == PANGO_TYPE_FONT_DESCRIPTION)
    {
      prop_edit = gtk_font_chooser_widget_new ();

      g_object_connect_property (object, spec,
                                 G_CALLBACK (font_changed),
                                 prop_edit, G_OBJECT (prop_edit));

      connect_controller (G_OBJECT (prop_edit), "notify::font-desc",
                          object, spec, G_CALLBACK (font_modified));
    }
  else if (type == G_TYPE_PARAM_BOXED &&
           G_PARAM_SPEC_VALUE_TYPE (spec) == G_TYPE_STRV)
    {
      prop_edit = g_object_new (gtk_inspector_strv_editor_get_type (),
                                "visible", TRUE,
                                NULL);

      g_object_connect_property (object, spec,
                                 G_CALLBACK (strv_changed),
                                 prop_edit, G_OBJECT (prop_edit));

      connect_controller (G_OBJECT (prop_edit), "changed",
                          object, spec, G_CALLBACK (strv_modified));

      gtk_widget_set_halign (prop_edit, GTK_ALIGN_START);
      gtk_widget_set_valign (prop_edit, GTK_ALIGN_CENTER);
    }
  else
    {
      msg = g_strdup_printf (_("Uneditable property type: %s"),
                             g_type_name (G_PARAM_SPEC_TYPE (spec)));
      prop_edit = gtk_label_new (msg);
      g_free (msg);
      gtk_widget_set_halign (prop_edit, GTK_ALIGN_START);
      gtk_widget_set_valign (prop_edit, GTK_ALIGN_CENTER);
    }

  if (g_param_spec_get_blurb (spec))
    gtk_widget_set_tooltip_text (prop_edit, g_param_spec_get_blurb (spec));

  notify_property (object, spec);

  return prop_edit;
}

static void
gtk_inspector_prop_editor_init (GtkInspectorPropEditor *editor)
{
  editor->priv = gtk_inspector_prop_editor_get_instance_private (editor);
  g_object_set (editor,
                "orientation", GTK_ORIENTATION_VERTICAL,
                "spacing", 10,
                "margin", 10,
                NULL);
}

static GtkTreeModel *
gtk_cell_layout_get_model (GtkCellLayout *layout)
{
  if (GTK_IS_TREE_VIEW_COLUMN (layout))
    return gtk_tree_view_get_model (GTK_TREE_VIEW (gtk_tree_view_column_get_tree_view (GTK_TREE_VIEW_COLUMN (layout))));
  else if (GTK_IS_ICON_VIEW (layout))
    return gtk_icon_view_get_model (GTK_ICON_VIEW (layout));
  else if (GTK_IS_COMBO_BOX (layout)) 
    return gtk_combo_box_get_model (GTK_COMBO_BOX (layout));
  else
    return NULL;
}

static GtkWidget *
gtk_cell_layout_get_widget (GtkCellLayout *layout)
{
  if (GTK_IS_TREE_VIEW_COLUMN (layout))
    return gtk_tree_view_column_get_tree_view (GTK_TREE_VIEW_COLUMN (layout));
  else if (GTK_IS_WIDGET (layout))
    return GTK_WIDGET (layout);
  else
    return NULL;
}

static void
model_properties (GtkButton              *button,
                  GtkInspectorPropEditor *editor)
{
  GObject *model;

  model = g_object_get_data (G_OBJECT (button), "model");
  g_signal_emit (editor, signals[SHOW_OBJECT], 0, model, "model", "data");
}

static void
attribute_mapping_changed (GtkComboBox            *combo,
                           GtkInspectorPropEditor *editor)
{
  gint col;
  gpointer layout;
  GtkCellRenderer *cell;
  GtkCellArea *area;

  col = gtk_combo_box_get_active (combo) - 1;
  layout = g_object_get_data (editor->priv->object, "gtk-inspector-cell-layout");
  if (GTK_IS_CELL_LAYOUT (layout))
    {
      cell = GTK_CELL_RENDERER (editor->priv->object);
      area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (layout));
      gtk_cell_area_attribute_disconnect (area, cell, editor->priv->name);
      if (col != -1)
        gtk_cell_area_attribute_connect (area, cell, editor->priv->name, col);
      gtk_widget_set_sensitive (editor->priv->editor, col == -1);
      notify_property (editor->priv->object, find_property (editor));
      gtk_widget_queue_draw (gtk_cell_layout_get_widget (GTK_CELL_LAYOUT (layout)));
    }
}

static GtkWidget *
attribute_editor (GObject                *object,
                  GParamSpec             *spec,
                  GtkInspectorPropEditor *editor)
{
  gpointer layout;
  GtkCellArea *area;
  GtkTreeModel *model = NULL;
  gint col = -1;
  GtkWidget *label;
  GtkWidget *button;
  GtkWidget *vbox;
  GtkWidget *box;
  GtkWidget *combo;
  gchar *text;
  gint i;
  gboolean sensitive;
  GtkCellRenderer *renderer;
  GtkListStore *store;
  GtkTreeIter iter;

  layout = g_object_get_data (object, "gtk-inspector-cell-layout");
  if (GTK_IS_CELL_LAYOUT (layout))
    {
      area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (layout));
      col = gtk_cell_area_attribute_get_column (area,
                                                GTK_CELL_RENDERER (object), 
                                                editor->priv->name);
      model = gtk_cell_layout_get_model (GTK_CELL_LAYOUT (layout));
    }

  vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);

  label = gtk_label_new (_("Attribute mapping"));
  gtk_widget_set_margin_top (label, 10);
  gtk_container_add (GTK_CONTAINER (vbox), label);

  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
  gtk_container_add (GTK_CONTAINER (box), gtk_label_new (_("Model:")));
  text = g_strdup_printf (_("%p (%s)"), model, model ? g_type_name (G_TYPE_FROM_INSTANCE (model)) : "null" );
  gtk_container_add (GTK_CONTAINER (box), gtk_label_new (text));
  g_free (text);
  button = gtk_button_new_with_label (_("Properties"));
  g_object_set_data (G_OBJECT (button), "model", model);
  g_signal_connect (button, "clicked", G_CALLBACK (model_properties), editor);
  gtk_container_add (GTK_CONTAINER (box), button);
  gtk_container_add (GTK_CONTAINER (vbox), box);

  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
  gtk_container_add (GTK_CONTAINER (box), gtk_label_new (_("Column:")));
  store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_BOOLEAN);
  combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store));
  renderer = gtk_cell_renderer_text_new ();
  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, FALSE);
  gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer,
                                  "text", 0,
                                  "sensitive", 1,
                                  NULL);
  gtk_list_store_append (store, &iter);
  gtk_list_store_set (store, &iter, 0, C_("property name", "None"), 1, TRUE, -1);
  for (i = 0; i < gtk_tree_model_get_n_columns (model); i++)
    {
      text = g_strdup_printf ("%d", i);
      sensitive = g_value_type_transformable (gtk_tree_model_get_column_type (model, i),
                                              spec->value_type);
      gtk_list_store_append (store, &iter);
      gtk_list_store_set (store, &iter, 0, text, 1, sensitive, -1);
      g_free (text);
    }
  gtk_combo_box_set_active (GTK_COMBO_BOX (combo), col + 1);
  attribute_mapping_changed (GTK_COMBO_BOX (combo), editor);
  g_signal_connect (combo, "changed",
                    G_CALLBACK (attribute_mapping_changed), editor);
  gtk_container_add (GTK_CONTAINER (box), combo);
  gtk_container_add (GTK_CONTAINER (vbox), box);
  gtk_widget_show_all (vbox);

  return vbox;
}

static GtkWidget *
action_ancestor (GtkWidget *widget)
{
  if (GTK_IS_MENU (widget))
    return gtk_menu_get_attach_widget (GTK_MENU (widget));
  else if (GTK_IS_POPOVER (widget))
    return gtk_popover_get_relative_to (GTK_POPOVER (widget));
  else
    return gtk_widget_get_parent (widget);
}

static GObject *
find_action_owner (GtkActionable *actionable)
{
  GtkWidget *widget = GTK_WIDGET (actionable);
  const gchar *full_name;
  const gchar *dot;
  const gchar *name;
  gchar *prefix;
  GtkWidget *win;
  GActionGroup *group;

  full_name = gtk_actionable_get_action_name (actionable);
  if (!full_name)
    return NULL;

  dot = strchr (full_name, '.');
  prefix = g_strndup (full_name, dot - full_name);
  name = dot + 1;

  win = gtk_widget_get_ancestor (widget, GTK_TYPE_APPLICATION_WINDOW);
  if (g_strcmp0 (prefix, "win") == 0)
    {
      if (G_IS_OBJECT (win))
        return (GObject *)win;
    }
  else if (g_strcmp0 (prefix, "app") == 0)
    {  
      if (GTK_IS_WINDOW (win))
        return (GObject *)gtk_window_get_application (GTK_WINDOW (win));
    }

  while (widget != NULL)
    {
      group = gtk_widget_get_action_group (widget, prefix);
      if (group && g_action_group_has_action (group, name))
        return (GObject *)widget;
      widget = action_ancestor (widget);
    }

  return NULL;  
}

static void
show_action_owner (GtkButton              *button,
                   GtkInspectorPropEditor *editor)
{
  GObject *owner;

  owner = g_object_get_data (G_OBJECT (button), "owner");
  g_signal_emit (editor, signals[SHOW_OBJECT], 0, owner, NULL, "actions");
}

static GtkWidget *
action_editor (GObject                *object,
               GtkInspectorPropEditor *editor)
{
  GtkWidget *vbox;
  GtkWidget *label;
  GtkWidget *box;
  GtkWidget *button;
  GObject *owner;
  gchar *text;

  owner = find_action_owner (GTK_ACTIONABLE (object));

  vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
  if (owner)
    {
      label = gtk_label_new (_("Action"));
      gtk_widget_set_margin_top (label, 10);
      gtk_container_add (GTK_CONTAINER (vbox), label);
      box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
      text = g_strdup_printf (_("Defined at: %p (%s)"),
                              owner, g_type_name_from_instance ((GTypeInstance *)owner));
      gtk_container_add (GTK_CONTAINER (box), gtk_label_new (text));
      g_free (text);
      button = gtk_button_new_with_label (_("Properties"));
      g_object_set_data (G_OBJECT (button), "owner", owner);
      g_signal_connect (button, "clicked",
                        G_CALLBACK (show_action_owner), editor);
      gtk_container_add (GTK_CONTAINER (box), button);
      gtk_container_add (GTK_CONTAINER (vbox), box);
      gtk_widget_show_all (vbox);
    }

  return vbox;
}

static void
binding_object_properties (GtkButton *button, GtkInspectorPropEditor *editor)
{
  GObject *obj;

  obj = (GObject *)g_object_get_data (G_OBJECT (button), "object");
  if (G_IS_OBJECT (obj))
    g_signal_emit (editor, signals[SHOW_OBJECT], 0, obj, NULL, "properties");
}

static void
add_binding_info (GtkInspectorPropEditor *editor)
{
  GObject *object;
  const gchar *name;
  GHashTable *bindings;
  GHashTableIter iter;
  GBinding *binding;
  GtkWidget *row;
  GtkWidget *button;
  gchar *str;
  GObject *other;
  const gchar *property;
  const gchar *direction;
  const gchar *tip;
  GtkWidget *label;

  object = editor->priv->object;
  name = editor->priv->name;

  /* Note: this is accessing private GBinding details, so keep it
   * in sync with the implementation in GObject
   */
  bindings = (GHashTable *)g_object_get_data (G_OBJECT (object), "g-binding");
  if (!bindings)
    return;

  g_hash_table_iter_init (&iter, bindings);
  while (g_hash_table_iter_next (&iter, (gpointer*)&binding, NULL))
    {
      if (g_binding_get_source (binding) == object &&
          g_str_equal (g_binding_get_source_property (binding), name))
        {
          other = g_binding_get_target (binding);
          property = g_binding_get_target_property (binding);
          if (g_binding_get_flags (binding) & G_BINDING_INVERT_BOOLEAN)
            {
              direction = "↛";
              tip = _("inverted");
            }
          else
            {
              direction = "→";
              tip = NULL;
            }
        }
      else if (g_binding_get_target (binding) == object &&
               g_str_equal (g_binding_get_target_property (binding), name))
        {
          other = g_binding_get_source (binding);
          property = g_binding_get_source_property (binding);
          if (g_binding_get_flags (binding) & G_BINDING_INVERT_BOOLEAN)
            {
              direction = "↚";
              tip = _("inverted");
            }
          else
            {
              direction = "←";
              tip = NULL;
            }
        }
      else
        continue;
     
      if (g_binding_get_flags (binding) & G_BINDING_BIDIRECTIONAL)
        {
          if (g_binding_get_flags (binding) & G_BINDING_INVERT_BOOLEAN)
            {
              direction = "↮";
              tip = _("bidirectional, inverted");
            }
          else
            {
              direction = "↔";
              tip = _("bidirectional");
            }
        }

      row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
      gtk_container_add (GTK_CONTAINER (row), gtk_label_new (_("Binding:")));
      label = gtk_label_new (direction);
      if (tip)
        gtk_widget_set_tooltip_text (label, tip);
      gtk_container_add (GTK_CONTAINER (row), label);
      str = g_strdup_printf ("%p :: %s", other, property);
      label = gtk_label_new (str);
      gtk_container_add (GTK_CONTAINER (row), label);
      g_free (str);
      button = gtk_button_new_with_label (_("Properties"));
      g_object_set_data (G_OBJECT (button), "object", other);
      g_signal_connect (button, "clicked",
                        G_CALLBACK (binding_object_properties), editor);
      gtk_container_add (GTK_CONTAINER (row), button);

       gtk_widget_show_all (row);
       gtk_container_add (GTK_CONTAINER (editor), row);
    }
}

/* Note: Slightly nasty that we have to poke at the
 * GSettingsSchemaKey internals here. Keep this in
 * sync with the implementation in GIO!
 */
struct _GSettingsSchemaKey
{
  GSettingsSchema *schema;
  const gchar *name;

  guint is_flags : 1;
  guint is_enum  : 1;

  const guint32 *strinfo;
  gsize strinfo_length;

  const gchar *unparsed;
  gchar lc_char;

  const GVariantType *type;
  GVariant *minimum, *maximum;
  GVariant *default_value;

  gint ref_count;
};

typedef struct
{
  GSettingsSchemaKey key;
  GSettings *settings;
  GObject *object;

  GSettingsBindGetMapping get_mapping;
  GSettingsBindSetMapping set_mapping;
  gpointer user_data;
  GDestroyNotify destroy;

  guint writable_handler_id;
  guint property_handler_id;
  const GParamSpec *property;
  guint key_handler_id;

  /* prevent recursion */
  gboolean running;
} GSettingsBinding;

static void
add_attribute_info (GtkInspectorPropEditor *editor,
                    GParamSpec             *spec)
{
  if (GTK_IS_CELL_RENDERER (editor->priv->object))
    gtk_container_add (GTK_CONTAINER (editor),
                       attribute_editor (editor->priv->object, spec, editor));
}

static void
add_actionable_info (GtkInspectorPropEditor *editor)
{
  if (GTK_IS_ACTIONABLE (editor->priv->object) &&
      g_strcmp0 (editor->priv->name, "action-name") == 0)
    gtk_container_add (GTK_CONTAINER (editor),
                       action_editor (editor->priv->object, editor));
}

static void
add_settings_info (GtkInspectorPropEditor *editor)
{
  gchar *key;
  GSettingsBinding *binding;
  GObject *object;
  const gchar *name;
  const gchar *direction;
  const gchar *tip;
  GtkWidget *row;
  GtkWidget *label;
  gchar *str;

  object = editor->priv->object;
  name = editor->priv->name;

  key = g_strconcat ("gsettingsbinding-", name, NULL);
  binding = (GSettingsBinding *)g_object_get_data (object, key);
  g_free (key);

  if (!binding)
    return;

  if (binding->key_handler_id && binding->property_handler_id)
    {
      direction = "↔";
      tip = _("bidirectional");
    }
  else if (binding->key_handler_id)
    {
      direction = "←";
      tip = NULL;
    }
  else if (binding->property_handler_id)
    {
      direction = "→";
      tip = NULL;
    }
  else
    {
      direction = "?";
      tip = NULL;
    }

  row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
  gtk_container_add (GTK_CONTAINER (row), gtk_label_new (_("Setting:")));
  label = gtk_label_new (direction);
  if (tip)
    gtk_widget_set_tooltip_text (label, tip);
  gtk_container_add (GTK_CONTAINER (row), label);
  
  str = g_strdup_printf ("%s %s",
                         g_settings_schema_get_id (binding->key.schema),
                         binding->key.name);
  label = gtk_label_new (str);
  gtk_container_add (GTK_CONTAINER (row), label);
  g_free (str);

  gtk_widget_show_all (row);
  gtk_container_add (GTK_CONTAINER (editor), row);
}

static void
reset_setting (GtkInspectorPropEditor *editor)
{
  gtk_settings_reset_property (GTK_SETTINGS (editor->priv->object),
                               editor->priv->name);
}

static void
add_gtk_settings_info (GtkInspectorPropEditor *editor)
{
  GObject *object;
  const gchar *name;
  GtkWidget *row;
  const gchar *source;
  GtkWidget *button;

  object = editor->priv->object;
  name = editor->priv->name;

  if (!GTK_IS_SETTINGS (object))
    return;

  row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
  gtk_container_add (GTK_CONTAINER (row), gtk_label_new (_("Source:")));

  button = gtk_button_new_with_label (_("Reset"));
  g_signal_connect_swapped (button, "clicked", G_CALLBACK (reset_setting), editor);

  gtk_widget_set_halign (button, GTK_ALIGN_END);
  gtk_widget_show (button);
  gtk_widget_set_sensitive (button, FALSE);
  gtk_box_pack_end (GTK_BOX (row), button, FALSE, FALSE, 0);

  switch (_gtk_settings_get_setting_source (GTK_SETTINGS (object), name))
    {
    case GTK_SETTINGS_SOURCE_DEFAULT:
      source = _("Default");
      break;
    case GTK_SETTINGS_SOURCE_THEME:
      source = _("Theme");
      break;
    case GTK_SETTINGS_SOURCE_XSETTING:
      source = _("XSettings");
      break;
    case GTK_SETTINGS_SOURCE_APPLICATION:
      gtk_widget_set_sensitive (button, TRUE);
      source = _("Application");
      break;
    default:
      source = _("Unknown");
      break;
    }
  gtk_container_add (GTK_CONTAINER (row), gtk_label_new (source));

  gtk_widget_show_all (row);
  gtk_container_add (GTK_CONTAINER (editor), row);
}

static void
constructed (GObject *object)
{
  GtkInspectorPropEditor *editor = GTK_INSPECTOR_PROP_EDITOR (object);
  GParamSpec *spec;
  GtkWidget *label;
  gboolean can_modify;

  spec = find_property (editor);

  label = gtk_label_new (g_param_spec_get_nick (spec));
  gtk_widget_show (label);
  gtk_container_add (GTK_CONTAINER (editor), label);

  can_modify = ((spec->flags & G_PARAM_WRITABLE) != 0 &&
                (spec->flags & G_PARAM_CONSTRUCT_ONLY) == 0);

  if ((spec->flags & G_PARAM_CONSTRUCT_ONLY) != 0)
    label = gtk_label_new ("(construct-only)");
  else if ((spec->flags & G_PARAM_WRITABLE) == 0)
    label = gtk_label_new ("(not writable)");
  else
    label = NULL;

  if (label)
    {
      gtk_widget_show (label);
      gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_DIM_LABEL);
      gtk_container_add (GTK_CONTAINER (editor), label);
    }

  /* By reaching this, we already know the property is readable.
   * Since all we can do for a GObject is dive down into it's properties
   * and inspect bindings and such, pretend to be mutable.
   */
  if (g_type_is_a (spec->value_type, G_TYPE_OBJECT))
    can_modify = TRUE;

  if (!can_modify)
    return;

  editor->priv->editor = property_editor (editor->priv->object, spec, editor);
  gtk_widget_show (editor->priv->editor);
  gtk_container_add (GTK_CONTAINER (editor), editor->priv->editor);

  add_attribute_info (editor, spec);
  add_actionable_info (editor);
  add_binding_info (editor);
  add_settings_info (editor);
  add_gtk_settings_info (editor);
}

static void
finalize (GObject *object)
{
  GtkInspectorPropEditor *editor = GTK_INSPECTOR_PROP_EDITOR (object);

  g_free (editor->priv->name);

  G_OBJECT_CLASS (gtk_inspector_prop_editor_parent_class)->finalize (object);
}

static void
get_property (GObject    *object,
              guint       param_id,
              GValue     *value,
              GParamSpec *pspec)
{
  GtkInspectorPropEditor *r = GTK_INSPECTOR_PROP_EDITOR (object);

  switch (param_id)
    {
    case PROP_OBJECT:
      g_value_set_object (value, r->priv->object);
      break;

    case PROP_NAME:
      g_value_set_string (value, r->priv->name);
      break;

    case PROP_IS_CHILD_PROPERTY:
      g_value_set_boolean (value, r->priv->is_child_property);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
      break;
    }
}

static void
set_property (GObject      *object,
              guint         param_id,
              const GValue *value,
              GParamSpec   *pspec)
{
  GtkInspectorPropEditor *r = GTK_INSPECTOR_PROP_EDITOR (object);

  switch (param_id)
    {
    case PROP_OBJECT:
      r->priv->object = g_value_get_object (value);
      break;

    case PROP_NAME:
      g_free (r->priv->name);
      r->priv->name = g_value_dup_string (value);
      break;

    case PROP_IS_CHILD_PROPERTY:
      r->priv->is_child_property = g_value_get_boolean (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
      break;
    } 
}

static void
gtk_inspector_prop_editor_class_init (GtkInspectorPropEditorClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = constructed;
  object_class->finalize = finalize;
  object_class->get_property = get_property;
  object_class->set_property = set_property;

  signals[SHOW_OBJECT] =
    g_signal_new ("show-object",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (GtkInspectorPropEditorClass, show_object),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 3, G_TYPE_OBJECT, G_TYPE_STRING, G_TYPE_STRING);

  g_object_class_install_property (object_class, PROP_OBJECT,
      g_param_spec_object ("object", "Object", "The object owning the property",
                           G_TYPE_OBJECT, G_PARAM_READWRITE|G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "Name", "The property name",
                           NULL, G_PARAM_READWRITE|G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_IS_CHILD_PROPERTY,
      g_param_spec_boolean ("is-child-property", "Child property", "Whether this is a child property",
                            FALSE, G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
}

GtkWidget *
gtk_inspector_prop_editor_new (GObject     *object,
                               const gchar *name,
                               gboolean     is_child_property)
{
  return g_object_new (GTK_TYPE_INSPECTOR_PROP_EDITOR,
                       "object", object,
                       "name", name,
                       "is-child-property", is_child_property,
                       NULL);
}

gboolean
gtk_inspector_prop_editor_should_expand (GtkInspectorPropEditor *editor)
{
  if (GTK_IS_SCROLLED_WINDOW (editor->priv->editor))
    {
      GtkPolicyType policy;

      g_object_get (editor->priv->editor, "vscrollbar-policy", &policy, NULL);
      if (policy != GTK_POLICY_NEVER)
        return TRUE;
    }

  return FALSE;
}


// vim: set et:
