/* Touch and Drawing Tablets
 *
 * Demonstrates advanced handling of event information from exotic
 * input devices.
 *
 * On one hand, this snippet demonstrates management of drawing tablets,
 * those contain additional information for the pointer other than
 * X/Y coordinates. Tablet pads events are mapped to actions, which
 * are both defined and interpreted by the application.
 *
 * Input axes are dependent on hardware devices, on linux/unix you
 * can see the device axes through xinput list <device>. Each time
 * a different hardware device is used to move the pointer, the
 * master device will be updated to match the axes it provides,
 * these changes can be tracked through GdkDevice::changed, or
 * checking gdk_event_get_source_device().
 *
 * On the other hand, this demo handles basic multitouch events,
 * each event coming from an specific touchpoint will contain a
 * GdkEventSequence that's unique for its lifetime, so multiple
 * touchpoints can be tracked.
 */

#include <glib/gi18n.h>
#include <gtk/gtk.h>

typedef struct {
  GdkDevice *last_source;
  GdkDeviceTool *last_tool;
  gdouble *axes;
  GdkRGBA color;
  gdouble x;
  gdouble y;
} AxesInfo;

typedef struct {
  GHashTable *pointer_info; /* GdkDevice -> AxesInfo */
  GHashTable *touch_info; /* GdkEventSequence -> AxesInfo */
} EventData;

const gchar *colors[] = {
  "black",
  "orchid",
  "fuchsia",
  "indigo",
  "thistle",
  "sienna",
  "azure",
  "plum",
  "lime",
  "navy",
  "maroon",
  "burlywood"
};

static GtkPadActionEntry pad_actions[] = {
  { GTK_PAD_ACTION_BUTTON, 1, -1, N_("Nuclear strike"), "pad.nuke" },
  { GTK_PAD_ACTION_BUTTON, 2, -1, N_("Release siberian methane reserves"), "pad.heat" },
  { GTK_PAD_ACTION_BUTTON, 3, -1, N_("Release solar flare"), "pad.fry" },
  { GTK_PAD_ACTION_BUTTON, 4, -1, N_("De-stabilize Oort cloud"), "pad.fall" },
  { GTK_PAD_ACTION_BUTTON, 5, -1, N_("Ignite WR-104"), "pad.burst" },
  { GTK_PAD_ACTION_BUTTON, 6, -1, N_("Lart whoever asks about this button"), "pad.lart" },
  { GTK_PAD_ACTION_RING, -1, -1, N_("Earth axial tilt"), "pad.tilt" },
  { GTK_PAD_ACTION_STRIP, -1, -1, N_("Extent of weak nuclear force"), "pad.dissolve" },
};

static const gchar *pad_action_results[] = {
  "☢",
  "♨",
  "☼",
  "☄",
  "⚡",
  "💫",
  "◑",
  "⚛"
};

static guint cur_color = 0;
static guint pad_action_timeout_id = 0;

static AxesInfo *
axes_info_new (void)
{
  AxesInfo *info;

  info = g_new0 (AxesInfo, 1);
  gdk_rgba_parse (&info->color, colors[cur_color]);

  cur_color = (cur_color + 1) % G_N_ELEMENTS (colors);

  return info;
}

static EventData *
event_data_new (void)
{
  EventData *data;

  data = g_new0 (EventData, 1);
  data->pointer_info = g_hash_table_new_full (NULL, NULL, NULL,
                                              (GDestroyNotify) g_free);
  data->touch_info = g_hash_table_new_full (NULL, NULL, NULL,
                                            (GDestroyNotify) g_free);

  return data;
}

static void
event_data_free (EventData *data)
{
  g_hash_table_destroy (data->pointer_info);
  g_hash_table_destroy (data->touch_info);
  g_free (data);
}

