<commit_before>/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
 * mx-table.c: Table layout widget
 *
 * Copyright 2008, 2009 Intel Corporation.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU Lesser General Public License,
 * version 2.1, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope 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 St - Fifth Floor, Boston, MA 02110-1301 USA.
 * Boston, MA 02111-1307, USA.
 *
 * Written by: <NAME> <<EMAIL>>
 *
 */

/**
 * SECTION:mx-table
 * @short_description: A multi-child layout container based on rows
 * and columns
 *
 * #MxTable is a mult-child layout container based on a table arrangement
 * with rows and columns. #MxTable adds several child properties to its
 * children which control their position and size in the table.
 *
 * While other layouts (like #MxGrid) allow you to achieve 
 * table-like effects, #MxTable is the only layout which allows 
 * you to precisely (and easily) place elements at particular grid coordinates,
 * via mx_table_add_actor().
 *
 * <figure id="mx-table">
 *   <title>#MxTable, 3 rows by 3 columns</title>
 *   <para>Notice how rectangles have only been placed in a few of 
 *   the cells inside the table: this would be very difficult to do 
 *   with any other layout, without having layouts within layouts within 
 *   layouts...</para>
 *   <graphic fileref="MxTable.png" format="PNG"/>
 * </figure>
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "mx-table.h"

#include <stdlib.h>
#include <string.h>
#include <glib.h>
#include <clutter/clutter.h>

#include "mx-enum-types.h"
#include "mx-marshal.h"
#include "mx-private.h"
#include "mx-table-child.h"
#include "mx-stylable.h"
#include "mx-focusable.h"

enum
{
  PROP_0,

  PROP_PADDING,
  PROP_COLUMN_SPACING,
  PROP_ROW_SPACING,

  PROP_ROW_COUNT,
  PROP_COL_COUNT,
};

#define MX_TABLE_GET_PRIVATE(obj)    \
  (G_TYPE_INSTANCE_GET_PRIVATE ((obj), MX_TYPE_TABLE, MxTablePrivate))

typedef struct
{
  guint expand : 1;
  guint is_visible : 1;

  gfloat min_size;
  gfloat pref_size;
  gfloat final_size;

} DimensionData;

struct _MxTablePrivate
{
  GList *children;

  guint   ignore_css_col_spacing : 1;
  guint   ignore_css_row_spacing : 1;
  gint    col_spacing;
  gint    row_spacing;

  gint    visible_rows;
  gint    visible_cols;

  gint    n_rows;
  gint    n_cols;

  gint    active_row;
  gint    active_col;

  GArray *columns;
  GArray *rows;

  MxFocusable *last_focus;
};

static void mx_container_iface_init (ClutterContainerIface *iface);
static void mx_focusable_iface_init (MxFocusableIface *iface);
static void mx_stylable_iface_init (MxStylableIface *iface);

G_DEFINE_TYPE_WITH_CODE (MxTable, mx_table, MX_TYPE_WIDGET,
                         G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTAINER,
                                                mx_container_iface_init)
                         G_IMPLEMENT_INTERFACE (MX_TYPE_STYLABLE,
                                                mx_stylable_iface_init)
                         G_IMPLEMENT_INTERFACE (MX_TYPE_FOCUSABLE,
                                                mx_focusable_iface_init));


static ClutterActor*
mx_table_find_actor_at (MxTable *table,
                        int      row,
                        int      column)
{
  MxTablePrivate *priv;
  GList *l;

  priv = table->priv;

  for (l = priv->children; l; l = g_list_next (l))
    {
      MxTableChild *child;
      ClutterActor *actor_child = CLUTTER_ACTOR (l->data);

      child = (MxTableChild *) clutter_container_get_child_meta (CLUTTER_CONTAINER (table),
                                                                 actor_child);

      if ((row >= child->row && row <= child->row + (child->row_span - 1)) &&
          (column >= child->col && column <= child->col + (child->col_span - 1)))
        return actor_child;
    }

  return NULL;
}

static MxFocusable*
mx_table_move_focus (MxFocusable      *focusable,
                     MxFocusDirection  direction,
                     MxFocusable      *from)
{
  MxTablePrivate *priv = MX_TABLE (focusable)->priv;
  MxTable *table = MX_TABLE (focusable);
  GList *l, *childlink;
  MxTableChild *child_meta;
  ClutterActor *child_actor;
  MxFocusable *focused;
  gint row, column;
  ClutterActor *found;

  /* find the current focus */
  childlink = g_list_find (priv->children, from);

  if (!childlink)
    return NULL;

  priv->last_focus = from;

  child_actor = CLUTTER_ACTOR (childlink->data);
  child_meta = (MxTableChild *) clutter_container_get_child_meta (CLUTTER_CONTAINER (focusable),
                                                                  child_actor);

  /* find the next widget to focus */
  switch (direction)
    {
    case MX_FOCUS_DIRECTION_NEXT:

      for (l = childlink->next; l; l = g_list_next (l))
        {
          if (MX_IS_FOCUSABLE (l->data))
            {
              focused = mx_focusable_accept_focus (MX_FOCUSABLE (l->data),
                                                   MX_FOCUS_HINT_FIRST);

              if (focused)
                return focused;
            }
        }

      /* no next widgets to focus */
      return NULL;

    case MX_FOCUS_DIRECTION_PREVIOUS:

      for (l = g_list_previous (childlink); l; l = g_list_previous (l))
        {
          if (MX_IS_FOCUSABLE (l->data))
            {
              focused = mx_focusable_accept_focus (MX_FOCUSABLE (l->data),
                                                   MX_FOCUS_HINT_LAST);

              if (focused)
                return focused;
            }
        }

      /* no widget found in the previous position */
      return NULL;

    case MX_FOCUS_DIRECTION_UP:
      /* move focus up */

      row = child_meta->row - 1;
      column = child_meta->col;

      focused = NULL;

      while (!focused && row >= 0)
        {
          found = mx_table_find_actor_at (table, row, column);

          if (found)
            {
              if (MX_IS_FOCUSABLE (found))
                {
                  focused = mx_focusable_accept_focus (MX_FOCUSABLE (found),
                                                       MX_FOCUS_HINT_FIRST);
                  if (focused)
                    break;
                }

              child_meta = (MxTableChild *) clutter_container_get_child_meta
                (CLUTTER_CONTAINER (focusable), found);

              /* row might not be the top row if @found is a spanned actor */
              row = child_meta->row - 1;
            }
          else
            row --;
        }

      return focused;


    case MX_FOCUS_DIRECTION_DOWN:
      /* move focus down */

      row = child_meta->row + child_meta->row_span;
      column = child_meta->col;

      focused = NULL;

      while (!focused && row < priv->n_rows)
        {
          found = mx_table_find_actor_at (table, row, column);

          if (found)
            {
              if (MX_IS_FOCUSABLE (found))
                {
                  focused = mx_focusable_accept_focus (MX_FOCUSABLE (found),
                                                       MX_FOCUS_HINT_FIRST);
                  if (focused)
                    break;
                }

              child_meta = (MxTableChild *) clutter_container_get_child_meta
                (CLUTTER_CONTAINER (focusable), found);

              row = child_meta->row + child_meta->row_span;
            }
          else
            row ++;
        }

      return focused;


    case MX_FOCUS_DIRECTION_LEFT:
      /* move focus left */

      row = child_meta->row;
      column = child_meta->col - 1;

      focused = NULL;

      while (!focused && column >= 0)
        {
          found = mx_table_find_actor_at (table, row, column);

          if (found)
            {
              if (MX_IS_FOCUSABLE (found))
                {
                  focused = mx_focusable_accept_focus (MX_FOCUSABLE (found),
                                                       MX_FOCUS_HINT_FIRST);
                  if (focused)
                    break;
                }

              child_meta = (MxTableChild *) clutter_container_get_child_meta
                (CLUTTER_CONTAINER (focusable), found);

              /* col might not be the first column if @found is a spanned actor */
              column = child_meta->col - 1;
            }
          else
            column --;
        }

      return focused;


    case MX_FOCUS_DIRECTION_RIGHT:
      /* move focus right */

      row = child_meta->row;
      column = child_meta->col + child_meta->col_span;

      focused = NULL;

      while (!focused && column < priv->n_cols)
        {
          found = mx_table_find_actor_at (table, row, column);

          if (found)
            {
              if (MX_IS_FOCUSABLE (found))
                {
                  focused = mx_focusable_accept_focus (MX_FOCUSABLE (found),
                                                       MX_FOCUS_HINT_FIRST);
                  if (focused)
                    break;
                }

              child_meta = (MxTableChild *) clutter_container_get_child_meta
                (CLUTTER_CONTAINER (focusable), found);

              column = child_meta->col + child_meta->col_span;
            }
          else
            column ++;
        }

      return focused;

    default:
      break;
    }

  return NULL;
}

