Blob Blame History Raw
/*
 * glade-gtk-table.c - GladeWidgetAdaptor for GtkTable widget
 *
 * Copyright (C) 2008 Tristan Van Berkom
 *
 * Author(s):
 *      Tristan Van Berkom <tvb@gnome.org>
 *
 * 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 <gtk/gtk.h>
#include <glib/gi18n-lib.h>
#include <string.h>

#include "glade-fixed.h"

typedef struct
{
  /* comparable part: */
  GladeWidget *widget;
  gint left_attach;
  gint right_attach;
  gint top_attach;
  gint bottom_attach;
} GladeGtkTableChild;

typedef enum
{
  DIR_UP,
  DIR_DOWN,
  DIR_LEFT,
  DIR_RIGHT
} GladeTableDir;

typedef enum
{
  GROUP_ACTION_INSERT_ROW,
  GROUP_ACTION_INSERT_COLUMN,
  GROUP_ACTION_REMOVE_COLUMN,
  GROUP_ACTION_REMOVE_ROW
} GroupAction;

/* Redefine GTK_TABLE() macro, as GtkTable is deprecated */
#undef GTK_TABLE
#define GTK_TABLE(obj) ((GtkTable *)obj)

static void
glade_gtk_table_get_child_attachments (GtkWidget * table,
                                       GtkWidget * child,
                                       GtkTableChild * tchild)
{
  guint left, right, top, bottom;

  gtk_container_child_get (GTK_CONTAINER (table), child,
                           "left-attach", (guint *) & left,
                           "right-attach", (guint *) & right,
                           "bottom-attach", (guint *) & bottom,
                           "top-attach", (guint *) & top, NULL);

  tchild->widget = child;
  tchild->left_attach = left;
  tchild->right_attach = right;
  tchild->top_attach = top;
  tchild->bottom_attach = bottom;
}

static gboolean
glade_gtk_table_widget_exceeds_bounds (GtkTable * table, gint n_rows,
                                       gint n_cols)
{
  GList *list, *children;
  gboolean ret = FALSE;

  children = gtk_container_get_children (GTK_CONTAINER (table));

  for (list = children; list && list->data; list = list->next)
    {
      GtkTableChild child;

      glade_gtk_table_get_child_attachments (GTK_WIDGET (table),
                                             GTK_WIDGET (list->data), &child);

      if (GLADE_IS_PLACEHOLDER (child.widget) == FALSE &&
          (child.right_attach > n_cols || child.bottom_attach > n_rows))
        {
          ret = TRUE;
          break;
        }
    }

  g_list_free (children);

  return ret;
}

#define TABLE_OCCUPIED(occmap, n_columns, col, row) \
    (occmap)[row * n_columns + col]

static void
glade_gtk_table_build_occupation_maps(GtkTable *table, guint n_columns, guint n_rows,
				      gchar **child_map, gpointer **placeholder_map)
{
    guint i, j;
    GList *list, *children = gtk_container_get_children (GTK_CONTAINER (table));

    *child_map = g_malloc0(n_columns * n_rows * sizeof(gchar));  /* gchar is smaller than gboolean */
    *placeholder_map = g_malloc0(n_columns * n_rows * sizeof(gpointer));

    for (list = children; list && list->data; list = list->next)
    {
	GtkTableChild child;

	glade_gtk_table_get_child_attachments (GTK_WIDGET (table),
					       GTK_WIDGET (list->data), &child);

	if (GLADE_IS_PLACEHOLDER(list->data))
	{
	    /* assumption: placeholders are always attached to exactly 1 cell */
	    TABLE_OCCUPIED(*placeholder_map, n_columns, child.left_attach, child.top_attach) = list->data;
	}
	else
	{
	    for (i = child.left_attach; i < child.right_attach && i < n_columns; i++)
	    {
		for (j = child.top_attach; j < child.bottom_attach && j < n_rows; j++)
		{
		    TABLE_OCCUPIED(*child_map, n_columns, i, j) = 1;
		}
	    }
	}
    }
    g_list_free (children);
}

