5. Making an actor respond to button events

5.1. Problem

You want an actor to respond to button events. These might be buttons on an input device like a mouse; or input events caused by other means, like touches on a screen.

Some examples of where this is useful:

  • For implementing button widgets which respond to button clicks.

  • To make actor selections by mouse click (e.g. as part of a drawing application).

  • To recognise a button press followed by pointer motion and button release (e.g. to implement drag and drop or kinetic animations).

5.2. Solution

Connect a handler to the button-press-event and/or button-release-event signals of an actor.

Note

The button-press-event is emitted when a button is pressed (not necessarily released) on a reactive actor; the button-release-event when a button is released on a reactive actor (even if the button was pressed down somewhere else).

First, ensure the actor is reactive:

clutter_actor_set_reactive (actor, TRUE);

Next, create a function to handle the signal(s) you are interested in. The function signature is the same for both the press and release signals:

gboolean
callback_function (ClutterActor *actor,
                   ClutterEvent *event,
                   gpointer      user_data);

You can use a single function as the callback for both signals (or write a different one for each signal). Here's an example function which can be used as a callback for both press and release signals, as it simply pulls data out of the event and displays it:

/* event is a ClutterButtonEvent
 * for both the press and the release signal; it contains
 * data about where the event occurred
 */
static gboolean
button_event_cb (ClutterActor *actor,
                 ClutterEvent *event,
                 gpointer      user_data)
{
  gfloat x, y;
  gchar *event_type;
  guint button_pressed;
  ClutterModifierType state;
  gchar *ctrl_pressed;
  guint32 click_count;

  /* where the pointer was (relative to the stage)
   * when the button event occurred; use
   * clutter_actor_transform_stage_point()
   * to transform to actor-relative coordinates
   */
  clutter_event_get_coords (event, &x, &y);

  /* check whether it was a press or release event */
  event_type = "released";
  if (clutter_event_type (event) == CLUTTER_BUTTON_PRESS)
    event_type = "pressed";

  /* which button triggered the event */
  button_pressed = clutter_event_get_button (event);

  /* keys down when the event occurred;
   * this is a bit mask composed of the bits for each key held down
   * when the button was pressed or released; see the
   * ClutterModifierType enum in the Clutter API docs
   * for a list of the available modifiers
   */
  state = clutter_event_get_state (event);

  ctrl_pressed = "ctrl not pressed";
  if (state & CLUTTER_CONTROL_MASK)
    ctrl_pressed = "ctrl pressed";

  /* click count */
  click_count = clutter_event_get_click_count (event);

  g_debug ("button %d was %s at %.0f,%.0f; %s; click count %d",
           button_pressed,
           event_type,
           x,
           y,
           ctrl_pressed,
           click_count);

  return CLUTTER_EVENT_STOP;
}

Finally, connect the signals to the function(s):

/* connect the press event */
g_signal_connect (actor,
                  "button-press-event",
                  G_CALLBACK (button_event_cb),
                  NULL);

/* connect the release event */
g_signal_connect (actor,
                  "button-release-event",
                  G_CALLBACK (button_event_cb),
                  NULL);

Pressing or releasing a button on the actor will now trigger a call to the button_event_cb() function. See the full example for more details.

5.3. Discussion

Properties of the ClutterButtonEvent emitted by both signals should be examined using the clutter_event_* functions (rather than struct members directly), as in the example above. While most of these functions are self-explanatory, a couple require more explanation: see the sections below.

Also covered below is an alternative approach to handling a button press followed by a release on a single actor: by adding a ClutterClickAction to an actor. See this section for details.

Finally, a longer example is included, showing how to make use of button press, button release and pointer events in a simple drawing application.

5.3.1. Click count

The click count records the number of times a press/release pair occurred in sequence. You can retrieve it via the clutter_event_get_click_count() function.

Note

A press/release pair is effectively a click, so this term will be used from now on throughout this section, to make the explanation simpler. However, the click count has nothing to do with ClutterClickActions, described later.

For clicks to be considered part of the same sequence (for the purposes of counting), all the clicks after the first one must occur within the global double_click_distance (pixels) of the first click; and the time between click n and click n+1 must be <= the global double_click_time (milliseconds).

