Blob Blame History Raw
/*
 * glade-intro.c
 *
 * Copyright (C) 2017 Juan Pablo Ugarte
 *
 * 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.
 *
 * Authors:
 *   Juan Pablo Ugarte <juanpablougarte@gmail.com>
 */

#include "glade-intro.h"

typedef struct
{
  GtkWidget          *widget;
  const gchar        *name;
  const gchar        *widget_name;
  const gchar        *text;
  GladeIntroPosition  position;
  gint                delay;
} ScriptNode;

typedef struct
{
  GtkWidget  *toplevel;

  GList      *script;      /* List of (ScriptNode *) */
  GHashTable *widgets;     /* Table with all named widget in toplevel */

  GtkPopover *popover;     /* Popover to show the script text */

  guint       timeout_id;  /* Timeout id for running the script */
  GList      *current;     /* Current script node */

  gboolean    hiding_node;
} GladeIntroPrivate;

struct _GladeIntro
{
  GObject parent_instance;
};

enum
{
  PROP_0,
  PROP_TOPLEVEL,
  PROP_STATE,

  N_PROPERTIES
};

enum
{
  SHOW_NODE,
  HIDE_NODE,

  LAST_SIGNAL
};

static guint intro_signals[LAST_SIGNAL] = { 0 };

static GParamSpec *properties[N_PROPERTIES];

G_DEFINE_TYPE_WITH_PRIVATE (GladeIntro, glade_intro, G_TYPE_OBJECT);

#define GET_PRIVATE(d) ((GladeIntroPrivate *) glade_intro_get_instance_private((GladeIntro*)d))

static void
glade_intro_init (GladeIntro *intro)
{
}

