Blob Blame History Raw
/* dzl-centering-bin.c
 *
 * Copyright (C) 2015 Christian Hergert <chergert@redhat.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#define G_LOG_DOMAIN "dzl-centering-bin"

#include "config.h"

#include "widgets/dzl-centering-bin.h"
#include "bindings/dzl-signal-group.h"

/**
 * SECTION:dzlcenteringbin:
 * @title: DzlCenteringBin
 * @short_description: center a widget with respect to the toplevel
 *
 * First off, you probably want to use GtkBox with a center widget instead
 * of this widget. However, the case where this widget is useful is when
 * you cannot control your layout within the width of the toplevel, but
 * still want your child centered within the toplevel.
 *
 * This is done by translating coordinates of the widget with respect to
 * the toplevel and anchoring the child at TRUE_CENTER-(alloc.width/2).
 */

typedef struct
{
  DzlSignalGroup *signals;
  gint            max_width_request;
} DzlCenteringBinPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (DzlCenteringBin, dzl_centering_bin, GTK_TYPE_BIN)

enum {
  PROP_0,
  PROP_MAX_WIDTH_REQUEST,
  LAST_PROP
};

static GParamSpec *properties [LAST_PROP];

GtkWidget *
dzl_centering_bin_new (void)
{
  return g_object_new (DZL_TYPE_CENTERING_BIN, NULL);
}

static void
dzl_centering_bin_size_allocate (GtkWidget     *widget,
                                 GtkAllocation *allocation)
{
  DzlCenteringBin *self = (DzlCenteringBin *)widget;
  DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self);
  GtkWidget *child;

  g_assert (GTK_IS_WIDGET (widget));
  g_assert (allocation != NULL);

  gtk_widget_set_allocation (widget, allocation);

  child = gtk_bin_get_child (GTK_BIN (widget));

  if ((child != NULL) && gtk_widget_get_visible (child))
    {
      GtkWidget *toplevel = gtk_widget_get_toplevel (child);
      GtkAllocation top_allocation;
      GtkAllocation child_allocation;
      GtkRequisition nat_child_req;
      gint translated_x;
      gint translated_y;
      gint border_width;

      border_width = gtk_container_get_border_width (GTK_CONTAINER (self));

      gtk_widget_get_allocation (toplevel, &top_allocation);
      gtk_widget_translate_coordinates (toplevel, widget,
                                        top_allocation.x + (top_allocation.width / 2),
                                        0,
                                        &translated_x,
                                        &translated_y);

      gtk_widget_get_preferred_size (child, NULL, &nat_child_req);

      child_allocation.x = allocation->x;
      child_allocation.y = allocation->y;
      child_allocation.height = allocation->height;
      child_allocation.width = translated_x * 2;

      child_allocation.y += border_width;
      child_allocation.height -= border_width * 2;

      if (nat_child_req.width > child_allocation.width)
        child_allocation.width = MIN (nat_child_req.width, allocation->width);

      if ((priv->max_width_request > 0) && (child_allocation.width > priv->max_width_request))
        {
          child_allocation.x += (child_allocation.width - priv->max_width_request) / 2;
          child_allocation.width = priv->max_width_request;
        }

      gtk_widget_size_allocate (child, &child_allocation);
    }
}

static gboolean
queue_allocate_in_idle (gpointer data)
{
  g_autoptr(DzlCenteringBin) self = data;

  gtk_widget_queue_allocate (GTK_WIDGET (self));

  return G_SOURCE_REMOVE;
}

static void
dzl_centering_bin_toplevel_size_allocate (DzlCenteringBin *self,
                                          GtkAllocation   *allocation,
                                          GtkWindow       *toplevel)
{
  g_assert (DZL_IS_CENTERING_BIN (self));
  g_assert (GTK_IS_WINDOW (toplevel));

  /*
   * If we ::queue_allocate() immediately, we can get into a state where an
   * allocation is needed when ::draw() should be called. That causes a
   * warning on the command line, so instead just delay until we leave
   * our current main loop iteration.
   */
  g_timeout_add (0, queue_allocate_in_idle, g_object_ref (self));
}

static void
dzl_centering_bin_hierarchy_changed (GtkWidget *widget,
                                     GtkWidget *previous_toplevel)
{
  DzlCenteringBin *self = (DzlCenteringBin *)widget;
  DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self);
  GtkWidget *toplevel;

  g_assert (DZL_IS_CENTERING_BIN (self));

  /*
   * The hierarcy has changed, so we need to ensure we get allocation change
   * from the toplevel so we can relayout our child to be centered.
   */

  toplevel = gtk_widget_get_toplevel (widget);

  if (GTK_IS_WINDOW (toplevel))
    dzl_signal_group_set_target (priv->signals, toplevel);
  else
    dzl_signal_group_set_target (priv->signals, NULL);
}