static void
update_axes_from_event (GdkEvent  *event,
                        EventData *data)
{
  GdkDevice *device, *source_device;
  GdkEventSequence *sequence;
  GdkDeviceTool *tool;
  gdouble x, y;
  AxesInfo *info;

  device = gdk_event_get_device (event);
  source_device = gdk_event_get_source_device (event);
  sequence = gdk_event_get_event_sequence (event);
  tool = gdk_event_get_device_tool (event);

  if (event->type == GDK_TOUCH_END ||
      event->type == GDK_TOUCH_CANCEL)
    {
      g_hash_table_remove (data->touch_info, sequence);
      return;
    }
  else if (event->type == GDK_LEAVE_NOTIFY)
    {
      g_hash_table_remove (data->pointer_info, device);
      return;
    }

  if (!source_device)
    return;

  if (!sequence)
    {
      info = g_hash_table_lookup (data->pointer_info, device);

      if (!info)
        {
          info = axes_info_new ();
          g_hash_table_insert (data->pointer_info, device, info);
        }
    }
  else
    {
      info = g_hash_table_lookup (data->touch_info, sequence);

      if (!info)
        {
          info = axes_info_new ();
          g_hash_table_insert (data->touch_info, sequence, info);
        }
    }

  if (info->last_source != source_device)
    info->last_source = source_device;

  if (info->last_tool != tool)
    info->last_tool = tool;

  g_clear_pointer (&info->axes, g_free);

  if (event->type == GDK_TOUCH_BEGIN ||
      event->type == GDK_TOUCH_UPDATE)
    {
      if (sequence && event->touch.emulating_pointer)
        g_hash_table_remove (data->pointer_info, device);
    }
  if (event->type == GDK_MOTION_NOTIFY)
    {
      info->axes =
      g_memdup (event->motion.axes,
                sizeof (gdouble) * gdk_device_get_n_axes (source_device));
    }
  else if (event->type == GDK_BUTTON_PRESS ||
           event->type == GDK_BUTTON_RELEASE)
    {
      info->axes =
      g_memdup (event->button.axes,
                sizeof (gdouble) * gdk_device_get_n_axes (source_device));
    }

  if (gdk_event_get_coords (event, &x, &y))
    {
      info->x = x;
      info->y = y;
    }
}

static gboolean
event_cb (GtkWidget *widget,
          GdkEvent  *event,
          gpointer   user_data)
{
  update_axes_from_event (event, user_data);
  gtk_widget_queue_draw (widget);
  return FALSE;
}

static void
render_arrow (cairo_t     *cr,
              gdouble      x_diff,
              gdouble      y_diff,
              const gchar *label)
{
  cairo_save (cr);

  cairo_set_source_rgb (cr, 0, 0, 0);
  cairo_new_path (cr);
  cairo_move_to (cr, 0, 0);
  cairo_line_to (cr, x_diff, y_diff);
  cairo_stroke (cr);

  cairo_move_to (cr, x_diff, y_diff);
  cairo_show_text (cr, label);

  cairo_restore (cr);
}

