Blob Blame History Raw
/*
 * glade-previewer.c
 *
 * Copyright (C) 2013-2016 Juan Pablo Ugarte
   *
 * Author: Juan Pablo Ugarte <juanpablougarte@gmail.com>
 *
 * 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.1 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 program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */
#include <config.h>

#include "glade-previewer.h"
#include <glib/gi18n-lib.h>
#include <glib/gprintf.h>
#include <cairo-pdf.h>
#include <cairo-svg.h>
#include <cairo-ps.h>

struct _GladePreviewerPrivate
{
  GtkWidget *widget;    /* Preview widget */
  GList     *objects;   /* SlideShow objects */
  GtkWidget *dialog;    /* Dialog to show messages */
  GtkWidget *textview;

  GtkCssProvider *css_provider;
  GFileMonitor *css_monitor;
  gchar *css_file;
  gchar *extension;

  gboolean print_handlers;
};

G_DEFINE_TYPE_WITH_PRIVATE (GladePreviewer, glade_previewer, G_TYPE_OBJECT);

static void
glade_previewer_init (GladePreviewer *preview)
{
  GladePreviewerPrivate *priv = glade_previewer_get_instance_private (preview);

  preview->priv = priv;
}

static void
glade_previewer_dispose (GObject *object)
{
  GladePreviewerPrivate *priv = GLADE_PREVIEWER (object)->priv;

  g_list_free (priv->objects);
  
  priv->objects = NULL;
  priv->dialog = NULL;
  g_clear_object (&priv->css_provider);
  g_clear_object (&priv->css_monitor);

  G_OBJECT_CLASS (glade_previewer_parent_class)->dispose (object);
}

