524 lines
14 KiB
C
524 lines
14 KiB
C
|
/* Sliding Puzzle
|
||
|
* #Keywords: GdkPaintable, GdkGesture, GtkShortcutController, game
|
||
|
*
|
||
|
* This demo demonstrates how to use gestures and paintables to create a
|
||
|
* small sliding puzzle game.
|
||
|
*/
|
||
|
|
||
|
#include "config.h"
|
||
|
#include <gtk/gtk.h>
|
||
|
|
||
|
/* Include the header for the puzzle piece */
|
||
|
#include "puzzlepiece.h"
|
||
|
#include "paintable.h"
|
||
|
|
||
|
|
||
|
static GtkWidget *window = NULL;
|
||
|
static GtkWidget *frame = NULL;
|
||
|
static GtkWidget *choices = NULL;
|
||
|
static GtkWidget *size_spin = NULL;
|
||
|
static GdkPaintable *puzzle = NULL;
|
||
|
|
||
|
static gboolean solved = TRUE;
|
||
|
static guint width = 3;
|
||
|
static guint height = 3;
|
||
|
static guint pos_x;
|
||
|
static guint pos_y;
|
||
|
|
||
|
static void
|
||
|
ended (GObject *object)
|
||
|
{
|
||
|
g_object_unref (object);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
celebrate (gboolean win)
|
||
|
{
|
||
|
char *path;
|
||
|
GtkMediaStream *stream;
|
||
|
|
||
|
if (win)
|
||
|
path = g_build_filename (GTK_DATADIR, "sounds", "freedesktop", "stereo", "complete.oga", NULL);
|
||
|
else
|
||
|
path = g_build_filename (GTK_DATADIR, "sounds", "freedesktop", "stereo", "suspend-error.oga", NULL);
|
||
|
stream = gtk_media_file_new_for_filename (path);
|
||
|
gtk_media_stream_set_volume (stream, 1.0);
|
||
|
gtk_media_stream_play (stream);
|
||
|
|
||
|
g_signal_connect (stream, "notify::ended", G_CALLBACK (ended), NULL);
|
||
|
g_free (path);
|
||
|
}
|
||
|
|
||
|
static gboolean
|
||
|
move_puzzle (GtkWidget *grid,
|
||
|
int dx,
|
||
|
int dy)
|
||
|
{
|
||
|
GtkWidget *pos, *next;
|
||
|
GdkPaintable *piece;
|
||
|
guint next_x, next_y;
|
||
|
|
||
|
/* We don't move anything if the puzzle is solved */
|
||
|
if (solved)
|
||
|
return FALSE;
|
||
|
|
||
|
/* Return FALSE if we can't move to where the call
|
||
|
* wants us to move.
|
||
|
*/
|
||
|
if ((dx < 0 && pos_x < -dx) ||
|
||
|
dx + pos_x >= width ||
|
||
|
(dy < 0 && pos_y < -dy) ||
|
||
|
dy + pos_y >= height)
|
||
|
return FALSE;
|
||
|
|
||
|
/* Compute the new position */
|
||
|
next_x = pos_x + dx;
|
||
|
next_y = pos_y + dy;
|
||
|
|
||
|
/* Get the current and next image */
|
||
|
pos = gtk_grid_get_child_at (GTK_GRID (grid), pos_x, pos_y);
|
||
|
next = gtk_grid_get_child_at (GTK_GRID (grid), next_x, next_y);
|
||
|
|
||
|
/* Move the displayed piece. */
|
||
|
piece = gtk_picture_get_paintable (GTK_PICTURE (next));
|
||
|
gtk_picture_set_paintable (GTK_PICTURE (pos), piece);
|
||
|
gtk_picture_set_paintable (GTK_PICTURE (next), NULL);
|
||
|
|
||
|
/* Update the current position */
|
||
|
pos_x = next_x;
|
||
|
pos_y = next_y;
|
||
|
|
||
|
/* Return TRUE because we successfully moved the piece */
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
shuffle_puzzle (GtkWidget *grid)
|
||
|
{
|
||
|
guint i, n_steps;
|
||
|
|
||
|
/* Do this many random moves */
|
||
|
n_steps = width * height * 50;
|
||
|
|
||
|
for (i = 0; i < n_steps; i++)
|
||
|
{
|
||
|
/* Get a random number for the direction to move in */
|
||
|
switch (g_random_int_range (0, 4))
|
||
|
{
|
||
|
case 0:
|
||
|
/* left */
|
||
|
move_puzzle (grid, -1, 0);
|
||
|
break;
|
||
|
|
||
|
case 1:
|
||
|
/* up */
|
||
|
move_puzzle (grid, 0, -1);
|
||
|
break;
|
||
|
|
||
|
case 2:
|
||
|
/* right */
|
||
|
move_puzzle (grid, 1, 0);
|
||
|
break;
|
||
|
|
||
|
case 3:
|
||
|
/* down */
|
||
|
move_puzzle (grid, 0, 1);
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
g_assert_not_reached ();
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static gboolean
|
||
|
check_solved (GtkWidget *grid)
|
||
|
{
|
||
|
GtkWidget *picture;
|
||
|
GdkPaintable *piece;
|
||
|
guint x, y;
|
||
|
|
||
|
/* Nothing to check if the puzzle is already solved */
|
||
|
if (solved)
|
||
|
return TRUE;
|
||
|
|
||
|
/* If the empty cell isn't in the bottom right,
|
||
|
* the puzzle is obviously not solved */
|
||
|
if (pos_x != width - 1 ||
|
||
|
pos_y != height - 1)
|
||
|
return FALSE;
|
||
|
|
||
|
/* Check that all pieces are in the right position */
|
||
|
for (y = 0; y < height; y++)
|
||
|
{
|
||
|
for (x = 0; x < width; x++)
|
||
|
{
|
||
|
picture = gtk_grid_get_child_at (GTK_GRID (grid), x, y);
|
||
|
piece = gtk_picture_get_paintable (GTK_PICTURE (picture));
|
||
|
|
||
|
/* empty cell */
|
||
|
if (piece == NULL)
|
||
|
continue;
|
||
|
|
||
|
if (gtk_puzzle_piece_get_x (GTK_PUZZLE_PIECE (piece)) != x ||
|
||
|
gtk_puzzle_piece_get_y (GTK_PUZZLE_PIECE (piece)) != y)
|
||
|
return FALSE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* We solved the puzzle!
|
||
|
*/
|
||
|
solved = TRUE;
|
||
|
|
||
|
/* Fill the empty cell to show that we're done.
|
||
|
*/
|
||
|
picture = gtk_grid_get_child_at (GTK_GRID (grid), 0, 0);
|
||
|
piece = gtk_picture_get_paintable (GTK_PICTURE (picture));
|
||
|
|
||
|
piece = gtk_puzzle_piece_new (gtk_puzzle_piece_get_puzzle (GTK_PUZZLE_PIECE (piece)),
|
||
|
pos_x, pos_y,
|
||
|
width, height);
|
||
|
picture = gtk_grid_get_child_at (GTK_GRID (grid), pos_x, pos_y);
|
||
|
gtk_picture_set_paintable (GTK_PICTURE (picture), piece);
|
||
|
|
||
|
celebrate (TRUE);
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
static gboolean
|
||
|
puzzle_key_pressed (GtkWidget *grid,
|
||
|
GVariant *args,
|
||
|
gpointer unused)
|
||
|
{
|
||
|
int dx, dy;
|
||
|
|
||
|
g_variant_get (args, "(ii)", &dx, &dy);
|
||
|
|
||
|
if (!move_puzzle (grid, dx, dy))
|
||
|
{
|
||
|
/* Make the error sound and then return TRUE.
|
||
|
* We handled this key, even though we didn't
|
||
|
* do anything to the puzzle.
|
||
|
*/
|
||
|
gtk_widget_error_bell (grid);
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
check_solved (grid);
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
puzzle_button_pressed (GtkGestureClick *gesture,
|
||
|
int n_press,
|
||
|
double x,
|
||
|
double y,
|
||
|
GtkWidget *grid)
|
||
|
{
|
||
|
GtkWidget *child;
|
||
|
int l, t, i;
|
||
|
int pos;
|
||
|
|
||
|
child = gtk_widget_pick (grid, x, y, GTK_PICK_DEFAULT);
|
||
|
|
||
|
if (!child)
|
||
|
{
|
||
|
gtk_widget_error_bell (grid);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
gtk_grid_query_child (GTK_GRID (grid), child, &l, &t, NULL, NULL);
|
||
|
|
||
|
if (l == pos_x && t == pos_y)
|
||
|
{
|
||
|
gtk_widget_error_bell (grid);
|
||
|
}
|
||
|
else if (l == pos_x)
|
||
|
{
|
||
|
pos = pos_y;
|
||
|
for (i = t; i < pos; i++)
|
||
|
{
|
||
|
if (!move_puzzle (grid, 0, -1))
|
||
|
gtk_widget_error_bell (grid);
|
||
|
}
|
||
|
for (i = pos; i < t; i++)
|
||
|
{
|
||
|
if (!move_puzzle (grid, 0, 1))
|
||
|
gtk_widget_error_bell (grid);
|
||
|
}
|
||
|
}
|
||
|
else if (t == pos_y)
|
||
|
{
|
||
|
pos = pos_x;
|
||
|
for (i = l; i < pos; i++)
|
||
|
{
|
||
|
if (!move_puzzle (grid, -1, 0))
|
||
|
gtk_widget_error_bell (grid);
|
||
|
}
|
||
|
for (i = pos; i < l; i++)
|
||
|
{
|
||
|
if (!move_puzzle (grid, 1, 0))
|
||
|
gtk_widget_error_bell (grid);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
gtk_widget_error_bell (grid);
|
||
|
}
|
||
|
|
||
|
check_solved (grid);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
add_move_binding (GtkShortcutController *controller,
|
||
|
guint keyval,
|
||
|
guint kp_keyval,
|
||
|
int dx,
|
||
|
int dy)
|
||
|
{
|
||
|
GtkShortcut *shortcut;
|
||
|
|
||
|
shortcut = gtk_shortcut_new_with_arguments (
|
||
|
gtk_alternative_trigger_new (gtk_keyval_trigger_new (keyval, 0),
|
||
|
gtk_keyval_trigger_new (kp_keyval, 0)),
|
||
|
gtk_callback_action_new (puzzle_key_pressed, NULL, NULL),
|
||
|
"(ii)", dx, dy);
|
||
|
gtk_shortcut_controller_add_shortcut (controller, shortcut);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
start_puzzle (GdkPaintable *paintable)
|
||
|
{
|
||
|
GtkWidget *picture, *grid;
|
||
|
GtkEventController *controller;
|
||
|
guint x, y;
|
||
|
float aspect_ratio;
|
||
|
|
||
|
/* Create a new grid */
|
||
|
grid = gtk_grid_new ();
|
||
|
gtk_widget_set_focusable (grid, TRUE);
|
||
|
gtk_aspect_frame_set_child (GTK_ASPECT_FRAME (frame), grid);
|
||
|
aspect_ratio = gdk_paintable_get_intrinsic_aspect_ratio (paintable);
|
||
|
if (aspect_ratio == 0.0)
|
||
|
aspect_ratio = 1.0;
|
||
|
gtk_aspect_frame_set_ratio (GTK_ASPECT_FRAME (frame), aspect_ratio);
|
||
|
gtk_aspect_frame_set_obey_child (GTK_ASPECT_FRAME (frame), FALSE);
|
||
|
|
||
|
/* Add shortcuts so people can use the arrow
|
||
|
* keys to move the puzzle
|
||
|
*/
|
||
|
controller = gtk_shortcut_controller_new ();
|
||
|
gtk_shortcut_controller_set_scope (GTK_SHORTCUT_CONTROLLER (controller),
|
||
|
GTK_SHORTCUT_SCOPE_LOCAL);
|
||
|
add_move_binding (GTK_SHORTCUT_CONTROLLER (controller),
|
||
|
GDK_KEY_Left, GDK_KEY_KP_Left,
|
||
|
-1, 0);
|
||
|
add_move_binding (GTK_SHORTCUT_CONTROLLER (controller),
|
||
|
GDK_KEY_Right, GDK_KEY_KP_Right,
|
||
|
1, 0);
|
||
|
add_move_binding (GTK_SHORTCUT_CONTROLLER (controller),
|
||
|
GDK_KEY_Up, GDK_KEY_KP_Up,
|
||
|
0, -1);
|
||
|
add_move_binding (GTK_SHORTCUT_CONTROLLER (controller),
|
||
|
GDK_KEY_Down, GDK_KEY_KP_Down,
|
||
|
0, 1);
|
||
|
gtk_widget_add_controller (GTK_WIDGET (grid), controller);
|
||
|
|
||
|
controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ());
|
||
|
g_signal_connect (controller, "pressed",
|
||
|
G_CALLBACK (puzzle_button_pressed),
|
||
|
grid);
|
||
|
gtk_widget_add_controller (GTK_WIDGET (grid), controller);
|
||
|
|
||
|
/* Make sure the cells have equal size */
|
||
|
gtk_grid_set_row_homogeneous (GTK_GRID (grid), TRUE);
|
||
|
gtk_grid_set_column_homogeneous (GTK_GRID (grid), TRUE);
|
||
|
|
||
|
/* Reset the variables */
|
||
|
solved = FALSE;
|
||
|
pos_x = width - 1;
|
||
|
pos_y = height - 1;
|
||
|
|
||
|
/* add a picture for every cell */
|
||
|
for (y = 0; y < height; y++)
|
||
|
{
|
||
|
for (x = 0; x < width; x++)
|
||
|
{
|
||
|
GdkPaintable *piece;
|
||
|
|
||
|
/* Don't paint anything for the lsiding part of the video */
|
||
|
if (x == pos_x && y == pos_y)
|
||
|
piece = NULL;
|
||
|
else
|
||
|
piece = gtk_puzzle_piece_new (paintable,
|
||
|
x, y,
|
||
|
width, height);
|
||
|
picture = gtk_picture_new_for_paintable (piece);
|
||
|
gtk_picture_set_content_fit (GTK_PICTURE (picture), GTK_CONTENT_FIT_FILL);
|
||
|
gtk_grid_attach (GTK_GRID (grid),
|
||
|
picture,
|
||
|
x, y,
|
||
|
1, 1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
shuffle_puzzle (grid);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
reshuffle (void)
|
||
|
{
|
||
|
GtkWidget *grid;
|
||
|
|
||
|
if (solved)
|
||
|
{
|
||
|
start_puzzle (puzzle);
|
||
|
grid = gtk_aspect_frame_get_child (GTK_ASPECT_FRAME (frame));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
grid = gtk_aspect_frame_get_child (GTK_ASPECT_FRAME (frame));
|
||
|
shuffle_puzzle (grid);
|
||
|
}
|
||
|
gtk_widget_grab_focus (grid);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
reconfigure (void)
|
||
|
{
|
||
|
GtkWidget *popover;
|
||
|
GtkWidget *grid;
|
||
|
GtkWidget *child;
|
||
|
GtkWidget *image;
|
||
|
GList *selected;
|
||
|
|
||
|
width = height = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (size_spin));
|
||
|
|
||
|
selected = gtk_flow_box_get_selected_children (GTK_FLOW_BOX (choices));
|
||
|
if (selected == NULL)
|
||
|
child = gtk_widget_get_first_child (choices);
|
||
|
else
|
||
|
{
|
||
|
child = selected->data;
|
||
|
g_list_free (selected);
|
||
|
}
|
||
|
|
||
|
image = gtk_flow_box_child_get_child (GTK_FLOW_BOX_CHILD (child));
|
||
|
puzzle = gtk_image_get_paintable (GTK_IMAGE (image));
|
||
|
|
||
|
start_puzzle (puzzle);
|
||
|
popover = gtk_widget_get_ancestor (size_spin, GTK_TYPE_POPOVER);
|
||
|
gtk_popover_popdown (GTK_POPOVER (popover));
|
||
|
grid = gtk_aspect_frame_get_child (GTK_ASPECT_FRAME (frame));
|
||
|
gtk_widget_grab_focus (grid);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
add_choice (GtkWidget *container,
|
||
|
GdkPaintable *paintable)
|
||
|
{
|
||
|
GtkWidget *icon;
|
||
|
|
||
|
icon = gtk_image_new_from_paintable (paintable);
|
||
|
gtk_image_set_icon_size (GTK_IMAGE (icon), GTK_ICON_SIZE_LARGE);
|
||
|
|
||
|
gtk_flow_box_insert (GTK_FLOW_BOX (container), icon, -1);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
widget_destroyed (gpointer data,
|
||
|
GObject *widget)
|
||
|
{
|
||
|
if (data)
|
||
|
*(gpointer *) data = NULL;
|
||
|
}
|
||
|
|
||
|
|
||
|
GtkWidget *
|
||
|
do_sliding_puzzle (GtkWidget *do_widget)
|
||
|
{
|
||
|
if (!window)
|
||
|
{
|
||
|
GtkWidget *header;
|
||
|
GtkWidget *restart;
|
||
|
GtkWidget *tweak;
|
||
|
GtkWidget *popover;
|
||
|
GtkWidget *tweaks;
|
||
|
GtkWidget *apply;
|
||
|
GtkWidget *label;
|
||
|
GtkWidget *sw;
|
||
|
GtkMediaStream *media;
|
||
|
|
||
|
puzzle = GDK_PAINTABLE (gdk_texture_new_from_resource ("/sliding_puzzle/portland-rose.jpg"));
|
||
|
|
||
|
tweaks = gtk_grid_new ();
|
||
|
gtk_grid_set_row_spacing (GTK_GRID (tweaks), 10);
|
||
|
gtk_grid_set_column_spacing (GTK_GRID (tweaks), 10);
|
||
|
gtk_widget_set_margin_start (tweaks, 10);
|
||
|
gtk_widget_set_margin_end (tweaks, 10);
|
||
|
gtk_widget_set_margin_top (tweaks, 10);
|
||
|
gtk_widget_set_margin_bottom (tweaks, 10);
|
||
|
|
||
|
choices = gtk_flow_box_new ();
|
||
|
gtk_widget_add_css_class (choices, "view");
|
||
|
add_choice (choices, puzzle);
|
||
|
add_choice (choices, gtk_nuclear_animation_new (TRUE));
|
||
|
media = gtk_media_file_new_for_resource ("/images/gtk-logo.webm");
|
||
|
gtk_media_stream_set_loop (media, TRUE);
|
||
|
gtk_media_stream_set_muted (media, TRUE);
|
||
|
gtk_media_stream_play (media);
|
||
|
add_choice (choices, GDK_PAINTABLE (media));
|
||
|
sw = gtk_scrolled_window_new ();
|
||
|
gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), choices);
|
||
|
gtk_grid_attach (GTK_GRID (tweaks), sw, 0, 0, 2, 1);
|
||
|
|
||
|
label = gtk_label_new ("Size");
|
||
|
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
|
||
|
gtk_grid_attach (GTK_GRID (tweaks), label, 0, 1, 1, 1);
|
||
|
size_spin = gtk_spin_button_new_with_range (2, 10, 1);
|
||
|
gtk_spin_button_set_value (GTK_SPIN_BUTTON (size_spin), width);
|
||
|
gtk_grid_attach (GTK_GRID (tweaks), size_spin, 1, 1, 1, 1);
|
||
|
|
||
|
apply = gtk_button_new_with_label ("Apply");
|
||
|
gtk_widget_set_halign (apply, GTK_ALIGN_END);
|
||
|
gtk_grid_attach (GTK_GRID (tweaks), apply, 1, 2, 1, 1);
|
||
|
g_signal_connect (apply, "clicked", G_CALLBACK (reconfigure), NULL);
|
||
|
|
||
|
popover = gtk_popover_new ();
|
||
|
gtk_popover_set_child (GTK_POPOVER (popover), tweaks);
|
||
|
|
||
|
tweak = gtk_menu_button_new ();
|
||
|
gtk_menu_button_set_popover (GTK_MENU_BUTTON (tweak), popover);
|
||
|
gtk_menu_button_set_icon_name (GTK_MENU_BUTTON (tweak), "emblem-system-symbolic");
|
||
|
|
||
|
restart = gtk_button_new_from_icon_name ("view-refresh-symbolic");
|
||
|
g_signal_connect (restart, "clicked", G_CALLBACK (reshuffle), NULL);
|
||
|
|
||
|
header = gtk_header_bar_new ();
|
||
|
gtk_header_bar_pack_start (GTK_HEADER_BAR (header), restart);
|
||
|
gtk_header_bar_pack_end (GTK_HEADER_BAR (header), tweak);
|
||
|
window = gtk_window_new ();
|
||
|
gtk_window_set_display (GTK_WINDOW (window),
|
||
|
gtk_widget_get_display (do_widget));
|
||
|
gtk_window_set_title (GTK_WINDOW (window), "Sliding Puzzle");
|
||
|
gtk_window_set_titlebar (GTK_WINDOW (window), header);
|
||
|
gtk_window_set_default_size (GTK_WINDOW (window), 400, 300);
|
||
|
g_object_weak_ref (G_OBJECT (window), widget_destroyed, &window);
|
||
|
|
||
|
frame = gtk_aspect_frame_new (0.5, 0.5, (float) gdk_paintable_get_intrinsic_aspect_ratio (puzzle), FALSE);
|
||
|
gtk_window_set_child (GTK_WINDOW (window), frame);
|
||
|
|
||
|
start_puzzle (puzzle);
|
||
|
}
|
||
|
|
||
|
if (!gtk_widget_get_visible (window))
|
||
|
gtk_widget_set_visible (window, TRUE);
|
||
|
else
|
||
|
gtk_window_destroy (GTK_WINDOW (window));
|
||
|
|
||
|
return window;
|
||
|
}
|