static MxFocusable*
mx_table_accept_focus (MxFocusable *focusable, MxFocusHint hint)
{
  MxTablePrivate *priv = MX_TABLE (focusable)->priv;
  MxFocusable *return_focusable;
  GList* list, *l;

  return_focusable = NULL;

  /* find the first/last focusable widget */
  switch (hint)
    {
    case MX_FOCUS_HINT_LAST:
      list = g_list_reverse (g_list_copy (priv->children));
      break;

    case MX_FOCUS_HINT_PRIOR:
      if (priv->last_focus)
        {
          list = g_list_copy (g_list_find (priv->children, priv->last_focus));
          if (list)
            break;
        }
      /* This intentionally runs into the next switch case */

    default:
    case MX_FOCUS_HINT_FIRST:
      list = g_list_copy (priv->children);
      break;
    }

  for (l = list; l; l = g_list_next (l))
    {
      if (MX_IS_FOCUSABLE (l->data))
        {
          return_focusable = mx_focusable_accept_focus (MX_FOCUSABLE (l->data),
                                                        hint);
          if (return_focusable)
            break;
        }
    }

  g_list_free (list);

  return return_focusable;
}

static void
mx_focusable_iface_init (MxFocusableIface *iface)
{
  iface->move_focus = mx_table_move_focus;
  iface->accept_focus = mx_table_accept_focus;
}

/*
 * ClutterContainer Implementation
 */
static void
mx_container_add_actor (ClutterContainer *container,
                        ClutterActor     *actor)
{
  MxTablePrivate *priv = MX_TABLE (container)->priv;

  clutter_actor_set_parent (actor, CLUTTER_ACTOR (container));

  priv->children = g_list_append (priv->children, actor);

  /* default position of the actor is 0, 0 */
  _mx_table_update_row_col (MX_TABLE (container), 0, 0);

  clutter_actor_queue_relayout (CLUTTER_ACTOR (container));

  g_signal_emit_by_name (container, "actor-added", actor);
}