static void
glade_previewer_finalize (GObject *object)
{
  GladePreviewerPrivate *priv = GLADE_PREVIEWER (object)->priv;

  g_free (priv->css_file);
  g_free (priv->extension);

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

static gboolean 
on_widget_key_press_event (GtkWidget      *widget,
                           GdkEventKey    *event,
                           GladePreviewer *preview)
{
  GladePreviewerPrivate *priv = preview->priv;
  GList *node = NULL;
  GtkStack *stack;
  gchar *extension;

  if (priv->objects)
    {
      stack = GTK_STACK (gtk_bin_get_child (GTK_BIN (priv->widget)));
      node = g_list_find (priv->objects, gtk_stack_get_visible_child (stack));
    }

  switch (event->keyval)
    {
      case GDK_KEY_Page_Up:
        if (node && node->prev)
          gtk_stack_set_visible_child  (stack, node->prev->data);
        return TRUE;
      break;
      case GDK_KEY_Page_Down:
        if (node && node->next)
          gtk_stack_set_visible_child  (stack, node->next->data);
        return TRUE;
      break;
      case GDK_KEY_F5:
        extension = "svg";
      break;
      case GDK_KEY_F6:
        extension = "ps";
      break;
      case GDK_KEY_F7:
        extension = "pdf";
      break;
      case GDK_KEY_F8:
        extension = priv->extension ? priv->extension : "png";
      break;
      case GDK_KEY_F11:
        if (gdk_window_get_state (gtk_widget_get_window (widget)) & GDK_WINDOW_STATE_FULLSCREEN)
          gtk_window_unfullscreen (GTK_WINDOW (widget));
        else
          gtk_window_fullscreen (GTK_WINDOW (widget));

        return TRUE;
      break;
      default:
        return FALSE;
      break;
    }

  if (extension)
    {
      gchar *tmp_file = g_strdup_printf ("glade-screenshot-XXXXXX.%s", extension); 

      g_mkstemp (tmp_file);
      glade_previewer_screenshot (preview, FALSE, tmp_file);
      g_free (tmp_file);

      return TRUE;
    }

  return FALSE;
}

static void
glade_previewer_class_init (GladePreviewerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = glade_previewer_dispose;
  object_class->finalize = glade_previewer_finalize;
}

GObject *
glade_previewer_new (void)
{
  return g_object_new (GLADE_TYPE_PREVIEWER, NULL);
}

void
glade_previewer_present (GladePreviewer *preview)
{
  g_return_if_fail (GLADE_IS_PREVIEWER (preview));
  gtk_window_present (GTK_WINDOW (preview->priv->widget));
}

void
glade_previewer_set_widget (GladePreviewer *preview, GtkWidget *widget)
{
  GladePreviewerPrivate *priv;
  GtkWidget *sw;

  g_return_if_fail (GLADE_IS_PREVIEWER (preview));
  g_return_if_fail (GTK_IS_WIDGET (widget));

  priv = preview->priv;

  if (priv->widget)
    gtk_widget_destroy (priv->widget);

  if (!gtk_widget_is_toplevel (widget))
    {
      GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

      gtk_container_add (GTK_CONTAINER (window), widget);

      priv->widget = window;
    }
  else
    {
      priv->widget = widget;
    }

  /* Create dialog to display messages */
  priv->dialog = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_default_size (GTK_WINDOW (priv->dialog), 640, 320);
  gtk_window_set_title (GTK_WINDOW (priv->dialog), _("Glade Previewer log"));
  gtk_window_set_transient_for (GTK_WINDOW (priv->dialog), GTK_WINDOW (priv->widget));

  priv->textview = gtk_text_view_new ();
  gtk_widget_show (priv->textview);

  sw = gtk_scrolled_window_new (NULL, NULL);
  gtk_widget_show (sw);

  gtk_container_add (GTK_CONTAINER (sw), priv->textview);
  gtk_container_add (GTK_CONTAINER (priv->dialog), sw);

  /* Hide dialog on delete event */
  g_signal_connect (priv->dialog, "delete-event",
                    G_CALLBACK (gtk_widget_hide),
                    NULL);

  /* Quit on delete event */
  g_signal_connect (priv->widget, "delete-event",
                    G_CALLBACK (gtk_main_quit),
                    NULL);

  /* Make sure we get press events */
  gtk_widget_add_events (priv->widget, GDK_KEY_PRESS_MASK);

  /* Handle key presses for screenshot feature */
  g_signal_connect_object (priv->widget, "key-press-event",
                           G_CALLBACK (on_widget_key_press_event),
                           preview, 0);
}

void
glade_previewer_set_message (GladePreviewer *preview, 
                             GtkMessageType  type,
                             const gchar    *message)
{
  GladePreviewerPrivate *priv;
  GtkTextBuffer *buffer;

  g_return_if_fail (GLADE_IS_PREVIEWER (preview));
  priv = preview->priv;

  if (!priv->textview)
    return;

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->textview));
  
  if (message)
    {
      GtkTextIter iter;

      /* TODO: use message type to color text */
      gtk_text_buffer_get_start_iter (buffer, &iter);
      gtk_text_buffer_insert (buffer, &iter, "\n", -1);

      gtk_text_buffer_get_start_iter (buffer, &iter);
      gtk_text_buffer_insert (buffer, &iter, message, -1);

      gtk_window_present (GTK_WINDOW (priv->dialog));
    }
}

static void 
on_css_monitor_changed (GFileMonitor       *monitor,
                        GFile              *file,
                        GFile              *other_file,
                        GFileMonitorEvent   event_type,
                        GladePreviewer     *preview)
{
  GladePreviewerPrivate *priv = preview->priv;
  GError *error = NULL;

  gtk_css_provider_load_from_file (priv->css_provider, file, &error);

  if (error)
    {
      glade_previewer_set_message (preview, GTK_MESSAGE_WARNING, error->message);
      g_error_free (error);
    }
  else
    glade_previewer_set_message (preview, GTK_MESSAGE_OTHER, NULL);
}

void
glade_previewer_set_css_file (GladePreviewer *preview,
                            const gchar  *css_file)
{
  GladePreviewerPrivate *priv;
  GError *error = NULL;
  GFile *file;

  g_return_if_fail (GLADE_IS_PREVIEWER (preview));
  priv = preview->priv;

  g_free (priv->css_file);
  g_clear_object (&priv->css_monitor);

  priv->css_file = g_strdup (css_file);
  
  file = g_file_new_for_path (css_file);
  
  if (!priv->css_provider)
    {
      priv->css_provider = gtk_css_provider_new ();
      g_object_ref_sink (priv->css_provider);

      /* Set provider for default screen once */
      gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
                                                 GTK_STYLE_PROVIDER (priv->css_provider),
                                                 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    }

  priv->css_monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, &error);
  if (error)
    {
      g_warning ("Cant monitor CSS file %s: %s", css_file, error->message);
      g_error_free (error);
    }
  else
    {
      g_object_ref_sink (priv->css_monitor);
      g_signal_connect (priv->css_monitor, "changed",
                        G_CALLBACK (on_css_monitor_changed), preview);
    }

  /* load CSS */
  gtk_css_provider_load_from_file (priv->css_provider, file, &error);
  if (error)
    {
      glade_previewer_set_message (preview, GTK_MESSAGE_INFO, error->message);
      g_message ("%s CSS parsing failed: %s", css_file, error->message);
      g_error_free (error);
    }
  
  g_object_unref (file);
}