static void
glade_gtk_table_refresh_placeholders (GtkTable * table)
{
  guint n_columns, n_rows, i, j;
  gchar *child_map;
  gpointer *placeholder_map;

  g_object_get (table, "n-columns", &n_columns, "n-rows", &n_rows, NULL);
  glade_gtk_table_build_occupation_maps (table, n_columns, n_rows,
					 &child_map, &placeholder_map);

  for (i = 0; i < n_columns; i++)
    {
      for (j = 0; j < n_rows; j++)
	{
	  gpointer placeholder = TABLE_OCCUPIED(placeholder_map, n_columns, i, j);

	  if (TABLE_OCCUPIED(child_map, n_columns, i, j))
	    {
	      if (placeholder)
		{
		  gtk_container_remove (GTK_CONTAINER (table), 
					GTK_WIDGET (placeholder));
		}
	    }
	  else
	    {
	      if (!placeholder)
		{
		  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
		  gtk_table_attach_defaults (table, 
					     glade_placeholder_new (), 
					     i, i + 1, j, j + 1);
		  G_GNUC_END_IGNORE_DEPRECATIONS;
		}
	    }
	}
    }
  g_free(child_map);
  g_free(placeholder_map);

  if (gtk_widget_get_realized (GTK_WIDGET (table)))
    gtk_container_check_resize (GTK_CONTAINER (table));
}

static void
gtk_table_children_callback (GtkWidget * widget, gpointer client_data)
{
  GList **children;

  children = (GList **) client_data;
  *children = g_list_prepend (*children, widget);
}

GList *
glade_gtk_table_get_children (GladeWidgetAdaptor * adaptor,
                              GtkContainer * container)
{
  GList *children = NULL;

  gtk_container_forall (container, gtk_table_children_callback, &children);

  /* GtkTable has the children list already reversed */
  return children;
}

void
glade_gtk_table_add_child (GladeWidgetAdaptor * adaptor,
                           GObject * object, GObject * child)
{
  gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child));

  glade_gtk_table_refresh_placeholders (GTK_TABLE (object));
}

void
glade_gtk_table_remove_child (GladeWidgetAdaptor * adaptor,
                              GObject * object, GObject * child)
{
  gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child));

  glade_gtk_table_refresh_placeholders (GTK_TABLE (object));
}

void
glade_gtk_table_replace_child (GladeWidgetAdaptor * adaptor,
                               GtkWidget * container,
                               GtkWidget * current, GtkWidget * new_widget)
{
  /* Chain Up */
  GWA_GET_CLASS
      (GTK_TYPE_CONTAINER)->replace_child (adaptor,
                                           G_OBJECT (container),
                                           G_OBJECT (current),
                                           G_OBJECT (new_widget));

  /* If we are replacing a GladeWidget, we must refresh placeholders
   * because the widget may have spanned multiple rows/columns, we must
   * not do so in the case we are pasting multiple widgets into a table,
   * where destroying placeholders results in default packing properties
   * (since the remaining placeholder templates no longer exist, only the
   * first pasted widget would have proper packing properties).
   */
  if (!GLADE_IS_PLACEHOLDER (new_widget))
    glade_gtk_table_refresh_placeholders (GTK_TABLE (container));

}