static void
draw_axes_info (cairo_t       *cr,
                AxesInfo      *info,
                GtkAllocation *allocation)
{
  gdouble pressure, tilt_x, tilt_y, distance, wheel, rotation, slider;
  GdkAxisFlags axes = gdk_device_get_axes (info->last_source);

  cairo_save (cr);

  cairo_set_line_width (cr, 1);
  gdk_cairo_set_source_rgba (cr, &info->color);

  cairo_move_to (cr, 0, info->y);
  cairo_line_to (cr, allocation->width, info->y);
  cairo_move_to (cr, info->x, 0);
  cairo_line_to (cr, info->x, allocation->height);
  cairo_stroke (cr);

  cairo_translate (cr, info->x, info->y);

  if (!info->axes)
    {
      cairo_restore (cr);
      return;
    }

  if (axes & GDK_AXIS_FLAG_PRESSURE)
    {
      cairo_pattern_t *pattern;

      gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_PRESSURE,
                           &pressure);

      pattern = cairo_pattern_create_radial (0, 0, 0, 0, 0, 100);
      cairo_pattern_add_color_stop_rgba (pattern, pressure, 1, 0, 0, pressure);
      cairo_pattern_add_color_stop_rgba (pattern, 1, 0, 0, 1, 0);

      cairo_set_source (cr, pattern);

      cairo_arc (cr, 0, 0, 100, 0, 2 * G_PI);
      cairo_fill (cr);

      cairo_pattern_destroy (pattern);
    }

  if (axes & GDK_AXIS_FLAG_XTILT &&
      axes & GDK_AXIS_FLAG_YTILT)
    {
      gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_XTILT,
                           &tilt_x);
      gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_YTILT,
                           &tilt_y);

      render_arrow (cr, tilt_x * 100, tilt_y * 100, "Tilt");
    }

  if (axes & GDK_AXIS_FLAG_DISTANCE)
    {
      double dashes[] = { 5.0, 5.0 };
      cairo_text_extents_t extents;

      gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_DISTANCE,
                           &distance);

      cairo_save (cr);

      cairo_move_to (cr, distance * 100, 0);

      cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
      cairo_set_dash (cr, dashes, 2, 0.0);
      cairo_arc (cr, 0, 0, distance * 100, 0, 2 * G_PI);
      cairo_stroke (cr);

      cairo_move_to (cr, 0, -distance * 100);
      cairo_text_extents (cr, "Distance", &extents);
      cairo_rel_move_to (cr, -extents.width / 2, 0);
      cairo_show_text (cr, "Distance");

      cairo_move_to (cr, 0, 0);

      cairo_restore (cr);
    }

  if (axes & GDK_AXIS_FLAG_WHEEL)
    {
      gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_WHEEL,
                           &wheel);

      cairo_save (cr);
      cairo_set_line_width (cr, 10);
      cairo_set_source_rgba (cr, 0, 0, 0, 0.5);

      cairo_new_sub_path (cr);
      cairo_arc (cr, 0, 0, 100, 0, wheel * 2 * G_PI);
      cairo_stroke (cr);
      cairo_restore (cr);
    }

  if (axes & GDK_AXIS_FLAG_ROTATION)
    {
      gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_ROTATION,
                           &rotation);
      rotation *= 2 * G_PI;

      cairo_save (cr);
      cairo_rotate (cr, - G_PI / 2);
      cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
      cairo_set_line_width (cr, 5);

      cairo_new_sub_path (cr);
      cairo_arc (cr, 0, 0, 100, 0, rotation);
      cairo_stroke (cr);
      cairo_restore (cr);
    }

  if (axes & GDK_AXIS_FLAG_SLIDER)
    {
      cairo_pattern_t *pattern, *mask;

      gdk_device_get_axis (info->last_source, info->axes, GDK_AXIS_SLIDER,
                           &slider);

      cairo_save (cr);

      cairo_move_to (cr, 0, -10);
      cairo_rel_line_to (cr, 0, -50);
      cairo_rel_line_to (cr, 10, 0);
      cairo_rel_line_to (cr, -5, 50);
      cairo_close_path (cr);

      cairo_clip_preserve (cr);

      pattern = cairo_pattern_create_linear (0, -10, 0, -60);
      cairo_pattern_add_color_stop_rgb (pattern, 0, 0, 1, 0);
      cairo_pattern_add_color_stop_rgb (pattern, 1, 1, 0, 0);
      cairo_set_source (cr, pattern);
      cairo_pattern_destroy (pattern);

      mask = cairo_pattern_create_linear (0, -10, 0, -60);
      cairo_pattern_add_color_stop_rgba (mask, 0, 0, 0, 0, 1);
      cairo_pattern_add_color_stop_rgba (mask, slider, 0, 0, 0, 1);
      cairo_pattern_add_color_stop_rgba (mask, slider, 0, 0, 0, 0);
      cairo_pattern_add_color_stop_rgba (mask, 1, 0, 0, 0, 0);
      cairo_mask (cr, mask);
      cairo_pattern_destroy (mask);

      cairo_set_source_rgb (cr, 0, 0, 0);
      cairo_stroke (cr);

      cairo_restore (cr);
    }

  cairo_restore (cr);
}