static void
glade_intro_finalize (GObject *object)
{
  GladeIntroPrivate *priv = GET_PRIVATE (object);

  if (priv->timeout_id)
    {
      g_source_remove (priv->timeout_id);
      priv->timeout_id = 0;
    }

  gtk_popover_set_relative_to (priv->popover, NULL);
  g_clear_object (&priv->popover);

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

static void
glade_intro_set_property (GObject      *object,
                          guint         prop_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  g_return_if_fail (GLADE_IS_INTRO (object));

  switch (prop_id)
    {
      case PROP_TOPLEVEL:
        glade_intro_set_toplevel (GLADE_INTRO (object), g_value_get_object (value));
      break;
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
glade_intro_get_property (GObject    *object,
                          guint       prop_id,
                          GValue     *value,
                          GParamSpec *pspec)
{
  GladeIntroPrivate *priv;

  g_return_if_fail (GLADE_IS_INTRO (object));
  priv = GET_PRIVATE (object);

  switch (prop_id)
    {
      case PROP_TOPLEVEL:
        g_value_set_object (value, priv->toplevel);
      break;
      case PROP_STATE:
        if (priv->timeout_id)
          g_value_set_enum (value, GLADE_INTRO_STATE_PLAYING);
        else if (priv->current)
          g_value_set_enum (value, GLADE_INTRO_STATE_PAUSED);
        else
          g_value_set_enum (value, GLADE_INTRO_STATE_NULL);
      break;
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static GType
glade_intro_state_get_type (void)
{
    static GType etype = 0;
    if (G_UNLIKELY(etype == 0)) {
        static const GEnumValue values[] = {
            { GLADE_INTRO_STATE_NULL, "GLADE_INTRO_STATE_NULL", "null" },
            { GLADE_INTRO_STATE_PLAYING, "GLADE_INTRO_STATE_PLAYING", "playing" },
            { GLADE_INTRO_STATE_PAUSED, "GLADE_INTRO_STATE_PAUSED", "paused" },
            { 0, NULL, NULL }
        };
        etype = g_enum_register_static (g_intern_static_string ("GladeIntroStatus"), values);
    }
    return etype;
}

static void
glade_intro_class_init (GladeIntroClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = glade_intro_finalize;
  object_class->set_property = glade_intro_set_property;
  object_class->get_property = glade_intro_get_property;

  /* Properties */
  properties[PROP_TOPLEVEL] =
    g_param_spec_object ("toplevel", "Toplevel",
                         "The main toplevel from where to get the widgets",
                         GTK_TYPE_WINDOW,
                         G_PARAM_READWRITE);
  properties[PROP_STATE] =
    g_param_spec_enum ("state", "State",
                       "Playback state",
                       glade_intro_state_get_type (),
                       GLADE_INTRO_STATE_NULL,
                       G_PARAM_READABLE);

  intro_signals[SHOW_NODE] =
    g_signal_new ("show-node", G_OBJECT_CLASS_TYPE (klass), 0, 0,
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 2,
                  G_TYPE_STRING,
                  GTK_TYPE_WIDGET);
  intro_signals[HIDE_NODE] =
    g_signal_new ("hide-node", G_OBJECT_CLASS_TYPE (klass), 0, 0,
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 2,
                  G_TYPE_STRING,
                  GTK_TYPE_WIDGET);

  g_object_class_install_properties (object_class, N_PROPERTIES, properties);
}

/* Public API */

GladeIntro *
glade_intro_new (GtkWindow *toplevel)
{
  return (GladeIntro*) g_object_new (GLADE_TYPE_INTRO, "toplevel", toplevel, NULL);
}

static void
get_toplevel_widgets (GtkWidget *widget, gpointer data)
{
  const gchar *name;

  if ((name = gtk_widget_get_name (widget)) &&
      g_strcmp0 (name, G_OBJECT_TYPE_NAME (widget)))
    g_hash_table_insert (GET_PRIVATE (data)->widgets, (gpointer)name, widget);

  if (GTK_IS_CONTAINER (widget))
    gtk_container_forall (GTK_CONTAINER (widget), get_toplevel_widgets, data);
}

void
glade_intro_set_toplevel (GladeIntro *intro, GtkWindow *toplevel)
{
  GladeIntroPrivate *priv;

  g_return_if_fail (GLADE_IS_INTRO (intro));
  priv = GET_PRIVATE (intro);

  g_clear_object (&priv->toplevel);
  g_clear_pointer (&priv->widgets, g_hash_table_unref);

  if (toplevel)
    {
      priv->toplevel = g_object_ref (toplevel);
      priv->widgets = g_hash_table_new (g_str_hash, g_str_equal);
      gtk_container_forall (GTK_CONTAINER (toplevel), get_toplevel_widgets, intro);
    }
}

void
glade_intro_script_add (GladeIntro         *intro,
                        const gchar        *name,
                        const gchar        *widget,
                        const gchar        *text,
                        GladeIntroPosition  position,
                        gdouble             delay)
{
  GladeIntroPrivate *priv;
  ScriptNode *node;

  g_return_if_fail (GLADE_IS_INTRO (intro));
  priv = GET_PRIVATE (intro);

  node = g_new0 (ScriptNode, 1);
  node->name        = name;
  node->widget_name = widget;
  node->text        = text;
  node->position    = position;
  node->delay       = delay * 1000;

  priv->script = g_list_append (priv->script, node);
}

static gboolean script_play (gpointer data);

static void
on_popover_closed (GtkPopover *popover, GladeIntro *intro)
{
  glade_intro_pause (intro);
}

static void
hide_current_node (GladeIntro *intro)
{
  GladeIntroPrivate *priv = GET_PRIVATE (intro);
  ScriptNode *node;

  if (priv->hiding_node)
    return;
  priv->hiding_node = TRUE;
  if (priv->popover)
    {
      g_signal_handlers_disconnect_by_func (priv->popover, on_popover_closed, intro);
      gtk_popover_popdown (priv->popover);
      g_clear_object (&priv->popover);
    }

  if (priv->current && (node = priv->current->data))
    {
      if (node->widget)
        gtk_style_context_remove_class (gtk_widget_get_style_context (node->widget),
                                        "glade-intro-highlight");
      g_signal_emit (intro, intro_signals[HIDE_NODE], 0, node->name, node->widget);
    }

  /* Set next node */
  priv->current = (priv->current) ? g_list_next (priv->current) : NULL;

  priv->hiding_node = FALSE;
}

static gboolean
script_transition (gpointer data)
{
  GladeIntroPrivate *priv = GET_PRIVATE (data);

  priv->timeout_id = g_timeout_add (250, script_play, data);
  hide_current_node (data);

  return G_SOURCE_REMOVE;
}

static GtkWidget *
glade_intro_popover_new (GladeIntro *intro, const gchar *text)
{
  GtkWidget *popover, *box, *image, *label;

  popover = gtk_popover_new (NULL);
  label = gtk_label_new (text);
  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
  image = gtk_image_new_from_icon_name ("dialog-information-symbolic", GTK_ICON_SIZE_DIALOG);

  gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
  gtk_label_set_max_width_chars (GTK_LABEL (label), 28);

  gtk_box_pack_start (GTK_BOX (box), image, FALSE, FALSE, 0);
  gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0);
  gtk_container_add (GTK_CONTAINER (popover), box);

  gtk_style_context_add_class (gtk_widget_get_style_context (popover), "glade-intro");
  g_signal_connect (popover, "closed", G_CALLBACK (on_popover_closed), intro);

  gtk_widget_show_all (box);

  return popover;
}

static gboolean
script_play (gpointer data)
{
  GladeIntroPrivate *priv = GET_PRIVATE (data);
  GtkStyleContext *context;
  ScriptNode *node;

  priv->timeout_id = 0;

  if (!priv->current || !(node = priv->current->data))
    return G_SOURCE_REMOVE;

  node->widget = NULL;

  if (node->widget_name &&
      (node->widget = g_hash_table_lookup (priv->widgets, node->widget_name)) &&
      node->text)
    {
      /* Ensure the widget is visible */
      if (!gtk_widget_is_visible (node->widget))
        {
          GtkWidget *parent;
          /* if the widget is inside a popover pop it up */
          if ((parent = gtk_widget_get_ancestor (node->widget, GTK_TYPE_POPOVER)))
            gtk_popover_popup (GTK_POPOVER (parent));
        }

      context = gtk_widget_get_style_context (node->widget);
      gtk_style_context_add_class (context, "glade-intro-highlight");

      /* Create popover */
      priv->popover = g_object_ref_sink (glade_intro_popover_new (data, node->text));
      gtk_popover_set_relative_to (priv->popover, node->widget);

      if (node->position == GLADE_INTRO_BOTTOM)
        gtk_popover_set_position (priv->popover, GTK_POS_BOTTOM);
      else if (node->position == GLADE_INTRO_LEFT)
        gtk_popover_set_position (priv->popover, GTK_POS_LEFT);
      else if (node->position == GLADE_INTRO_RIGHT)
        gtk_popover_set_position (priv->popover, GTK_POS_RIGHT);
      else if (node->position == GLADE_INTRO_CENTER)
        {
          GdkRectangle rect = {
            gtk_widget_get_allocated_width (node->widget)/2,
            gtk_widget_get_allocated_height (node->widget)/2,
            4, 4
          };

          gtk_popover_set_pointing_to (priv->popover, &rect);
          gtk_popover_set_position (priv->popover, GTK_POS_TOP);
        }
    }

  g_signal_emit (data, intro_signals[SHOW_NODE], 0, node->name, node->widget);

  if (priv->popover)
    gtk_popover_popup (priv->popover);

  priv->timeout_id = g_timeout_add (node->delay, script_transition, data);

  return G_SOURCE_REMOVE;
}

void
glade_intro_play (GladeIntro *intro)
{
  GladeIntroPrivate *priv;

  g_return_if_fail (GLADE_IS_INTRO (intro));
  priv = GET_PRIVATE (intro);

  if (priv->script == NULL)
    return;

  if (priv->current == NULL)
    priv->current = priv->script;

  script_play (intro);

  g_object_notify_by_pspec (G_OBJECT (intro), properties[PROP_STATE]);
}

void
glade_intro_pause (GladeIntro *intro)
{
  GladeIntroPrivate *priv;

  g_return_if_fail (GLADE_IS_INTRO (intro));
  priv = GET_PRIVATE (intro);

  if (priv->timeout_id)
    g_source_remove (priv->timeout_id);

  priv->timeout_id = 0;
  hide_current_node (intro);

  g_object_notify_by_pspec (G_OBJECT (intro), properties[PROP_STATE]);
}

void
glade_intro_stop (GladeIntro *intro)
{
  GladeIntroPrivate *priv;

  g_return_if_fail (GLADE_IS_INTRO (intro));
  priv = GET_PRIVATE (intro);

  glade_intro_pause (intro);
  priv->current = NULL;

  g_object_notify_by_pspec (G_OBJECT (intro), properties[PROP_STATE]);
}