The clicks do not have to occur on the same actor: providing they occur within the double click distance and time, they are counted as part of the same click sequence. Also note that the clicks don't even have to happen on a reactive actor: providing they happen somewhere on the stage, they will still increment the click count.

The default double click time and distance are stored in the ClutterSettings associated with an application. You can get/set their values like this:

gint double_click_distance;
gint double_click_time;

ClutterSettings *settings = clutter_settings_get_default ();

/* get double click settings */
g_object_get (settings,
              "double-click-distance", &double_click_distance,
              "double-click-time", &double_click_time,
              NULL);

/* set */
g_object_set (settings,
              "double-click-distance", 50,
              "double-click-time", 1000,
              NULL);

5.3.2. Button numbering

clutter_event_get_button() returns an integer representing the pressed or released button.

In the case of a standard scroll mouse, the numbers returned are reliable across different hardware models:

  • 1 = left mouse button in a right-handed configuration, or the right mouse button in a left-handed configuration

  • 2 = scroll wheel button

  • 3 = right mouse button in a right-handed configuration, or the left mouse button in a left-handed configuration

Note

Clutter provides symbolic names for the three buttons above: CLUTTER_BUTTON_PRIMARY, CLUTTER_BUTTON_MIDDLE, and CLUTTER_BUTTON_SECONDARY.

For mice with more buttons, or other types of input devices, the mappings may not be so straightforward: you may have to experiment to see which button returns which value.

5.3.3. ClutterClickAction

ClutterActions add flexible event handling to ClutterActors. They recognise and abstract common sequences of low-level events into a single, more easily managed high-level event. In the case of a ClutterClickAction, the abstraction is over a press followed by a release on a single actor. This is achieved by "synthesising" the press and release signals on the actor: in other words, the action captures those two signals when emitted by a single actor; and, once captured, the action emits a single clicked signal instead of the two signals being emitted by the actor.

The pointer can move off the actor between the press and release, but the press and release must both occur on the same actor, with no intervening presses or releases on other actors. In addition, there are no maximum distance or time constraints on the press and release.

If a press occurs and you want to force it to be released (e.g. to break a pointer grab after a certain length of time has elapsed), use clutter_click_action_release().

On the down side, the clicked signal doesn't present the same detailed ClutterButtonEvent to the handler. So, for example, you can't get a click count from a ClutterClickAction (though you could count the clicks yourself, of course); and you don't have access to the coordinates where the press or release occurred.

To add a click action to a ClutterActor:

ClutterAction *action = clutter_click_action_new ();
clutter_actor_add_action (actor, action);

Note

An actor must still be set to reactive so that its signals can be routed to a click action.

Create a handler function (note the function signature is different from the one for the press or releas signal handler):

void
clicked_cb (ClutterClickAction *action,
            ClutterActor       *actor,
            gpointer            user_data)
{
  /* display the number of the clicked button (equivalent
   * to the number returned by clutter_event_get_button())
   */
  g_debug ("Button %d clicked", clutter_click_action_get_button (action));
}

Connect the signal to the handler:

g_signal_connect (action,
                  "clicked",
                  G_CALLBACK (clicked_cb),
                  NULL);

The example code gives a bit more detail about how to use click actions.

5.4. Full examples

Example 3.6. Examining properties of a ClutterButtonEvent

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

static const ClutterColor stage_color = { 0x33, 0x33, 0x55, 0xff };
static const ClutterColor red_color = { 0xff, 0x00, 0x00, 0xff };
static const ClutterColor green_color = { 0x00, 0xff, 0x00, 0xff };

static gboolean
button_event_cb (ClutterActor *actor,
                 ClutterEvent *event,
                 gpointer      user_data)
{
  gfloat x, y;
  gchar *event_type;
  guint button_pressed;
  ClutterModifierType state;
  gchar *ctrl_pressed;
  guint32 click_count;

  /* where the pointer was when the button event occurred */
  clutter_event_get_coords (event, &x, &y);

  /* check whether it was a press or release event */
  event_type = "released";
  if (clutter_event_type (event) == CLUTTER_BUTTON_PRESS)
    event_type = "pressed";

  /* which button triggered the event */
  button_pressed = clutter_event_get_button (event);

  /* keys down when the button was pressed */
  state = clutter_event_get_state (event);

  ctrl_pressed = "ctrl not pressed";
  if (state & CLUTTER_CONTROL_MASK)
    ctrl_pressed = "ctrl pressed";

  /* click count */
  click_count = clutter_event_get_click_count (event);

  g_debug ("button %d %s at %.0f,%.0f; %s; click count %d",
           button_pressed,
           event_type,
           x,
           y,
           ctrl_pressed,
           click_count);

  return TRUE;
}