static void
glade_gtk_table_set_n_common (GObject * object, const GValue * value,
                              gboolean for_rows)
{
  GladeWidget *widget;
  GtkTable *table;
  guint new_size, old_size, n_columns, n_rows;

  table = GTK_TABLE (object);

  g_object_get (table, "n-columns", &n_columns, "n-rows", &n_rows, NULL);

  new_size = g_value_get_uint (value);
  old_size = for_rows ? n_rows : n_columns;

  if (new_size < 1)
    return;

  if (glade_gtk_table_widget_exceeds_bounds
      (table, for_rows ? new_size : n_rows, for_rows ? n_columns : new_size))
    /* Refuse to shrink if it means orphaning widgets */
    return;

  widget = glade_widget_get_from_gobject (GTK_WIDGET (table));
  g_return_if_fail (widget != NULL);

  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
  if (for_rows)
    gtk_table_resize (table, new_size, n_columns);
  else
    gtk_table_resize (table, n_rows, new_size);
  G_GNUC_END_IGNORE_DEPRECATIONS;

  /* Fill table with placeholders */
  glade_gtk_table_refresh_placeholders (table);

  if (new_size < old_size)
    {
      /* Remove from the bottom up */
      GList *list, *children;
      GList *list_to_free = NULL;

      children = gtk_container_get_children (GTK_CONTAINER (table));

      for (list = children; list && list->data; list = list->next)
        {
          GtkTableChild child;
          guint start, end;

          glade_gtk_table_get_child_attachments (GTK_WIDGET (table),
                                                 GTK_WIDGET (list->data),
                                                 &child);

          start = for_rows ? child.top_attach : child.left_attach;
          end = for_rows ? child.bottom_attach : child.right_attach;

          /* We need to completely remove it */
          if (start >= new_size)
            {
              list_to_free = g_list_prepend (list_to_free, child.widget);
              continue;
            }

          /* If the widget spans beyond the new border,
           * we should resize it to fit on the new table */
          if (end > new_size)
            gtk_container_child_set
                (GTK_CONTAINER (table), GTK_WIDGET (child.widget),
                 for_rows ? "bottom_attach" : "right_attach", new_size, NULL);
        }

      g_list_free (children);

      if (list_to_free)
        {
          for (list = g_list_first (list_to_free);
               list && list->data; list = list->next)
            {
              g_object_ref (G_OBJECT (list->data));
              gtk_container_remove (GTK_CONTAINER (table),
                                    GTK_WIDGET (list->data));
              /* This placeholder is no longer valid, force destroy */
              gtk_widget_destroy (GTK_WIDGET (list->data));
            }
          g_list_free (list_to_free);
        }

      G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
      gtk_table_resize (table,
                        for_rows ? new_size : n_rows,
                        for_rows ? n_columns : new_size);
      G_GNUC_END_IGNORE_DEPRECATIONS;
    }
}

void
glade_gtk_table_set_property (GladeWidgetAdaptor * adaptor,
                              GObject * object,
                              const gchar * id, const GValue * value)
{
  if (!strcmp (id, "n-rows"))
    glade_gtk_table_set_n_common (object, value, TRUE);
  else if (!strcmp (id, "n-columns"))
    glade_gtk_table_set_n_common (object, value, FALSE);
  else
    GWA_GET_CLASS (GTK_TYPE_CONTAINER)->set_property (adaptor, object,
                                                      id, value);
}

static gboolean
glade_gtk_table_verify_n_common (GObject * object, const GValue * value,
                                 gboolean for_rows)
{
  GtkTable *table = GTK_TABLE (object);
  guint n_columns, n_rows, new_size = g_value_get_uint (value);

  g_object_get (table, "n-columns", &n_columns, "n-rows", &n_rows, NULL);

  if (glade_gtk_table_widget_exceeds_bounds
      (table, for_rows ? new_size : n_rows, for_rows ? n_columns : new_size))
    /* Refuse to shrink if it means orphaning widgets */
    return FALSE;

  return TRUE;
}

gboolean
glade_gtk_table_verify_property (GladeWidgetAdaptor * adaptor,
                                 GObject * object,
                                 const gchar * id, const GValue * value)
{
  if (!strcmp (id, "n-rows"))
    return glade_gtk_table_verify_n_common (object, value, TRUE);
  else if (!strcmp (id, "n-columns"))
    return glade_gtk_table_verify_n_common (object, value, FALSE);
  else if (GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property)
    GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property (adaptor, object,
                                                         id, value);

  return TRUE;
}

void
glade_gtk_table_set_child_property (GladeWidgetAdaptor * adaptor,
                                    GObject * container,
                                    GObject * child,
                                    const gchar * property_name, GValue * value)
{
  GWA_GET_CLASS
      (GTK_TYPE_CONTAINER)->child_set_property (adaptor,
                                                container, child,
                                                property_name, value);

  if (strcmp (property_name, "bottom-attach") == 0 ||
      strcmp (property_name, "left-attach") == 0 ||
      strcmp (property_name, "right-attach") == 0 ||
      strcmp (property_name, "top-attach") == 0)
    {
      /* Refresh placeholders */
      glade_gtk_table_refresh_placeholders (GTK_TABLE (container));
    }

}