void
glade_previewer_set_screenshot_extension (GladePreviewer *preview,
                                          const gchar    *extension)
{
  GladePreviewerPrivate *priv;

  g_return_if_fail (GLADE_IS_PREVIEWER (preview));
  priv = preview->priv;

  g_free (priv->extension);
  priv->extension = g_strdup (extension);
}

static gboolean
quit_when_idle (gpointer loop)
{
  g_main_loop_quit (loop);

  return G_SOURCE_REMOVE;
}

static void
check_for_draw (GdkEvent *event, gpointer loop)
{
  if (event->type == GDK_EXPOSE)
    {
      g_idle_add (quit_when_idle, loop);
      gdk_event_handler_set ((GdkEventFunc) gtk_main_do_event, NULL, NULL);
    }

  gtk_main_do_event (event);
}

/* Taken from Gtk sources gtk-reftest.c  */
static void
glade_previewer_wait_for_drawing (GdkWindow *window)
{
  GMainLoop *loop;

  loop = g_main_loop_new (NULL, FALSE);
  /* We wait until the widget is drawn for the first time.
   * We can not wait for a GtkWidget::draw event, because that might not
   * happen if the window is fully obscured by windowed child widgets.
   * Alternatively, we could wait for an expose event on widget's window.
   * Both of these are rather hairy, not sure what's best. */
  gdk_event_handler_set (check_for_draw, loop, NULL);
  g_main_loop_run (loop);

  /* give the WM/server some time to sync. They need it.
   * Also, do use popups instead of toplevls in your tests
   * whenever you can. */
  gdk_display_sync (gdk_window_get_display (window));
  g_timeout_add (500, quit_when_idle, loop);
  g_main_loop_run (loop);
}

static const gchar *
glade_previewer_get_extension (const gchar *filename)
{
  gchar *extension;
  
  g_return_val_if_fail (filename != NULL, NULL);

  extension = g_strrstr (filename,".");

  if (extension)
    extension++;

  if (!extension)
    {
      g_warning ("%s has no extension!", filename);
      return NULL;
    }
  return extension;
}

static void
glade_previewer_get_scale (GdkScreen *screen, gdouble *sx, gdouble *sy)
{
  if (sx)
    *sx = 72.0 / (gdk_screen_get_width (screen) / (gdk_screen_get_width_mm (screen) * 0.03937008));

  if (sy)
    *sy = 72.0 / (gdk_screen_get_height (screen) / (gdk_screen_get_height_mm (screen) * 0.03937008));
}

static cairo_surface_t *
glade_previewer_surface_from_file (const gchar *filename, gdouble w, gdouble h)
{
  cairo_surface_t *surface;
  const gchar *extension;

  extension = glade_previewer_get_extension (filename);
  
  if (extension == NULL)
    return NULL;

  if (g_strcmp0 (extension, "svg") == 0)
#if CAIRO_HAS_SVG_SURFACE
    surface = cairo_svg_surface_create (filename, w, h);
#else
    g_warning ("PDF not supported by the cairo version used");
#endif
  else if (g_strcmp0 (extension, "ps") == 0)
#if CAIRO_HAS_PS_SURFACE
    surface = cairo_ps_surface_create (filename, w, h);
#else
    g_warning ("PS not supported by the cairo version used");
#endif
  else if (g_strcmp0 (extension, "pdf") == 0)
#if CAIRO_HAS_PDF_SURFACE
    surface = cairo_pdf_surface_create (filename, w, h);
#else
    g_warning ("PDF not supported by the cairo version used");
#endif
  else
    return NULL;

  return surface;
}

/**
 * glade_previewer_screenshot:
 * @preview: A GladePreviewer
 * @wait: True if it should wait for widget to draw.
 * @filename:  a filename to save the image.
 * 
 * Takes a screenshot of the current widget @window is showing and save it to @filename
 * Supported extension are svg, ps, pdf and wahtever gdk-pixbuf supports 
 */