int
main (int   argc,
      char *argv[])
{
  ClutterActor *stage;
  ClutterActor *red;
  ClutterActor *green;

  if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
    return 1;

  stage = clutter_stage_new ();
  clutter_actor_set_size (stage, 400, 400);
  clutter_stage_set_color (CLUTTER_STAGE (stage), &stage_color);
  g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);

  red = clutter_rectangle_new_with_color (&red_color);
  clutter_actor_set_size (red, 100, 100);
  clutter_actor_set_position (red, 50, 150);
  clutter_actor_set_reactive (red, TRUE);

  green = clutter_rectangle_new_with_color (&green_color);
  clutter_actor_set_size (green, 100, 100);
  clutter_actor_set_position (green, 250, 150);
  clutter_actor_set_reactive (green, TRUE);

  g_signal_connect (red,
                    "button-press-event",
                    G_CALLBACK (button_event_cb),
                    NULL);

  g_signal_connect (red,
                    "button-release-event",
                    G_CALLBACK (button_event_cb),
                    NULL);

  g_signal_connect (green,
                    "button-press-event",
                    G_CALLBACK (button_event_cb),
                    NULL);

  g_signal_connect (green,
                    "button-release-event",
                    G_CALLBACK (button_event_cb),
                    NULL);

  clutter_container_add (CLUTTER_CONTAINER (stage),
                         red,
                         green,
                         NULL);

  clutter_actor_show (stage);

  clutter_main ();

  return EXIT_SUCCESS;
}


Example 3.7. Using ClutterClickAction to capture button events on an actor

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

static const ClutterColor stage_color = { 0x33, 0x33, 0x55, 0xff };

void
clicked_cb (ClutterClickAction *action,
            ClutterActor       *actor,
            gpointer            user_data)
{
  g_print ("Pointer button %d clicked on actor %s\n",
           clutter_click_action_get_button (action),
           clutter_actor_get_name (actor));
}

int
main (int   argc,
      char *argv[])
{
  ClutterActor *stage;
  ClutterAction *action1;
  ClutterAction *action2;
  ClutterActor *actor1;
  ClutterActor *actor2;

  if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
    return 1;

  stage = clutter_stage_new ();
  clutter_actor_set_size (stage, 400, 400);
  clutter_stage_set_color (CLUTTER_STAGE (stage), &stage_color);
  g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);

  actor1 = clutter_actor_new ();
  clutter_actor_set_name (actor1, "Red Button");
  clutter_actor_set_background_color (actor1, CLUTTER_COLOR_Red);
  clutter_actor_set_size (actor1, 100, 100);
  clutter_actor_set_reactive (actor1, TRUE);
  clutter_actor_set_position (actor1, 50, 150);
  clutter_actor_add_child (stage, actor1);

  actor2 = clutter_actor_new ();
  clutter_actor_set_name (actor2, "Blue Button");
  clutter_actor_set_background_color (actor2, CLUTTER_COLOR_Blue);
  clutter_actor_set_size (actor2, 100, 100);
  clutter_actor_set_position (actor2, 250, 150);
  clutter_actor_set_reactive (actor2, TRUE);
  clutter_actor_add_child (stage, actor2);

  action1 = clutter_click_action_new ();
  clutter_actor_add_action (actor1, action1);

  action2 = clutter_click_action_new ();
  clutter_actor_add_action (actor2, action2);

  g_signal_connect (action1,
                    "clicked",
                    G_CALLBACK (clicked_cb),
                    NULL);

  g_signal_connect (action2,
                    "clicked",
                    G_CALLBACK (clicked_cb),
                    NULL);

  clutter_actor_show (stage);

  clutter_main ();

  return EXIT_SUCCESS;
}


Example 3.8. Using button and pointer events for drawing