static const gchar *
tool_type_to_string (GdkDeviceToolType tool_type)
{
  switch (tool_type)
    {
    case GDK_DEVICE_TOOL_TYPE_PEN:
      return "Pen";
    case GDK_DEVICE_TOOL_TYPE_ERASER:
      return "Eraser";
    case GDK_DEVICE_TOOL_TYPE_BRUSH:
      return "Brush";
    case GDK_DEVICE_TOOL_TYPE_PENCIL:
      return "Pencil";
    case GDK_DEVICE_TOOL_TYPE_AIRBRUSH:
      return "Airbrush";
    case GDK_DEVICE_TOOL_TYPE_MOUSE:
      return "Mouse";
    case GDK_DEVICE_TOOL_TYPE_LENS:
      return "Lens cursor";
    case GDK_DEVICE_TOOL_TYPE_UNKNOWN:
    default:
      return "Unknown";
    }
}

static void
draw_device_info (GtkWidget        *widget,
                  cairo_t          *cr,
                  GdkEventSequence *sequence,
                  gint             *y,
                  AxesInfo         *info)
{
  PangoLayout *layout;
  GString *string;
  gint height;

  cairo_save (cr);

  string = g_string_new (NULL);
  g_string_append_printf (string, "Source: %s",
                          gdk_device_get_name (info->last_source));

  if (sequence)
    g_string_append_printf (string, "\nSequence: %d",
                            GPOINTER_TO_UINT (sequence));

  if (info->last_tool)
    {
      const gchar *tool_type;
      guint64 serial;

      tool_type = tool_type_to_string (gdk_device_tool_get_tool_type (info->last_tool));
      serial = gdk_device_tool_get_serial (info->last_tool);
      g_string_append_printf (string, "\nTool: %s", tool_type);

      if (serial != 0)
        g_string_append_printf (string, ", Serial: %lx", serial);
    }

  cairo_move_to (cr, 10, *y);
  layout = gtk_widget_create_pango_layout (widget, string->str);
  pango_cairo_show_layout (cr, layout);
  cairo_stroke (cr);

  pango_layout_get_pixel_size (layout, NULL, &height);

  gdk_cairo_set_source_rgba (cr, &info->color);
  cairo_set_line_width (cr, 10);
  cairo_move_to (cr, 0, *y);

  *y = *y + height;
  cairo_line_to (cr, 0, *y);
  cairo_stroke (cr);

  cairo_restore (cr);

  g_object_unref (layout);
  g_string_free (string, TRUE);
}

static gboolean
draw_cb (GtkWidget *widget,
         cairo_t   *cr,
         gpointer   user_data)
{
  EventData *data = user_data;
  GtkAllocation allocation;
  AxesInfo *info;
  GHashTableIter iter;
  gpointer key, value;
  gint y = 0;

  gtk_widget_get_allocation (widget, &allocation);

  /* Draw Abs info */
  g_hash_table_iter_init (&iter, data->pointer_info);

  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      info = value;
      draw_axes_info (cr, info, &allocation);
    }

  g_hash_table_iter_init (&iter, data->touch_info);

  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      info = value;
      draw_axes_info (cr, info, &allocation);
    }

  /* Draw name, color legend and misc data */
  g_hash_table_iter_init (&iter, data->pointer_info);

  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      info = value;
      draw_device_info (widget, cr, NULL, &y, info);
    }

  g_hash_table_iter_init (&iter, data->touch_info);

  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      info = value;
      draw_device_info (widget, cr, key, &y, info);
    }

  return FALSE;
}