void
glade_previewer_screenshot (GladePreviewer *preview,
                            gboolean        wait,
                            const gchar    *filename)
{
  GladePreviewerPrivate *priv;
  cairo_surface_t *surface;
  GdkWindow *gdkwindow;
  GdkScreen *screen;
  gdouble sx, sy;
  gint w, h;

  g_return_if_fail (GLADE_IS_PREVIEWER (preview));
  g_return_if_fail (filename != NULL);
  priv = preview->priv;

  if (!priv->widget)
    return;

  gdkwindow = gtk_widget_get_window (priv->widget);
  screen = gdk_window_get_screen (gdkwindow);

  if (wait)
    glade_previewer_wait_for_drawing (gdkwindow);

  w = gtk_widget_get_allocated_width (priv->widget);
  h = gtk_widget_get_allocated_height (priv->widget);
  glade_previewer_get_scale (screen, &sx, &sy);
    
  surface = glade_previewer_surface_from_file (filename, w*sx, h*sy);

  if (surface)
    {
      cairo_t *cr = cairo_create (surface);
      cairo_scale (cr, sx, sy);
      gtk_widget_draw (priv->widget, cr);
      cairo_destroy (cr);
      cairo_surface_destroy(surface);
    }
  else
    {
      GdkPixbuf *pix = gdk_pixbuf_get_from_window (gdkwindow, 0, 0, w, h);
      const gchar *ext = glade_previewer_get_extension (filename);
      GError *error = NULL;
      
      if (!gdk_pixbuf_save (pix, filename, ext ? ext : "png", &error, NULL))
        {
          g_warning ("Could not save screenshot to %s because %s", filename, error->message);
          g_error_free (error);
        }

      g_object_unref (pix);
    }
}

static gint
objects_cmp_func (gconstpointer a, gconstpointer b)
{
  const gchar *name_a, *name_b;
  name_a = gtk_buildable_get_name (GTK_BUILDABLE (a));
  name_b = gtk_buildable_get_name (GTK_BUILDABLE (b));
  return g_strcmp0 (name_a, name_b);
}

/**
 * glade_previewer_set_slideshow_widgets:
 * @preview: A GladePreviewer
 * @objects: GSlist of GObject
 * 
 * Add a list of objects to slideshow
 */
void
glade_previewer_set_slideshow_widgets (GladePreviewer *preview,
                                      GSList          *objects)
{
  GladePreviewerPrivate *priv;
  GtkStack *stack;
  GSList *l;

  g_return_if_fail (GLADE_IS_PREVIEWER (preview));
  priv = preview->priv;

  stack = GTK_STACK (gtk_stack_new ());
  gtk_stack_set_transition_type (stack, GTK_STACK_TRANSITION_TYPE_CROSSFADE);

  objects = g_slist_sort (g_slist_copy (objects), objects_cmp_func);

  for (l = objects; l; l = g_slist_next (l))
    {
       GObject *obj = l->data;

       if (!GTK_IS_WIDGET (obj) || gtk_widget_get_parent (GTK_WIDGET (obj)))
         continue;

       /* TODO: make sure we can add a toplevel inside a stack */
       if (GTK_IS_WINDOW (obj))
         continue;

       priv->objects = g_list_prepend (priv->objects, obj);

       gtk_stack_add_named (stack, GTK_WIDGET (obj),
                            gtk_buildable_get_name (GTK_BUILDABLE (obj)));
    }

  priv->objects = g_list_reverse (priv->objects); 

  glade_previewer_set_widget (preview, GTK_WIDGET (stack));
  gtk_widget_show (GTK_WIDGET (stack));
  
  g_slist_free (objects);
}

/**
 * glade_previewer_slideshow_save:
 * @preview: A GladePreviewer
 * @filename:  a filename to save the slideshow.
 * 
 * Takes a screenshot of every widget GtkStack children and save it to @filename
 * each in a different page
 */