static void
mx_container_remove_actor (ClutterContainer *container,
                           ClutterActor     *actor)
{
  MxTablePrivate *priv = MX_TABLE (container)->priv;
  gint rows, cols;

  GList *item = NULL;
  MxTableChild *meta;
  GList *l;

  item = g_list_find (priv->children, actor);

  if (item == NULL)
    {
      g_warning ("Widget of type '%s' is not a child of container of type '%s'",
                 g_type_name (G_OBJECT_TYPE (actor)),
                 g_type_name (G_OBJECT_TYPE (container)));
      return;
    }

  g_object_ref (actor);

  if ((ClutterActor *)priv->last_focus == actor)
    priv->last_focus = NULL;

  priv->children = g_list_delete_link (priv->children, item);
  clutter_actor_unparent (actor);

  /* update row/column count */
  rows = 0;
  cols = 0;
  for (l = priv->children; l; l = l->next)
    {
      ClutterActor *child = CLUTTER_ACTOR (l->data);
      meta = (MxTableChild *) clutter_container_get_child_meta (container,
                                                                  child);
      rows = MAX (rows, meta->row + meta->row_span);
      cols = MAX (cols, meta->col + meta->col_span);
    }
  priv->n_rows = rows;
  priv->n_cols = cols;

  clutter_actor_queue_relayout (CLUTTER_ACTOR (container));

  g_signal_emit_by_name (container, "actor-removed", actor);

  g_object_unref (actor);
}

static void
mx_container_foreach (ClutterContainer *container,
                      ClutterCallback   callback,
                      gpointer          callback_data)
{
  MxTablePrivate *priv = MX_TABLE (container)->priv;

  g_list_foreach (priv->children, (GFunc) callback, callback_data);
}

static void
mx_container_lower (ClutterContainer *container,
                    ClutterActor     *actor,
                    ClutterActor     *sibling)
{
  gint i;
  GList *c, *position, *actor_link = NULL;

  MxTablePrivate *priv = MX_TABLE (container)->priv;

  if (priv->children && (priv->children->data == actor))
    return;

  position = priv->children;
  for (c = priv->children, i = 0; c; c = c->next, i++)
    {
      if (c->data == actor)
        actor_link = c;
      if (c->data == sibling)
        position = c;
    }

  if (!actor_link)
    {
      g_warning (G_STRLOC ": Actor of type '%s' is not a child of container "
                 "of type '%s'",
                 g_type_name (G_OBJECT_TYPE (actor)),
                 g_type_name (G_OBJECT_TYPE (container)));
      return;
    }

  priv->children = g_list_delete_link (priv->children, actor_link);
  priv->children = g_list_insert_before (priv->children, position, actor);

  clutter_actor_queue_redraw (CLUTTER_ACTOR (container));
}

static void
mx_container_raise (ClutterContainer *container,
                    ClutterActor     *actor,
                    ClutterActor     *sibling)
{
  gint i;
  GList *c, *actor_link = NULL;

  gint position = -1;
  MxTablePrivate *priv = MX_TABLE (container)->priv;

  for (c = priv->children, i = 0; c; c = c->next, i++)
    {
      if (c->data == actor)
        actor_link = c;
      if (c->data == sibling)
        position = i;
    }

  if (!actor_link)
    {
      g_warning (G_STRLOC ": Actor of type '%s' is not a child of container "
                 "of type '%s'",
                 g_type_name (G_OBJECT_TYPE (actor)),
                 g_type_name (G_OBJECT_TYPE (container)));
      return;
    }

  if (!actor_link->next)
    return;

  priv->children = g_list_delete_link (priv->children, actor_link);
  priv->children = g_list_insert (priv->children, actor, position);

  clutter_actor_queue_redraw (CLUTTER_ACTOR (container));
}

static gint
mx_table_depth_sort_cb (gconstpointer a,
                        gconstpointer b)
{
  gfloat depth_a = clutter_actor_get_depth ((ClutterActor *)a);
  gfloat depth_b = clutter_actor_get_depth ((ClutterActor *)a);

  if (depth_a < depth_b)
    return -1;
  else if (depth_a > depth_b)
    return 1;
  else
    return 0;
}

static void
mx_container_sort_depth_order (ClutterContainer *container)
{
  MxTablePrivate *priv = MX_TABLE (container)->priv;

  priv->children = g_list_sort (priv->children, mx_table_depth_sort_cb);

  clutter_actor_queue_redraw (CLUTTER_ACTOR (container));
}

static void
mx_container_iface_init (ClutterContainerIface *iface)
{
  iface->add = mx_container_add_actor;
  iface->remove = mx_container_remove_actor;
  iface->foreach = mx_container_foreach;
  iface->lower = mx_container_lower;
  iface->raise = mx_container_raise;
  iface->sort_depth_order = mx_container_sort_depth_order;
  iface->child_meta_type = MX_TYPE_TABLE_CHILD;
}

/* MxStylable implementation */
static void
mx_stylable_iface_init (MxStylableIface *iface)
{
  static gboolean is_initialized = FALSE;

  if (G_UNLIKELY (is_initialized == FALSE))
    {
      GParamSpec *pspec;

      is_initialized = TRUE;

      pspec = g_param_spec_uint ("x-mx-column-spacing",
                                 "Column Spacing",
                                 "The size of the column spacing",
                                 0, G_MAXUINT, 0,
                                 MX_PARAM_READWRITE);
      mx_stylable_iface_install_property (iface, MX_TYPE_TABLE, pspec);

      pspec = g_param_spec_uint ("x-mx-row-spacing",
                                 "Row Spacing",
                                 "The size of the row spacing",
                                 0, G_MAXUINT, 0,
                                 MX_PARAM_READWRITE);
      mx_stylable_iface_install_property (iface, MX_TYPE_TABLE, pspec);
    }
}