static void
update_label_text (GtkWidget   *label,
                   const gchar *text)
{
  gchar *markup = NULL;

  if (text)
    markup = g_strdup_printf ("<span font='48.0'>%s</span>", text);
  gtk_label_set_markup (GTK_LABEL (label), markup);
  g_free (markup);
}

static gboolean
reset_label_text_timeout_cb (gpointer user_data)
{
  GtkWidget *label = user_data;

  update_label_text (label, NULL);
  pad_action_timeout_id = 0;
  return G_SOURCE_REMOVE;
}

static void
update_label_and_timeout (GtkWidget   *label,
                          const gchar *text)
{
  if (pad_action_timeout_id)
    g_source_remove (pad_action_timeout_id);

  update_label_text (label, text);
  pad_action_timeout_id = g_timeout_add (200, reset_label_text_timeout_cb, label);
}

static void
on_action_activate (GSimpleAction *action,
                    GVariant      *parameter,
                    gpointer       user_data)
{
  GtkWidget *label = user_data;
  const gchar *result;
  gchar *str;

  result = g_object_get_data (G_OBJECT (action), "action-result");

  if (!parameter)
    update_label_and_timeout (label, result);
  else
    {
      str = g_strdup_printf ("%s %.2f", result, g_variant_get_double (parameter));
      update_label_and_timeout (label, str);
      g_free (str);
    }
}

static void
init_pad_controller (GtkWidget *window,
                     GtkWidget *label)
{
  GtkPadController *pad_controller;
  GSimpleActionGroup *action_group;
  GSimpleAction *action;
  gint i;

  action_group = g_simple_action_group_new ();
  pad_controller = gtk_pad_controller_new (GTK_WINDOW (window),
                                           G_ACTION_GROUP (action_group),
                                           NULL);

  for (i = 0; i < G_N_ELEMENTS (pad_actions); i++)
    {
      if (pad_actions[i].type == GTK_PAD_ACTION_BUTTON)
        {
          action = g_simple_action_new (pad_actions[i].action_name, NULL);
        }
      else
        {
          action = g_simple_action_new_stateful (pad_actions[i].action_name,
                                                 G_VARIANT_TYPE_DOUBLE, NULL);
        }

      g_signal_connect (action, "activate",
                        G_CALLBACK (on_action_activate), label);
      g_object_set_data (G_OBJECT (action), "action-result",
                         (gpointer) pad_action_results[i]);
      g_action_map_add_action (G_ACTION_MAP (action_group), G_ACTION (action));
      g_object_unref (action);
    }

  gtk_pad_controller_set_action_entries (pad_controller, pad_actions,
                                         G_N_ELEMENTS (pad_actions));
  g_object_set_data_full (G_OBJECT (window), "pad-controller",
                          pad_controller, g_object_unref);

  g_object_unref (action_group);
}

GtkWidget *
do_event_axes (GtkWidget *toplevel)
{
  static GtkWidget *window = NULL;
  EventData *event_data;
  GtkWidget *label;

  if (!window)
    {
      window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
      gtk_window_set_title (GTK_WINDOW (window), "Event Axes");
      gtk_window_set_default_size (GTK_WINDOW (window), 400, 400);

      g_signal_connect (window, "destroy",
                        G_CALLBACK (gtk_widget_destroyed), &window);

      gtk_widget_set_support_multidevice (window, TRUE);

      event_data = event_data_new ();
      g_object_set_data_full (G_OBJECT (window), "gtk-demo-event-data",
                              event_data, (GDestroyNotify) event_data_free);

      g_signal_connect (window, "event",
                        G_CALLBACK (event_cb), event_data);
      g_signal_connect (window, "draw",
                        G_CALLBACK (draw_cb), event_data);

      label = gtk_label_new ("");
      gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
      gtk_container_add (GTK_CONTAINER (window), label);

      init_pad_controller (window, label);
    }

  if (!gtk_widget_get_visible (window))
    gtk_widget_show (window);
  else
    gtk_widget_destroy (window);

  return window;
}