void
glade_previewer_slideshow_save (GladePreviewer *preview,
                                const gchar    *filename)
{
  GladePreviewerPrivate *priv;
  cairo_surface_t *surface;
  GdkWindow *gdkwindow;
  GtkWidget *child;
  GtkStack *stack;
  gdouble sx, sy;

  g_return_if_fail (GLADE_IS_PREVIEWER (preview));
  g_return_if_fail (filename != NULL);
  priv = preview->priv;

  g_return_if_fail (GTK_IS_BIN (priv->widget));

  child = gtk_bin_get_child (GTK_BIN (priv->widget));
  g_return_if_fail (GTK_IS_STACK (child));
  stack = GTK_STACK (child);

  gtk_stack_set_transition_type (stack, GTK_STACK_TRANSITION_TYPE_NONE);

  gdkwindow = gtk_widget_get_window (priv->widget);
  glade_previewer_wait_for_drawing (gdkwindow);
  
  glade_previewer_get_scale (gtk_widget_get_screen (GTK_WIDGET (priv->widget)), &sx, &sy); 
  surface = glade_previewer_surface_from_file (filename, 
                                             gtk_widget_get_allocated_width (GTK_WIDGET (stack))*sx,
                                             gtk_widget_get_allocated_height (GTK_WIDGET (stack))*sy);

  if (surface)
    {
      GList *l, *children = gtk_container_get_children (GTK_CONTAINER (stack));
      cairo_t *cr= cairo_create (surface);

      cairo_scale (cr, sx, sy);

      for (l = children; l; l = g_list_next (l))
        {
          GtkWidget *child = l->data;
          gtk_stack_set_visible_child (stack, child);
          glade_previewer_wait_for_drawing (gdkwindow);
          gtk_widget_draw (child, cr);
          cairo_show_page (cr);
        }

      if (children)
        gtk_stack_set_visible_child (stack, children->data);

      g_list_free (children);
      cairo_destroy (cr);
      cairo_surface_destroy(surface);
    }
  else
    g_warning ("Could not save slideshow to %s", filename);
}

/**
 * glade_previewer_set_print_handlers:
 * @preview: A GladePreviewer
 * @print: whether to print handlers or not
 * 
 * Set whether to print handlers when they are activated or not.
 * It only works if you use glade_previewer_connect_function() as the 
 * connect funtion.
 */
void
glade_previewer_set_print_handlers (GladePreviewer *preview,
                                    gboolean        print)
{
  g_return_if_fail (GLADE_IS_PREVIEWER (preview));
  preview->priv->print_handlers = print;
}

typedef struct
{
  gchar        *handler_name;
  GObject      *connect_object;
  GConnectFlags flags;
} HandlerData;

typedef struct
{
  GladePreviewer *window;
  gint          n_invocations;

  GSignalQuery  query;
  GObject      *object;
  GList        *handlers;
} SignalData;

static void
handler_data_free (gpointer udata)
{
  HandlerData *hd = udata;
  g_clear_object (&hd->connect_object);
  g_free (hd->handler_name);
  g_free (hd);
}

static void
signal_data_free (gpointer udata, GClosure *closure)
{
  SignalData *data = udata;

  g_list_free_full (data->handlers, handler_data_free);
  data->handlers = NULL;

  g_clear_object (&data->window);
  g_clear_object (&data->object);

  g_free (data);
}

static inline const gchar *
object_get_name (GObject *object)
{
  if (GTK_IS_BUILDABLE (object))
    return gtk_buildable_get_name (GTK_BUILDABLE (object));
  else
    return g_object_get_data (object, "gtk-builder-name");
}

static void
glade_handler_append (GString      *message,
                      GSignalQuery *query,
                      const gchar  *object,
                      GList        *handlers,
                      gboolean     after)
{
  GList *l;

  for (l = handlers; l; l = g_list_next (l))
    {
      HandlerData *hd = l->data;
      gboolean handler_after = (hd->flags & G_CONNECT_AFTER);
      gboolean swapped = (hd->flags & G_CONNECT_SWAPPED);
      GObject *obj = hd->connect_object;
      gint i;

      if ((after && !handler_after) || (!after && handler_after))
        continue;

      g_string_append_printf (message, "\n\t-> %s%s %s (%s%s%s",
                              g_type_name (query->return_type),
                              g_type_is_a (query->return_type, G_TYPE_OBJECT) ? " *" : "",
                              hd->handler_name,
                              (swapped) ? ((obj) ? G_OBJECT_TYPE_NAME (obj) : "") : g_type_name (query->itype),
                              (swapped) ? ((obj) ? " *" : "") : " *",
                              (swapped) ? ((obj) ? object_get_name (obj) : _("user_data")) : object);

      for (i = 1; i < query->n_params; i++)
        g_string_append_printf (message, ", %s%s", 
                                g_type_name (query->param_types[i]),
                                g_type_is_a (query->param_types[i], G_TYPE_OBJECT) ? " *" : "");

      g_string_append_printf (message, ", %s%s%s); ",
                              (swapped) ? g_type_name (query->itype) : ((obj) ? G_OBJECT_TYPE_NAME (obj) : ""),
                              (swapped) ? " *" : ((obj) ? " *" : ""),
                              (swapped) ? object : ((obj) ? object_get_name (obj) : _("user_data")));

      if (swapped && after)
        /* translators: GConnectFlags values */
        g_string_append (message, _("Swapped | After"));
      else if (swapped)
        /* translators: GConnectFlags value */
        g_string_append (message, _("Swapped"));
      else if (after)
        /* translators: GConnectFlags value */
        g_string_append (message, _("After"));
    }
}