static gboolean
glade_gtk_table_verify_attach_common (GObject * object,
                                      GValue * value,
                                      guint * val,
                                      const gchar * prop,
                                      guint * prop_val,
                                      const gchar * parent_prop,
                                      guint * parent_val)
{
  GladeWidget *widget, *parent;

  widget = glade_widget_get_from_gobject (object);
  g_return_val_if_fail (GLADE_IS_WIDGET (widget), TRUE);
  parent = glade_widget_get_parent (widget);
  g_return_val_if_fail (GLADE_IS_WIDGET (parent), TRUE);

  *val = g_value_get_uint (value);
  glade_widget_property_get (widget, prop, prop_val);
  glade_widget_property_get (parent, parent_prop, parent_val);

  return FALSE;
}

static gboolean
glade_gtk_table_verify_left_top_attach (GObject * object,
                                        GValue * value,
                                        const gchar * prop,
                                        const gchar * parent_prop)
{
  guint val, prop_val, parent_val;

  if (glade_gtk_table_verify_attach_common (object, value, &val,
                                            prop, &prop_val,
                                            parent_prop, &parent_val))
    return FALSE;

  if (val >= parent_val || val >= prop_val)
    return FALSE;

  return TRUE;
}

static gboolean
glade_gtk_table_verify_right_bottom_attach (GObject * object,
                                            GValue * value,
                                            const gchar * prop,
                                            const gchar * parent_prop)
{
  guint val, prop_val, parent_val;

  if (glade_gtk_table_verify_attach_common (object, value, &val,
                                            prop, &prop_val,
                                            parent_prop, &parent_val))
    return FALSE;

  if (val <= prop_val || val > parent_val)
    return FALSE;

  return TRUE;
}

gboolean
glade_gtk_table_child_verify_property (GladeWidgetAdaptor * adaptor,
                                       GObject * container,
                                       GObject * child,
                                       const gchar * id, GValue * value)
{
  if (!strcmp (id, "left-attach"))
    return glade_gtk_table_verify_left_top_attach (child,
                                                   value,
                                                   "right-attach", "n-columns");
  else if (!strcmp (id, "right-attach"))
    return glade_gtk_table_verify_right_bottom_attach (child,
                                                       value,
                                                       "left-attach",
                                                       "n-columns");
  else if (!strcmp (id, "top-attach"))
    return glade_gtk_table_verify_left_top_attach (child,
                                                   value,
                                                   "bottom-attach", "n-rows");
  else if (!strcmp (id, "bottom-attach"))
    return glade_gtk_table_verify_right_bottom_attach (child,
                                                       value,
                                                       "top-attach", "n-rows");
  else if (GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_verify_property)
    GWA_GET_CLASS
        (GTK_TYPE_CONTAINER)->child_verify_property (adaptor,
                                                     container, child,
                                                     id, value);

  return TRUE;
}

