Commit dc40fc59 authored by Juan Pablo Ugarte's avatar Juan Pablo Ugarte

Add UI interactive introduction

parent 97be4a28
......@@ -8,6 +8,7 @@
<property name="spacing">4</property>
<child>
<object class="GtkMenuButton" id="all_button">
<property name="name">adaptor-search-button</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
......@@ -53,6 +54,7 @@
</child>
<child>
<object class="GtkMenuButton" id="others_button">
<property name="name">adaptor-others-button</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
......@@ -74,11 +76,13 @@
</child>
<child>
<object class="GtkButtonBox" id="gtk_button_box">
<property name="name">adaptor-gtk-buttonbox</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkMenuButton" id="extra_button">
<property name="name">adaptor-extra-button</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Extra gtk objects</property>
......
......@@ -24,6 +24,7 @@ BUILT_SOURCES = glade-resources.c glade-resources.h
glade_SOURCES = \
glade-window.c \
glade-intro.c \
glade-resources.c \
glade-preferences.c \
glade-http.c \
......@@ -32,6 +33,7 @@ glade_SOURCES = \
noinst_HEADERS = \
glade-window.h \
glade-intro.h \
glade-resources.h \
glade-preferences.h \
glade-logo.h \
......
/*
* 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]);
}
/*
* glade-intro.h
*
* 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>
*/
#ifndef _GLADE_INTRO_H_
#define _GLADE_INTRO_H_
#include <gtk/gtk.h>
G_BEGIN_DECLS
#define GLADE_TYPE_INTRO (glade_intro_get_type ())
G_DECLARE_FINAL_TYPE (GladeIntro, glade_intro, GLADE, INTRO, GObject)
typedef enum
{
GLADE_INTRO_STATE_NULL = 0,
GLADE_INTRO_STATE_PLAYING,
GLADE_INTRO_STATE_PAUSED
} GladeIntroState;
typedef enum
{
GLADE_INTRO_NONE = 0,
GLADE_INTRO_TOP,
GLADE_INTRO_BOTTOM,
GLADE_INTRO_RIGHT,
GLADE_INTRO_LEFT,
GLADE_INTRO_CENTER
} GladeIntroPosition;
GladeIntro *glade_intro_new (GtkWindow *toplevel);
void glade_intro_set_toplevel (GladeIntro *intro,
GtkWindow *toplevel);
void glade_intro_script_add (GladeIntro *intro,
const gchar *name,
const gchar *widget,
const gchar *text,
GladeIntroPosition position,
gdouble delay);
void glade_intro_play (GladeIntro *intro);
void glade_intro_pause (GladeIntro *intro);
void glade_intro_stop (GladeIntro *intro);
G_END_DECLS
#endif /* _GLADE_INTRO_H_ */
......@@ -30,6 +30,7 @@
#include "glade-resources.h"
#include "glade-preferences.h"
#include "glade-registration.h"
#include "glade-intro.h"
#include <gladeui/glade.h>
#include <gladeui/glade-popup.h>
......@@ -118,6 +119,9 @@ struct _GladeWindowPrivate
GtkWidget *registration; /* Registration and user survey dialog */
GladeIntro *intro;
GType new_type;
GdkRectangle position;
};
......@@ -2096,6 +2100,13 @@ on_pointer_margin_edit_action_activate (GSimpleAction *action, GVariant *p, gpoi
glade_project_set_pointer_mode (get_active_project (data), GLADE_POINTER_MARGIN_EDIT);
}
static void
on_intro_action_activate (GSimpleAction *action, GVariant *p, gpointer data)
{
GladeWindow *window = data;
glade_intro_play (window->priv->intro);
}
static void
glade_window_init (GladeWindow *window)
{
......@@ -2155,6 +2166,170 @@ glade_window_switch_handler (GladeWindow *window, gint index)
switch_foreach, GINT_TO_POINTER (index));
}
static gboolean
intro_continue (gpointer intro)
{
glade_intro_play (intro);
return G_SOURCE_REMOVE;
}
static void
on_intro_project_add_widget (GladeProject *project,
GladeWidget *widget,
GladeWindow *window)
{
GladeWidgetAdaptor *adaptor = glade_widget_get_adaptor (widget);
if (glade_widget_adaptor_get_object_type (adaptor) == window->priv->new_type)
{
g_idle_add (intro_continue, window->priv->intro);
if (window->priv->new_type == GTK_TYPE_BUTTON)
g_signal_handlers_disconnect_by_func (project, on_intro_project_add_widget, window);
}
}
static void
on_user_new_action_activate (GSimpleAction *simple,
GVariant *parameter,
GladeWindow *window)
{
g_signal_connect (get_active_project (window), "add-widget",
G_CALLBACK (on_intro_project_add_widget),
window);
glade_intro_play (window->priv->intro);
g_signal_handlers_disconnect_by_func (simple, on_user_new_action_activate, window);
}
static void
on_intro_show_node (GladeIntro *intro,
const gchar *node,
GtkWidget *widget,
GladeWindow *window)
{
GladeWindowPrivate *priv = window->priv;
if (!g_strcmp0 (node, "new-project"))
{
/* Create two new project to make the project switcher visible */
g_action_group_activate_action (window->priv->actions, "new", NULL);
g_action_group_activate_action (window->priv->actions, "new", NULL);
}
else if (!g_strcmp0 (node, "add-project"))
{
GAction *new_action = g_action_map_lookup_action (G_ACTION_MAP (priv->actions), "new");
g_signal_connect (new_action, "activate",
G_CALLBACK (on_user_new_action_activate),
window);
}
else if (!g_strcmp0 (node, "add-window"))
{
window->priv->new_type = GTK_TYPE_WINDOW;
}
else if (!g_strcmp0 (node, "add-grid"))
{
window->priv->new_type = GTK_TYPE_GRID;
}
else if (!g_strcmp0 (node, "add-button"))
{
window->priv->new_type = GTK_TYPE_BUTTON;
}
else if (!g_strcmp0 (node, "search") ||
!g_strcmp0 (node, "others"))
{
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), TRUE);
}
else if (!g_strcmp0 (node, "gtk"))
{
GList *children;
if ((children = gtk_container_get_children (GTK_CONTAINER (widget))))
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (children->data), TRUE);
g_list_free (children);
}
}
static void
on_intro_hide_node (GladeIntro *intro,
const gchar *node,
GtkWidget *widget,
GladeWindow *window)
{
if (!g_strcmp0 (node, "search") ||
!g_strcmp0 (node, "others"))
{
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), FALSE);
}
else if (!g_strcmp0 (node, "gtk"))
{
GList *children;
if ((children = gtk_container_get_children (GTK_CONTAINER (widget))))
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (children->data), FALSE);
g_list_free (children);
}
else if (!g_strcmp0 (node, "add-project") ||
!g_strcmp0 (node, "add-window") ||
!g_strcmp0 (node, "add-grid") ||
!g_strcmp0 (node, "add-button"))
glade_intro_pause (window->priv->intro);
}
#define ADD_NODE(n,w,P,d,t) glade_intro_script_add (window->priv->intro, n, w, t, GLADE_INTRO_##P, d)
static void
glade_window_populate_intro (GladeWindow *window)
{
ADD_NODE (NULL, "intro-button", BOTTOM, 5, _("Hello, I will show you what's new in Glade"));
ADD_NODE (NULL, "headerbar", BOTTOM, 6, _("The menubar and toolbar where merged in the headerbar"));
ADD_NODE (NULL, "open-button", BOTTOM, 3, _("You can open a project"));
ADD_NODE (NULL, "recent-button", BOTTOM, 2, _("find recently used"));
ADD_NODE (NULL, "new-button", BOTTOM, 2, _("or create a new one"));
ADD_NODE ("new-project", NULL, NONE, .75, NULL);
ADD_NODE (NULL, "undo-button", BOTTOM, 2, _("Undo"));
ADD_NODE (NULL, "redo-button", BOTTOM, 2, _("Redo"));
ADD_NODE (NULL, "project-switcher", BOTTOM, 3, _("Project switcher"));
ADD_NODE (NULL, "save-button", BOTTOM, 4, _("and Save button are directly accesible in the headerbar"));
ADD_NODE (NULL, "save-as-button", BOTTOM, 2, _("just like Save As"));
ADD_NODE (NULL, "properties-button", BOTTOM, 2, _("project properties"));
ADD_NODE (NULL, "menu-button", BOTTOM, 3, _("and less commonly used actions"));
ADD_NODE (NULL, "inspector", CENTER, 3, _("The object inspector took the palette place"));
ADD_NODE (NULL, "editor", CENTER, 3, _("To free up space for the property editor"));
ADD_NODE (NULL, "adaptor-chooser", BOTTOM, 4, _("The palette was replaced with a new object chooser"));
ADD_NODE ("search", "adaptor-search-button", RIGHT, 3, _("Where you can search all supported classes"));
ADD_NODE ("gtk", "adaptor-gtk-buttonbox", BOTTOM, 2.5, _("investigate Gtk object groups"));