This code was inspired by ClutterSmith

/* Simple rectangle drawing using button and pointer events;
 * click, drag and release a mouse button to draw a rectangle
 */
#include <stdlib.h>
#include <clutter/clutter.h>

static const ClutterColor stage_color = { 0x33, 0x33, 0x55, 0xff };
static const ClutterColor lasso_color = { 0xaa, 0xaa, 0xaa, 0x33 };

typedef struct
{
  ClutterActor *actor;
  gfloat        x;
  gfloat        y;
} Lasso;

static guint
random_color_component ()
{
  return (guint) (155 + (100.0 * rand () / (RAND_MAX + 1.0)));
}

static gboolean
button_pressed_cb (ClutterActor *actor,
                   ClutterEvent *event,
                   gpointer      user_data)
{
  Lasso *lasso = (Lasso *) user_data;

  /* start drawing the lasso actor */
  lasso->actor = clutter_rectangle_new_with_color (&lasso_color);

  /* store lasso's start coordinates */
  clutter_event_get_coords (event, &(lasso->x), &(lasso->y));

  clutter_container_add_actor (CLUTTER_CONTAINER (actor), lasso->actor);

  return TRUE;
}

static gboolean
button_released_cb (ClutterActor *stage,
                    ClutterEvent *event,
                    gpointer      user_data)
{
  Lasso *lasso = (Lasso *) user_data;
  ClutterActor *rectangle;
  ClutterColor *random_color;
  gfloat x;
  gfloat y;
  gfloat width;
  gfloat height;

  if (lasso->actor == NULL)
    return TRUE;

  /* create a new rectangle */
  random_color = clutter_color_new (random_color_component (),
                                    random_color_component (),
                                    random_color_component (),
                                    random_color_component ());
  rectangle = clutter_rectangle_new_with_color (random_color);

  /* set the rectangle to the same size and shape as the lasso */
  clutter_actor_get_position (lasso->actor, &x, &y);
  clutter_actor_get_size (lasso->actor, &width, &height);

  clutter_actor_set_position (rectangle, x, y);
  clutter_actor_set_size (rectangle, width, height);

  clutter_container_add_actor (CLUTTER_CONTAINER (stage), rectangle);

  /* clear up the lasso actor */
  clutter_actor_destroy (lasso->actor);
  lasso->actor = NULL;

  clutter_actor_queue_redraw (stage);

  return TRUE;

}

static gboolean
pointer_motion_cb (ClutterActor *stage,
                   ClutterEvent *event,
                   gpointer      user_data)
{
  gfloat pointer_x;
  gfloat pointer_y;
  gfloat new_x;
  gfloat new_y;
  gfloat width;
  gfloat height;

  Lasso *lasso = (Lasso *) user_data;

  if (lasso->actor == NULL)
    return TRUE;

  /* redraw the lasso actor */
  clutter_event_get_coords (event, &pointer_x, &pointer_y);

  new_x = MIN (pointer_x, lasso->x);
  new_y = MIN (pointer_y, lasso->y);
  width = MAX (pointer_x, lasso->x) - new_x;
  height = MAX (pointer_y, lasso->y) - new_y;

  clutter_actor_set_position (lasso->actor, new_x, new_y);
  clutter_actor_set_size (lasso->actor, width, height);

  return TRUE;
}

int
main (int   argc,
      char *argv[])
{
  Lasso *lasso = g_new0 (Lasso, 1);

  ClutterActor *stage;

  /* seed random number generator */
  srand ((unsigned int) time (NULL));

  if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
    return 1;

  stage = clutter_stage_new ();
  clutter_actor_set_size (stage, 320, 240);
  clutter_stage_set_color (CLUTTER_STAGE (stage), &stage_color);
  g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);

  g_signal_connect (stage,
                    "button-press-event",
                    G_CALLBACK (button_pressed_cb),
                    lasso);

  g_signal_connect (stage,
                    "button-release-event",
                    G_CALLBACK (button_released_cb),
                    lasso);

  g_signal_connect (stage,
                    "motion-event",
                    G_CALLBACK (pointer_motion_cb),
                    lasso);

  clutter_actor_show (stage);

  clutter_main ();

  g_free (lasso);

  return EXIT_SUCCESS;
}