static void
dzl_centering_bin_get_preferred_width (GtkWidget *widget,
                                       gint      *min_width,
                                       gint      *nat_width)
{
  DzlCenteringBin *self = (DzlCenteringBin *)widget;
  DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self);

  g_assert (DZL_IS_CENTERING_BIN (self));

  GTK_WIDGET_CLASS (dzl_centering_bin_parent_class)->get_preferred_width (widget, min_width, nat_width);

  if ((priv->max_width_request > 0) && (*min_width > priv->max_width_request))
    *min_width = priv->max_width_request;

  if ((priv->max_width_request > 0) && (*nat_width > priv->max_width_request))
    *nat_width = priv->max_width_request;
}

static void
dzl_centering_bin_get_preferred_height_for_width (GtkWidget *widget,
                                                  gint       width,
                                                  gint      *min_height,
                                                  gint      *nat_height)
{
  DzlCenteringBin *self = (DzlCenteringBin *)widget;
  DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self);
  GtkWidget *child;
  gint border_width;

  g_assert (DZL_IS_CENTERING_BIN (self));

  /*
   * TODO: Something is still not right here. I'm seeing a situation where
   *       we are not getting the proper height for width. See the extensions
   *       page in preferences. We are not getting the right height from
   *       our box child, due to GtkLabel line wrapping.
   */

  *min_height = 0;
  *nat_height = 0;

  child = gtk_bin_get_child (GTK_BIN (self));
  if (child == NULL)
    return;

  if ((priv->max_width_request) > 0 && (width > priv->max_width_request))
    width = priv->max_width_request;

  border_width = gtk_container_get_border_width (GTK_CONTAINER (widget));
  width -= border_width * 2;

  gtk_widget_get_preferred_height_for_width (child, width, min_height, nat_height);

  *min_height += border_width * 2;
  *nat_height += border_width * 2;
}

static GtkSizeRequestMode
dzl_centering_bin_get_request_mode (GtkWidget *widget)
{
  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}

static void
dzl_centering_bin_finalize (GObject *object)
{
  DzlCenteringBin *self = (DzlCenteringBin *)object;
  DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self);

  g_clear_object (&priv->signals);

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

static void
dzl_centering_bin_get_property (GObject    *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
  DzlCenteringBin *self = DZL_CENTERING_BIN(object);
  DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self);

  switch (prop_id)
    {
    case PROP_MAX_WIDTH_REQUEST:
      g_value_set_int (value, priv->max_width_request);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void
dzl_centering_bin_set_property (GObject      *object,
                                guint         prop_id,
                                const GValue *value,
                                GParamSpec   *pspec)
{
  DzlCenteringBin *self = DZL_CENTERING_BIN(object);
  DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self);

  switch (prop_id)
    {
    case PROP_MAX_WIDTH_REQUEST:
      priv->max_width_request = g_value_get_int (value);
      gtk_widget_queue_resize (GTK_WIDGET (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void
dzl_centering_bin_class_init (DzlCenteringBinClass *klass)
{
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = dzl_centering_bin_finalize;
  object_class->get_property = dzl_centering_bin_get_property;
  object_class->set_property = dzl_centering_bin_set_property;

  widget_class->get_preferred_height_for_width = dzl_centering_bin_get_preferred_height_for_width;
  widget_class->get_preferred_width = dzl_centering_bin_get_preferred_width;
  widget_class->get_request_mode = dzl_centering_bin_get_request_mode;
  widget_class->hierarchy_changed = dzl_centering_bin_hierarchy_changed;
  widget_class->size_allocate = dzl_centering_bin_size_allocate;

  properties [PROP_MAX_WIDTH_REQUEST] =
    g_param_spec_int ("max-width-request",
                      "Max Width Request",
                      "Max Width Request",
                      -1,
                      G_MAXINT,
                      -1,
                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, LAST_PROP, properties);
}

static void
dzl_centering_bin_init (DzlCenteringBin *self)
{
  DzlCenteringBinPrivate *priv = dzl_centering_bin_get_instance_private (self);

  priv->signals = dzl_signal_group_new (GTK_TYPE_WINDOW);
  priv->max_width_request = -1;

  dzl_signal_group_connect_object (priv->signals,
                                   "size-allocate",
                                   G_CALLBACK (dzl_centering_bin_toplevel_size_allocate),
                                   self,
                                   G_CONNECT_SWAPPED);
}