/* MxTable Class Implementation */

static void
mx_table_set_property (GObject      *gobject,
                       guint         prop_id,
                       const GValue *value,
                       GParamSpec   *pspec)
{
  MxTable *table = MX_TABLE (gobject);

  switch (prop_id)
    {
    case PROP_COLUMN_SPACING:
      mx_table_set_column_spacing (table, g_value_get_int (value));
      break;

    case PROP_ROW_SPACING:
      mx_table_set_row_spacing (table, g_value_get_int (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

static void
mx_table_get_property (GObject    *gobject,
                       guint       prop_id,
                       GValue     *value,
                       GParamSpec *pspec)
{
  MxTablePrivate *priv = MX_TABLE (gobject)->priv;

  switch (prop_id)
    {
    case PROP_COLUMN_SPACING:
      g_value_set_int (value, priv->col_spacing);
      break;

    case PROP_ROW_SPACING:
      g_value_set_int (value, priv->row_spacing);
      break;

    case PROP_COL_COUNT:
      g_value_set_int (value, priv->n_cols);
      break;

    case PROP_ROW_COUNT:
      g_value_set_int (value, priv->n_rows);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

static void
mx_table_finalize (GObject *gobject)
{
  MxTablePrivate *priv = MX_TABLE (gobject)->priv;

  g_array_free (priv->columns, TRUE);
  g_array_free (priv->rows, TRUE);

  G_OBJECT_CLASS (mx_table_parent_class)->finalize (gobject);
}

static void
mx_table_dispose (GObject *gobject)
{
  MxTablePrivate *priv = MX_TABLE (gobject)->priv;

  /* destroy the children
   * clutter_actor_destroy() will call clutter_container_remove() which will
   * remove the children from the internal list
   */
  while (priv->children)
    clutter_actor_destroy (CLUTTER_ACTOR (priv->children->data));

  G_OBJECT_CLASS (mx_table_parent_class)->dispose (gobject);
}

static void
mx_table_calculate_col_widths (MxTable *table,
                               gint     for_width)
{
  gint i;
  MxTablePrivate *priv = table->priv;
  DimensionData *columns;
  GList *l;
  MxPadding padding;

  g_array_set_size (priv->columns, 0);
  g_array_set_size (priv->columns, priv->n_cols);
  columns = &g_array_index (priv->columns, DimensionData, 0);


  /* take off the padding values to calculate the allocatable width */
  mx_widget_get_padding (MX_WIDGET (table), &padding);

  for_width -= (int)(padding.left + padding.right);

  /* Reset all the visible attributes for the columns */
  priv->visible_cols = 0;
  for (i = 0; i < priv->n_cols; i++)
    columns[i].is_visible = FALSE;

  /* STAGE ONE: calculate column widths for non-spanned children */
  for (l = priv->children; l; l = g_list_next (l))
    {
      MxTableChild *meta;
      ClutterActor *child;
      DimensionData *col;
      gfloat c_min, c_pref;

      child = CLUTTER_ACTOR (l->data);

      if (!CLUTTER_ACTOR_IS_VISIBLE (child))
        continue;

      meta = (MxTableChild *)
        clutter_container_get_child_meta (CLUTTER_CONTAINER (table), child);

      if (meta->col_span > 1)
        continue;

      col = &columns[meta->col];

      /* If this child is visible, then its column is visible */
      if (!col->is_visible)
        {
          col->is_visible = TRUE;
          priv->visible_cols++;
        }

      clutter_actor_get_preferred_width (child, -1, &c_min, &c_pref);

      col->min_size = MAX (col->min_size, c_min);
      col->final_size = col->pref_size = MAX (col->pref_size, c_pref);
      col->expand = MAX (col->expand, meta->x_expand);
    }

  /* STAGE TWO: take spanning children into account */
  for (l = priv->children; l; l = g_list_next (l))
    {
      MxTableChild *meta;
      ClutterActor *child;
      DimensionData *col;
      gfloat c_min, c_pref;
      gfloat min_width, pref_width;
      gint start_col, end_col;
      gint n_expand;

      child = CLUTTER_ACTOR (l->data);

      if (!CLUTTER_ACTOR_IS_VISIBLE (child))
        continue;

      meta = (MxTableChild *)
        clutter_container_get_child_meta (CLUTTER_CONTAINER (table), child);

      if (meta->col_span < 2)
        continue;

      col = &columns[meta->col];
      start_col = meta->col;
      end_col = meta->col + meta->col_span - 1;

      clutter_actor_get_preferred_width (child, -1, &c_min, &c_pref);


      /* check there is enough room for this actor */
      min_width = 0;
      pref_width = 0;
      n_expand = 0;
      for (i = start_col; i <= end_col; i++)
        {
          min_width += columns[i].min_size;
          pref_width += columns[i].pref_size;

          if (columns[i].expand)
            {
              n_expand++;
            }

          /* If this child is visible, then the columns it spans
             are also visible */
          if (!col->is_visible)
            {
              col->is_visible = TRUE;
              priv->visible_cols++;
            }
        }
      min_width += priv->col_spacing * (meta->col_span - 1);
      pref_width += priv->col_spacing * (meta->col_span - 1);


      /* see mx_table_calculate_row_heights() for comments */
      /* (1) */
      if (c_min > min_width)
        {

          /* (2) */
          /* we can start from preferred width and decrease */
          if (pref_width > c_min)
            {<commit_msg>mx-table: Fix column visibility for spanning actors<commit_after>/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
 * mx-table.c: Table layout widget
 *
 * Copyright 2008, 2009 Intel Corporation.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU Lesser General Public License,
 * version 2.1, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope 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 St - Fifth Floor, Boston, MA 02110-1301 USA.
 * Boston, MA 02111-1307, USA.
 *
 * Written by: <NAME> <<EMAIL>>
 *
 */

/**
 * SECTION:mx-table
 * @short_description: A multi-child layout container based on rows
 * and columns
 *
 * #MxTable is a mult-child layout container based on a table arrangement
 * with rows and columns. #MxTable adds several child properties to its
 * children which control their position and size in the table.
 *
 * While other layouts (like #MxGrid) allow you to achieve 
 * table-like effects, #MxTable is the only layout which allows 
 * you to precisely (and easily) place elements at particular grid coordinates,
 * via mx_table_add_actor().
 *
 * <figure id="mx-table">
 *   <title>#MxTable, 3 rows by 3 columns</title>
 *   <para>Notice how rectangles have only been placed in a few of 
 *   the cells inside the table: this would be very difficult to do 
 *   with any other layout, without having layouts within layouts within 
 *   layouts...</para>
 *   <graphic fileref="MxTable.png" format="PNG"/>
 * </figure>
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "mx-table.h"

#include <stdlib.h>
#include <string.h>
#include <glib.h>
#include <clutter/clutter.h>

#include "mx-enum-types.h"
#include "mx-marshal.h"
#include "mx-private.h"
#include "mx-table-child.h"
#include "mx-stylable.h"
#include "mx-focusable.h"

enum
{
  PROP_0,

  PROP_PADDING,
  PROP_COLUMN_SPACING,
  PROP_ROW_SPACING,

  PROP_ROW_COUNT,
  PROP_COL_COUNT,
};

#define MX_TABLE_GET_PRIVATE(obj)    \
  (G_TYPE_INSTANCE_GET_PRIVATE ((obj), MX_TYPE_TABLE, MxTablePrivate))

typedef struct
{
  guint expand : 1;
  guint is_visible : 1;

  gfloat min_size;
  gfloat pref_size;
  gfloat final_size;

} DimensionData;

struct _MxTablePrivate
{
  GList *children;

  guint   ignore_css_col_spacing : 1;
  guint   ignore_css_row_spacing : 1;
  gint    col_spacing;
  gint    row_spacing;

  gint    visible_rows;
  gint    visible_cols;

  gint    n_rows;
  gint    n_cols;

  gint    active_row;
  gint    active_col;

  GArray *columns;
  GArray *rows;

  MxFocusable *last_focus;
};

static void mx_container_iface_init (ClutterContainerIface *iface);
static void mx_focusable_iface_init (MxFocusableIface *iface);
static void mx_stylable_iface_init (MxStylableIface *iface);

G_DEFINE_TYPE_WITH_CODE (MxTable, mx_table, MX_TYPE_WIDGET,
                         G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTAINER,
                                                mx_container_iface_init)
                         G_IMPLEMENT_INTERFACE (MX_TYPE_STYLABLE,
                                                mx_stylable_iface_init)
                         G_IMPLEMENT_INTERFACE (MX_TYPE_FOCUSABLE,
                                                mx_focusable_iface_init));


static ClutterActor*
mx_table_find_actor_at (MxTable *table,
                        int      row,
                        int      column)
{
  MxTablePrivate *priv;
  GList *l;

  priv = table->priv;

  for (l = priv->children; l; l = g_list_next (l))
    {
      MxTableChild *child;
      ClutterActor *actor_child = CLUTTER_ACTOR (l->data);

      child = (MxTableChild *) clutter_container_get_child_meta (CLUTTER_CONTAINER (table),
                                                                 actor_child);

      if ((row >= child->row && row <= child->row + (child->row_span - 1)) &&
          (column >= child->col && column <= child->col + (child->col_span - 1)))
        return actor_child;
    }

  return NULL;
}

static MxFocusable*
mx_table_move_focus (MxFocusable      *focusable,
                     MxFocusDirection  direction,
                     MxFocusable      *from)
{
  MxTablePrivate *priv = MX_TABLE (focusable)->priv;
  MxTable *table = MX_TABLE (focusable);
  GList *l, *childlink;
  MxTableChild *child_meta;
  ClutterActor *child_actor;
  MxFocusable *focused;
  gint row, column;
  ClutterActor *found;

  /* find the current focus */
  childlink = g_list_find (priv->children, from);

  if (!childlink)
    return NULL;

  priv->last_focus = from;

  child_actor = CLUTTER_ACTOR (childlink->data);
  child_meta = (MxTableChild *) clutter_container_get_child_meta (CLUTTER_CONTAINER (focusable),
                                                                  child_actor);

  /* find the next widget to focus */
  switch (direction)
    {
    case MX_FOCUS_DIRECTION_NEXT:

      for (l = childlink->next; l; l = g_list_next (l))
        {
          if (MX_IS_FOCUSABLE (l->data))
            {
              focused = mx_focusable_accept_focus (MX_FOCUSABLE (l->data),
                                                   MX_FOCUS_HINT_FIRST);

              if (focused)
                return focused;
            }
        }

      /* no next widgets to focus */
      return NULL;

    case MX_FOCUS_DIRECTION_PREVIOUS:

      for (l = g_list_previous (childlink); l; l = g_list_previous (l))
        {
          if (MX_IS_FOCUSABLE (l->data))
            {
              focused = mx_focusable_accept_focus (MX_FOCUSABLE (l->data),
                                                   MX_FOCUS_HINT_LAST);

              if (focused)
                return focused;
            }
        }

      /* no widget found in the previous position */
      return NULL;

    case MX_FOCUS_DIRECTION_UP:
      /* move focus up */

      row = child_meta->row - 1;
      column = child_meta->col;

      focused = NULL;

      while (!focused && row >= 0)
        {
          found = mx_table_find_actor_at (table, row, column);

          if (found)
            {
              if (MX_IS_FOCUSABLE (found))
                {
                  focused = mx_focusable_accept_focus (MX_FOCUSABLE (found),
                                                       MX_FOCUS_HINT_FIRST);
                  if (focused)
                    break;
                }

              child_meta = (MxTableChild *) clutter_container_get_child_meta
                (CLUTTER_CONTAINER (focusable), found);

              /* row might not be the top row if @found is a spanned actor */
              row = child_meta->row - 1;
            }
          else
            row --;
        }

      return focused;


    case MX_FOCUS_DIRECTION_DOWN:
      /* move focus down */

      row = child_meta->row + child_meta->row_span;
      column = child_meta->col;

      focused = NULL;

      while (!focused && row < priv->n_rows)
        {
          found = mx_table_find_actor_at (table, row, column);

          if (found)
            {
              if (MX_IS_FOCUSABLE (found))
                {
                  focused = mx_focusable_accept_focus (MX_FOCUSABLE (found),
                                                       MX_FOCUS_HINT_FIRST);
                  if (focused)
                    break;
                }

              child_meta = (MxTableChild *) clutter_container_get_child_meta
                (CLUTTER_CONTAINER (focusable), found);

              row = child_meta->row + child_meta->row_span;
            }
          else
            row ++;
        }

      return focused;


    case MX_FOCUS_DIRECTION_LEFT:
      /* move focus left */

      row = child_meta->row;
      column = child_meta->col - 1;

      focused = NULL;

      while (!focused && column >= 0)
        {
          found = mx_table_find_actor_at (table, row, column);

          if (found)
            {
              if (MX_IS_FOCUSABLE (found))
                {
                  focused = mx_focusable_accept_focus (MX_FOCUSABLE (found),
                                                       MX_FOCUS_HINT_FIRST);
                  if (focused)
                    break;
                }

              child_meta = (MxTableChild *) clutter_container_get_child_meta
                (CLUTTER_CONTAINER (focusable), found);

              /* col might not be the first column if @found is a spanned actor */
              column = child_meta->col - 1;
            }
          else
            column --;
        }

      return focused;


    case MX_FOCUS_DIRECTION_RIGHT:
      /* move focus right */

      row = child_meta->row;
      column = child_meta->col + child_meta->col_span;

      focused = NULL;

      while (!focused && column < priv->n_cols)
        {
          found = mx_table_find_actor_at (table, row, column);

          if (found)
            {
              if (MX_IS_FOCUSABLE (found))
                {
                  focused = mx_focusable_accept_focus (MX_FOCUSABLE (found),
                                                       MX_FOCUS_HINT_FIRST);
                  if (focused)
                    break;
                }

              child_meta = (MxTableChild *) clutter_container_get_child_meta
                (CLUTTER_CONTAINER (focusable), found);

              column = child_meta->col + child_meta->col_span;
            }
          else
            column ++;
        }

      return focused;

    default:
      break;
    }

  return NULL;
}

static MxFocusable*
mx_table_accept_focus (MxFocusable *focusable, MxFocusHint hint)
{
  MxTablePrivate *priv = MX_TABLE (focusable)->priv;
  MxFocusable *return_focusable;
  GList* list, *l;

  return_focusable = NULL;

  /* find the first/last focusable widget */
  switch (hint)
    {
    case MX_FOCUS_HINT_LAST:
      list = g_list_reverse (g_list_copy (priv->children));
      break;

    case MX_FOCUS_HINT_PRIOR:
      if (priv->last_focus)
        {
          list = g_list_copy (g_list_find (priv->children, priv->last_focus));
          if (list)
            break;
        }
      /* This intentionally runs into the next switch case */

    default:
    case MX_FOCUS_HINT_FIRST:
      list = g_list_copy (priv->children);
      break;
    }

  for (l = list; l; l = g_list_next (l))
    {
      if (MX_IS_FOCUSABLE (l->data))
        {
          return_focusable = mx_focusable_accept_focus (MX_FOCUSABLE (l->data),
                                                        hint);
          if (return_focusable)
            break;
        }
    }

  g_list_free (list);

  return return_focusable;
}

static void
mx_focusable_iface_init (MxFocusableIface *iface)
{
  iface->move_focus = mx_table_move_focus;
  iface->accept_focus = mx_table_accept_focus;
}

/*
 * ClutterContainer Implementation
 */
static void
mx_container_add_actor (ClutterContainer *container,
                        ClutterActor     *actor)
{
  MxTablePrivate *priv = MX_TABLE (container)->priv;

  clutter_actor_set_parent (actor, CLUTTER_ACTOR (container));

  priv->children = g_list_append (priv->children, actor);

  /* default position of the actor is 0, 0 */
  _mx_table_update_row_col (MX_TABLE (container), 0, 0);

  clutter_actor_queue_relayout (CLUTTER_ACTOR (container));

  g_signal_emit_by_name (container, "actor-added", actor);
}

static void
mx_container_remove_actor (ClutterContainer *container,
                           ClutterActor     *actor)
{
  MxTablePrivate *priv = MX_TABLE (container)->priv;
  gint rows, cols;

  GList *item = NULL;
  MxTableChild *meta;
  GList *l;

  item = g_list_find (priv->children, actor);

  if (item == NULL)
    {
      g_warning ("Widget of type '%s' is not a child of container of type '%s'",
                 g_type_name (G_OBJECT_TYPE (actor)),
                 g_type_name (G_OBJECT_TYPE (container)));
      return;
    }

  g_object_ref (actor);

  if ((ClutterActor *)priv->last_focus == actor)
    priv->last_focus = NULL;

  priv->children = g_list_delete_link (priv->children, item);
  clutter_actor_unparent (actor);

  /* update row/column count */
  rows = 0;
  cols = 0;
  for (l = priv->children; l; l = l->next)
    {
      ClutterActor *child = CLUTTER_ACTOR (l->data);
      meta = (MxTableChild *) clutter_container_get_child_meta (container,
                                                                  child);
      rows = MAX (rows, meta->row + meta->row_span);
      cols = MAX (cols, meta->col + meta->col_span);
    }
  priv->n_rows = rows;
  priv->n_cols = cols;

  clutter_actor_queue_relayout (CLUTTER_ACTOR (container));

  g_signal_emit_by_name (container, "actor-removed", actor);

  g_object_unref (actor);
}

static void
mx_container_foreach (ClutterContainer *container,
                      ClutterCallback   callback,
                      gpointer          callback_data)
{
  MxTablePrivate *priv = MX_TABLE (container)->priv;

  g_list_foreach (priv->children, (GFunc) callback, callback_data);
}

static void
mx_container_lower (ClutterContainer *container,
                    ClutterActor     *actor,
                    ClutterActor     *sibling)
{
  gint i;
  GList *c, *position, *actor_link = NULL;

  MxTablePrivate *priv = MX_TABLE (container)->priv;

  if (priv->children && (priv->children->data == actor))
    return;

  position = priv->children;
  for (c = priv->children, i = 0; c; c = c->next, i++)
    {
      if (c->data == actor)
        actor_link = c;
      if (c->data == sibling)
        position = c;
    }

  if (!actor_link)
    {
      g_warning (G_STRLOC ": Actor of type '%s' is not a child of container "
                 "of type '%s'",
                 g_type_name (G_OBJECT_TYPE (actor)),
                 g_type_name (G_OBJECT_TYPE (container)));
      return;
    }

  priv->children = g_list_delete_link (priv->children, actor_link);
  priv->children = g_list_insert_before (priv->children, position, actor);

  clutter_actor_queue_redraw (CLUTTER_ACTOR (container));
}

static void
mx_container_raise (ClutterContainer *container,
                    ClutterActor     *actor,
                    ClutterActor     *sibling)
{
  gint i;
  GList *c, *actor_link = NULL;

  gint position = -1;
  MxTablePrivate *priv = MX_TABLE (container)->priv;

  for (c = priv->children, i = 0; c; c = c->next, i++)
    {
      if (c->data == actor)
        actor_link = c;
      if (c->data == sibling)
        position = i;
    }

  if (!actor_link)
    {
      g_warning (G_STRLOC ": Actor of type '%s' is not a child of container "
                 "of type '%s'",
                 g_type_name (G_OBJECT_TYPE (actor)),
                 g_type_name (G_OBJECT_TYPE (container)));
      return;
    }

  if (!actor_link->next)
    return;

  priv->children = g_list_delete_link (priv->children, actor_link);
  priv->children = g_list_insert (priv->children, actor, position);

  clutter_actor_queue_redraw (CLUTTER_ACTOR (container));
}

static gint
mx_table_depth_sort_cb (gconstpointer a,
                        gconstpointer b)
{
  gfloat depth_a = clutter_actor_get_depth ((ClutterActor *)a);
  gfloat depth_b = clutter_actor_get_depth ((ClutterActor *)a);

  if (depth_a < depth_b)
    return -1;
  else if (depth_a > depth_b)
    return 1;
  else
    return 0;
}

static void
mx_container_sort_depth_order (ClutterContainer *container)
{
  MxTablePrivate *priv = MX_TABLE (container)->priv;

  priv->children = g_list_sort (priv->children, mx_table_depth_sort_cb);

  clutter_actor_queue_redraw (CLUTTER_ACTOR (container));
}

static void
mx_container_iface_init (ClutterContainerIface *iface)
{
  iface->add = mx_container_add_actor;
  iface->remove = mx_container_remove_actor;
  iface->foreach = mx_container_foreach;
  iface->lower = mx_container_lower;
  iface->raise = mx_container_raise;
  iface->sort_depth_order = mx_container_sort_depth_order;
  iface->child_meta_type = MX_TYPE_TABLE_CHILD;
}

/* MxStylable implementation */
static void
mx_stylable_iface_init (MxStylableIface *iface)
{
  static gboolean is_initialized = FALSE;

  if (G_UNLIKELY (is_initialized == FALSE))
    {
      GParamSpec *pspec;

      is_initialized = TRUE;

      pspec = g_param_spec_uint ("x-mx-column-spacing",
                                 "Column Spacing",
                                 "The size of the column spacing",
                                 0, G_MAXUINT, 0,
                                 MX_PARAM_READWRITE);
      mx_stylable_iface_install_property (iface, MX_TYPE_TABLE, pspec);

      pspec = g_param_spec_uint ("x-mx-row-spacing",
                                 "Row Spacing",
                                 "The size of the row spacing",
                                 0, G_MAXUINT, 0,
                                 MX_PARAM_READWRITE);
      mx_stylable_iface_install_property (iface, MX_TYPE_TABLE, pspec);
    }
}

/* MxTable Class Implementation */

static void
mx_table_set_property (GObject      *gobject,
                       guint         prop_id,
                       const GValue *value,
                       GParamSpec   *pspec)
{
  MxTable *table = MX_TABLE (gobject);

  switch (prop_id)
    {
    case PROP_COLUMN_SPACING:
      mx_table_set_column_spacing (table, g_value_get_int (value));
      break;

    case PROP_ROW_SPACING:
      mx_table_set_row_spacing (table, g_value_get_int (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

static void
mx_table_get_property (GObject    *gobject,
                       guint       prop_id,
                       GValue     *value,
                       GParamSpec *pspec)
{
  MxTablePrivate *priv = MX_TABLE (gobject)->priv;

  switch (prop_id)
    {
    case PROP_COLUMN_SPACING:
      g_value_set_int (value, priv->col_spacing);
      break;

    case PROP_ROW_SPACING:
      g_value_set_int (value, priv->row_spacing);
      break;

    case PROP_COL_COUNT:
      g_value_set_int (value, priv->n_cols);
      break;

    case PROP_ROW_COUNT:
      g_value_set_int (value, priv->n_rows);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

static void
mx_table_finalize (GObject *gobject)
{
  MxTablePrivate *priv = MX_TABLE (gobject)->priv;

  g_array_free (priv->columns, TRUE);
  g_array_free (priv->rows, TRUE);

  G_OBJECT_CLASS (mx_table_parent_class)->finalize (gobject);
}

static void
mx_table_dispose (GObject *gobject)
{
  MxTablePrivate *priv = MX_TABLE (gobject)->priv;

  /* destroy the children
   * clutter_actor_destroy() will call clutter_container_remove() which will
   * remove the children from the internal list
   */
  while (priv->children)
    clutter_actor_destroy (CLUTTER_ACTOR (priv->children->data));

  G_OBJECT_CLASS (mx_table_parent_class)->dispose (gobject);
}

static void
mx_table_calculate_col_widths (MxTable *table,
                               gint     for_width)
{
  gint i;
  MxTablePrivate *priv = table->priv;
  DimensionData *columns;
  GList *l;
  MxPadding padding;

  g_array_set_size (priv->columns, 0);
  g_array_set_size (priv->columns, priv->n_cols);
  columns = &g_array_index (priv->columns, DimensionData, 0);


  /* take off the padding values to calculate the allocatable width */
  mx_widget_get_padding (MX_WIDGET (table), &padding);

  for_width -= (int)(padding.left + padding.right);

  /* Reset all the visible attributes for the columns */
  priv->visible_cols = 0;
  for (i = 0; i < priv->n_cols; i++)
    columns[i].is_visible = FALSE;

  /* STAGE ONE: calculate column widths for non-spanned children */
  for (l = priv->children; l; l = g_list_next (l))
    {
      MxTableChild *meta;
      ClutterActor *child;
      DimensionData *col;
      gfloat c_min, c_pref;

      child = CLUTTER_ACTOR (l->data);

      if (!CLUTTER_ACTOR_IS_VISIBLE (child))
        continue;

      meta = (MxTableChild *)
        clutter_container_get_child_meta (CLUTTER_CONTAINER (table), child);

      if (meta->col_span > 1)
        continue;

      col = &columns[meta->col];

      /* If this child is visible, then its column is visible */
      if (!col->is_visible)
        {
          col->is_visible = TRUE;
          priv->visible_cols++;
        }

      clutter_actor_get_preferred_width (child, -1, &c_min, &c_pref);

      col->min_size = MAX (col->min_size, c_min);
      col->final_size = col->pref_size = MAX (col->pref_size, c_pref);
      col->expand = MAX (col->expand, meta->x_expand);
    }

  /* STAGE TWO: take spanning children into account */
  for (l = priv->children; l; l = g_list_next (l))
    {
      MxTableChild *meta;
      ClutterActor *child;
      gfloat c_min, c_pref;
      gfloat min_width, pref_width;
      gint start_col, end_col;
      gint n_expand;

      child = CLUTTER_ACTOR (l->data);

      if (!CLUTTER_ACTOR_IS_VISIBLE (child))
        continue;

      meta = (MxTableChild *)
        clutter_container_get_child_meta (CLUTTER_CONTAINER (table), child);

      if (meta->col_span < 2)
        continue;

      start_col = meta->col;
      end_col = meta->col + meta->col_span - 1;

      clutter_actor_get_preferred_width (child, -1, &c_min, &c_pref);


      /* check there is enough room for this actor */
      min_width = 0;
      pref_width = 0;
      n_expand = 0;
      for (i = start_col; i <= end_col; i++)
        {
          min_width += columns[i].min_size;
          pref_width += columns[i].pref_size;

          if (columns[i].expand)
            {
              n_expand++;
            }

          /* If this child is visible, then the columns it spans
             are also visible */
          if (!columns[i].is_visible)
            {
              columns[i].is_visible = TRUE;
              priv->visible_cols++;
            }
        }
      min_width += priv->col_spacing * (meta->col_span - 1);
      pref_width += priv->col_spacing * (meta->col_span - 1);


      /* see mx_table_calculate_row_heights() for comments */
      /* (1) */
      if (c_min > min_width)
        {

          /* (2) */
          /* we can start from preferred width and decrease */
          if (pref_width > c_min)
            {
{'fraction_non_alphanumeric': 0.09822081403850841, 'fraction_numerical': 0.003412137460394833, 'mean_word_length': 2.0909464456923708, 'pattern_counts': {'":': 4, '<': 47, '<?xml version=': 0, '>': 342, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 72, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7435002', 'n_tokens_mistral': 14734, 'n_tokens_neox': 14118, 'n_words': 3422}