static inline void
glade_handler_method_append (GString *msg, GSignalQuery *q, const gchar *flags)
{
  g_string_append_printf (msg, "\n\t%sClass->%s(); %s", g_type_name (q->itype),
                          q->signal_name, flags);
}

static void
on_handler_called (SignalData *data)
{
  GSignalQuery *query = &data->query;
  GObject *object = data->object;
  const gchar *object_name = object_get_name (object);
  GString *message = g_string_new ("");

  data->n_invocations++;

  if (data->n_invocations == 1)
    /* translators: this will be shown in glade previewer when a signal %s::%s is emited one time */
    g_string_append_printf (message, _("%s::%s emitted one time"),
                            G_OBJECT_TYPE_NAME (object), query->signal_name);
  else
    /* translators: this will be shown in glade previewer when a signal %s::%s is emited %d times */
    g_string_append_printf (message, _("%s::%s emitted %d times"),
                            G_OBJECT_TYPE_NAME (object), query->signal_name,
                            data->n_invocations);

  if (query->signal_flags & G_SIGNAL_RUN_FIRST)
    glade_handler_method_append (message, query, _("Run First"));

  glade_handler_append (message, query, object_name, data->handlers, FALSE);

  if (query->signal_flags & G_SIGNAL_RUN_LAST)
    glade_handler_method_append (message, query, _("Run Last"));

  glade_handler_append (message, query, object_name, data->handlers, TRUE);

  if (query->signal_flags & G_SIGNAL_RUN_CLEANUP)
    glade_handler_method_append (message, query, _("Run Cleanup"));

  glade_previewer_set_message (data->window, GTK_MESSAGE_INFO, message->str);

  if (data->window->priv->print_handlers)
    g_printf ("\n%s\n", message->str);

  g_string_free (message, TRUE);
}

/**
 * glade_previewer_connect_function:
 * @builder:
 * @object:
 * @signal_name:
 * @handler_name:
 * @connect_object:
 * @flags:
 * @window: a #GladePreviewer
 * 
 * Function that collects every signal handler in @builder and shows them
 * in @window info bar when the callback is activated
 */
void
glade_previewer_connect_function (GtkBuilder   *builder,
                                  GObject      *object,
                                  const gchar  *signal_name,
                                  const gchar  *handler_name,
                                  GObject      *connect_object,
                                  GConnectFlags flags,
                                  gpointer      window)
{
  SignalData *data;
  HandlerData *hd;
  guint signal_id;
  gchar *key;

  g_return_if_fail (GLADE_IS_PREVIEWER (window));

  if (!(signal_id = g_signal_lookup (signal_name, G_OBJECT_TYPE (object))))
    return;

  key = g_strconcat ("glade-signal-data-", signal_name, NULL);
  data = g_object_get_data (object, key);

  if (!data)
    {
      data = g_new0 (SignalData, 1);

      data->window = g_object_ref (window);
      g_signal_query (signal_id, &data->query);
      data->object = g_object_ref (object);

      g_signal_connect_data (object, signal_name,
                             G_CALLBACK (on_handler_called),
                             data, signal_data_free, G_CONNECT_SWAPPED);

      g_object_set_data (object, key, data);
    }

  hd = g_new0 (HandlerData, 1);
  hd->handler_name = g_strdup (handler_name);
  hd->connect_object = connect_object ? g_object_ref (connect_object) : NULL;
  hd->flags = flags;

  data->handlers = g_list_append (data->handlers, hd);

  g_free (key);
}