static void
glade_gtk_table_child_insert_remove_action (GladeWidgetAdaptor *adaptor, 
					    GObject            *container, 
					    GObject            *object, 
					    GroupAction         group_action,
					    const gchar        *n_row_col, 
					    const gchar        *attach1,    /* should be smaller (top/left) attachment */
                                            const gchar        *attach2,      /* should be larger (bot/right) attachment */
                                            gboolean            remove, 
					    gboolean            after)
{
  GladeWidget *parent;
  GList *children, *l;
  gint child_pos, size, offset;

  gtk_container_child_get (GTK_CONTAINER (container),
                           GTK_WIDGET (object),
                           after ? attach2 : attach1, &child_pos, NULL);

  parent = glade_widget_get_from_gobject (container);
  switch (group_action)
    {
      case GROUP_ACTION_INSERT_ROW:
        glade_command_push_group (_("Insert Row on %s"), glade_widget_get_name (parent));
        break;
      case GROUP_ACTION_INSERT_COLUMN:
        glade_command_push_group (_("Insert Column on %s"), glade_widget_get_name (parent));
        break;
      case GROUP_ACTION_REMOVE_COLUMN:
        glade_command_push_group (_("Remove Column on %s"), glade_widget_get_name (parent));
        break;
      case GROUP_ACTION_REMOVE_ROW:
        glade_command_push_group (_("Remove Row on %s"), glade_widget_get_name (parent));
        break;
      default:
        g_assert_not_reached ();
    }

  children = glade_widget_adaptor_get_children (adaptor, container);
  /* Make sure widgets does not get destroyed */
  g_list_foreach (children, (GFunc) g_object_ref, NULL);

  glade_widget_property_get (parent, n_row_col, &size);

  if (remove)
    {
      GList *del = NULL;
      /* Remove children first */
      for (l = children; l; l = g_list_next (l))
        {
          GladeWidget *gchild = glade_widget_get_from_gobject (l->data);
          gint pos1, pos2;

          /* Skip placeholders */
          if (gchild == NULL)
            continue;

          glade_widget_pack_property_get (gchild, attach1, &pos1);
          glade_widget_pack_property_get (gchild, attach2, &pos2);
          if ((pos1 + 1 == pos2) && ((after ? pos2 : pos1) == child_pos))
            {
              del = g_list_prepend (del, gchild);
            }
        }
      if (del)
        {
          glade_command_delete (del);
          g_list_free (del);
        }
      offset = -1;
    }
  else
    {
      /* Expand the table */
      glade_command_set_property (glade_widget_get_property (parent, n_row_col),
                                  size + 1);
      offset = 1;
    }

  /* Reorder children */
  for (l = children; l; l = g_list_next (l))
    {
      GladeWidget *gchild = glade_widget_get_from_gobject (l->data);
      gint pos;

      /* Skip placeholders */
      if (gchild == NULL)
        continue;

      /* if removing, do top/left before bot/right */
      if (remove)
        {
          /* adjust top-left attachment */
          glade_widget_pack_property_get (gchild, attach1, &pos);
          if (pos > child_pos || (after && pos == child_pos))
            {
              glade_command_set_property (glade_widget_get_pack_property
                                          (gchild, attach1), pos + offset);
            }

          /* adjust bottom-right attachment */
          glade_widget_pack_property_get (gchild, attach2, &pos);
          if (pos > child_pos || (after && pos == child_pos))
            {
              glade_command_set_property (glade_widget_get_pack_property
                                          (gchild, attach2), pos + offset);
            }

        }
      /* if inserting, do bot/right before top/left */
      else
        {
          /* adjust bottom-right attachment */
          glade_widget_pack_property_get (gchild, attach2, &pos);
          if (pos > child_pos)
            {
              glade_command_set_property (glade_widget_get_pack_property
                                          (gchild, attach2), pos + offset);
            }

          /* adjust top-left attachment */
          glade_widget_pack_property_get (gchild, attach1, &pos);
          if (pos >= child_pos)
            {
              glade_command_set_property (glade_widget_get_pack_property
                                          (gchild, attach1), pos + offset);
            }
        }
    }

  if (remove)
    {
      /* Shrink the table */
      glade_command_set_property (glade_widget_get_property (parent, n_row_col),
                                  size - 1);
    }

  g_list_foreach (children, (GFunc) g_object_unref, NULL);
  g_list_free (children);

  glade_command_pop_group ();
}

void
glade_gtk_table_child_action_activate (GladeWidgetAdaptor * adaptor,
                                       GObject * container,
                                       GObject * object,
                                       const gchar * action_path)
{
  if (strcmp (action_path, "insert_row/after") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_INSERT_ROW,
                                                  "n-rows", "top-attach",
                                                  "bottom-attach", FALSE, TRUE);
    }
  else if (strcmp (action_path, "insert_row/before") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_INSERT_ROW,
                                                  "n-rows", "top-attach",
                                                  "bottom-attach",
                                                  FALSE, FALSE);
    }
  else if (strcmp (action_path, "insert_column/after") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_INSERT_COLUMN,
                                                  "n-columns", "left-attach",
                                                  "right-attach", FALSE, TRUE);
    }
  else if (strcmp (action_path, "insert_column/before") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_INSERT_COLUMN,
                                                  "n-columns", "left-attach",
                                                  "right-attach", FALSE, FALSE);
    }
  else if (strcmp (action_path, "remove_column") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_REMOVE_COLUMN,
                                                  "n-columns", "left-attach",
                                                  "right-attach", TRUE, FALSE);
    }
  else if (strcmp (action_path, "remove_row") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_REMOVE_ROW,
                                                  "n-rows", "top-attach",
                                                  "bottom-attach", TRUE, FALSE);
    }
  else
    GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_action_activate (adaptor,
                                                               container,
                                                               object,
                                                               action_path);
}