496 lines
14 KiB
C
496 lines
14 KiB
C
/* Peg Solitaire
|
|
* #Keywords: GtkGridView, game, drag-and-drop, dnd
|
|
*
|
|
* This demo demonstrates how to use drag-and-drop to implement peg solitaire.
|
|
*
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include <gtk/gtk.h>
|
|
|
|
|
|
/* Create an object for the pegs that get moved around in the game.
|
|
*
|
|
* We implement the GdkPaintable interface for them, so we can use GtkPicture
|
|
* objects for the wholes we put the pegs into.
|
|
*/
|
|
#define SOLITAIRE_TYPE_PEG (solitaire_peg_get_type ())
|
|
G_DECLARE_FINAL_TYPE (SolitairePeg, solitaire_peg, SOLITAIRE, PEG, GObject)
|
|
|
|
/* Declare the struct. */
|
|
struct _SolitairePeg
|
|
{
|
|
GObject parent_instance;
|
|
|
|
int x;
|
|
int y;
|
|
};
|
|
|
|
struct _SolitairePegClass
|
|
{
|
|
GObjectClass parent_class;
|
|
};
|
|
|
|
/* Here, we implement the functionality required by the GdkPaintable interface */
|
|
static void
|
|
solitaire_peg_snapshot (GdkPaintable *paintable,
|
|
GdkSnapshot *snapshot,
|
|
double width,
|
|
double height)
|
|
{
|
|
/* The snapshot function is the only function we need to implement.
|
|
* It does the actual drawing of the paintable.
|
|
*/
|
|
gtk_snapshot_append_color (snapshot,
|
|
&(GdkRGBA) { 0.6, 0.3, 0.0, 1.0 },
|
|
&GRAPHENE_RECT_INIT (0, 0, width, height));
|
|
}
|
|
|
|
static GdkPaintableFlags
|
|
solitaire_peg_get_flags (GdkPaintable *paintable)
|
|
{
|
|
/* The flags are very useful to let GTK know that this image
|
|
* is never going to change.
|
|
* This allows many optimizations and should therefore always
|
|
* be set.
|
|
*/
|
|
return GDK_PAINTABLE_STATIC_CONTENTS | GDK_PAINTABLE_STATIC_SIZE;
|
|
}
|
|
|
|
static int
|
|
solitaire_peg_get_intrinsic_width (GdkPaintable *paintable)
|
|
{
|
|
return 32;
|
|
}
|
|
|
|
static int
|
|
solitaire_peg_get_intrinsic_height (GdkPaintable *paintable)
|
|
{
|
|
return 32;
|
|
}
|
|
|
|
static void
|
|
solitaire_peg_paintable_init (GdkPaintableInterface *iface)
|
|
{
|
|
iface->snapshot = solitaire_peg_snapshot;
|
|
iface->get_flags = solitaire_peg_get_flags;
|
|
iface->get_intrinsic_width = solitaire_peg_get_intrinsic_width;
|
|
iface->get_intrinsic_height = solitaire_peg_get_intrinsic_height;
|
|
}
|
|
|
|
/* When defining the GType, we need to implement the GdkPaintable interface */
|
|
G_DEFINE_TYPE_WITH_CODE (SolitairePeg, solitaire_peg, G_TYPE_OBJECT,
|
|
G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
|
|
solitaire_peg_paintable_init))
|
|
|
|
/* Here's the boilerplate for the GObject declaration.
|
|
* We don't need to do anything special here, because we keep no
|
|
* data of our own.
|
|
*/
|
|
static void
|
|
solitaire_peg_class_init (SolitairePegClass *klass)
|
|
{
|
|
}
|
|
|
|
static void
|
|
solitaire_peg_init (SolitairePeg *peg)
|
|
{
|
|
}
|
|
|
|
/* Add a little setter for the peg's position.
|
|
* We want to track those so that we can check for legal moves
|
|
* during drag-and-drop operations.
|
|
*/
|
|
static void
|
|
solitaire_peg_set_position (SolitairePeg *peg,
|
|
guint x,
|
|
guint y)
|
|
{
|
|
peg->x = x;
|
|
peg->y = y;
|
|
}
|
|
|
|
/* And finally, we add a simple constructor.
|
|
*/
|
|
static SolitairePeg *
|
|
solitaire_peg_new (void)
|
|
{
|
|
return g_object_new (SOLITAIRE_TYPE_PEG, NULL);
|
|
}
|
|
|
|
/*** Helper for finding a win ***/
|
|
|
|
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 (g_object_unref), NULL);
|
|
g_free (path);
|
|
}
|
|
|
|
static int
|
|
check_move (GtkGrid *grid,
|
|
int x,
|
|
int y,
|
|
int dx,
|
|
int dy)
|
|
{
|
|
GtkWidget *image;
|
|
/* We have a peg at x, y.
|
|
* Check if we can move the peg to x + 2*dx, y + 2*dy
|
|
*/
|
|
image = gtk_grid_get_child_at (grid, x + dx, y + dy);
|
|
if (!GTK_IS_IMAGE (image) ||
|
|
!SOLITAIRE_IS_PEG (gtk_image_get_paintable (GTK_IMAGE (image))))
|
|
return 0;
|
|
|
|
image = gtk_grid_get_child_at (grid, x + 2*dx, y + 2*dy);
|
|
if (!GTK_IMAGE (image) ||
|
|
SOLITAIRE_IS_PEG (gtk_image_get_paintable (GTK_IMAGE (image))))
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
check_for_end (GtkGrid *grid)
|
|
{
|
|
GtkWidget *image;
|
|
int x, y;
|
|
int pegs;
|
|
int moves;
|
|
|
|
pegs = 0;
|
|
moves = 0;
|
|
for (x = 0; x < 7; x++)
|
|
{
|
|
for (y = 0; y < 7; y++)
|
|
{
|
|
image = gtk_grid_get_child_at (grid, x, y);
|
|
if (GTK_IS_IMAGE (image) &&
|
|
SOLITAIRE_IS_PEG (gtk_image_get_paintable (GTK_IMAGE (image))))
|
|
{
|
|
pegs++;
|
|
moves += check_move (grid, x, y, 1, 0);
|
|
moves += check_move (grid, x, y, -1, 0);
|
|
moves += check_move (grid, x, y, 0, 1);
|
|
moves += check_move (grid, x, y, 0, -1);
|
|
}
|
|
|
|
if (pegs > 1 && moves > 0)
|
|
break;
|
|
}
|
|
}
|
|
|
|
image = gtk_grid_get_child_at (grid, 3, 3);
|
|
if (pegs == 1 &&
|
|
SOLITAIRE_IS_PEG (gtk_image_get_paintable (GTK_IMAGE (image))))
|
|
celebrate (TRUE);
|
|
else if (moves == 0)
|
|
celebrate (FALSE);
|
|
}
|
|
|
|
|
|
/*** DRAG AND DROP ***/
|
|
|
|
/* The user tries to start a drag operation.
|
|
* We check if the image contains a peg, and if so, we return the
|
|
* peg as the content to be dragged.
|
|
*/
|
|
static GdkContentProvider *
|
|
drag_prepare (GtkDragSource *source,
|
|
double x,
|
|
double y,
|
|
GtkWidget *image)
|
|
{
|
|
GdkPaintable *paintable = gtk_image_get_paintable (GTK_IMAGE (image));
|
|
|
|
if (!SOLITAIRE_IS_PEG (paintable))
|
|
return NULL;
|
|
|
|
return gdk_content_provider_new_typed (SOLITAIRE_TYPE_PEG, paintable);
|
|
}
|
|
|
|
/* This notifies us that the drag has begun.
|
|
* We can now set up the icon and the widget for the ongoing drag.
|
|
*/
|
|
static void
|
|
drag_begin (GtkDragSource *source,
|
|
GdkDrag *drag,
|
|
GtkWidget *image)
|
|
{
|
|
GdkPaintable *paintable = gtk_image_get_paintable (GTK_IMAGE (image));
|
|
|
|
/* We guaranteed in the drag_prepare function above that we
|
|
* only start a drag if a peg is available.
|
|
* So let's make sure we did not screw that up.
|
|
*/
|
|
g_assert (SOLITAIRE_IS_PEG (paintable));
|
|
|
|
/* We use the peg as the drag icon.
|
|
*/
|
|
gtk_drag_source_set_icon (source, paintable, -2, -2);
|
|
|
|
/* We also attach it to the drag operation as custom user data,
|
|
* so that we can get it back later if the drag fails.
|
|
*/
|
|
g_object_set_data (G_OBJECT (drag), "the peg", paintable);
|
|
|
|
/* Because we are busy dragging the peg, we want to unset it
|
|
* on the image.
|
|
*/
|
|
gtk_image_clear (GTK_IMAGE (image));
|
|
}
|
|
|
|
/* This is called once a drag operation has ended (successfully or not).
|
|
* We want to undo what we did in drag_begin() above and react
|
|
* to a potential move of the peg.
|
|
*/
|
|
static void
|
|
drag_end (GtkDragSource *source,
|
|
GdkDrag *drag,
|
|
gboolean delete_data,
|
|
GtkWidget *image)
|
|
{
|
|
SolitairePeg *peg;
|
|
|
|
/* If the drag was successful, we should now delete the peg.
|
|
* We did this in drag_begin() above to prepare for the drag, so
|
|
* there's no need to do anything anymore.
|
|
*/
|
|
if (delete_data)
|
|
return;
|
|
|
|
/* However, if the drag did not succeed, we need to undo what
|
|
* we did in drag_begin() and reinsert the peg here.
|
|
* Because we used it as the drag data
|
|
*/
|
|
peg = g_object_get_data (G_OBJECT (drag), "the peg");
|
|
gtk_image_set_from_paintable (GTK_IMAGE (image), GDK_PAINTABLE (peg));
|
|
}
|
|
|
|
/* Whenever a new drop operation starts, we need to check if we can
|
|
* accept it.
|
|
* The default check unfortunately is not good enough, because it only
|
|
* checks the data type. But we also need to check if our image can
|
|
* even accept data.
|
|
*/
|
|
static gboolean
|
|
drop_accept (GtkDropTarget *target,
|
|
GdkDrop *drop,
|
|
GtkWidget *image)
|
|
{
|
|
/* First, check the drop is actually trying to drop a peg */
|
|
if (!gdk_content_formats_contain_gtype (gdk_drop_get_formats (drop), SOLITAIRE_TYPE_PEG))
|
|
return FALSE;
|
|
|
|
/* If the image already contains a peg, we cannot accept another one */
|
|
if (SOLITAIRE_IS_PEG (gtk_image_get_paintable (GTK_IMAGE (image))))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
drop_drop (GtkDropTarget *target,
|
|
const GValue *value,
|
|
double x,
|
|
double y,
|
|
GtkWidget *image)
|
|
{
|
|
GtkGrid *grid;
|
|
SolitairePeg *peg;
|
|
int image_x, image_y;
|
|
GtkWidget *jumped;
|
|
|
|
grid = GTK_GRID (gtk_widget_get_parent (image));
|
|
/* The value contains the data in the type we demanded.
|
|
* We demanded a SolitairePeg, so that's what we get.
|
|
*/
|
|
peg = g_value_get_object (value);
|
|
|
|
/* Make sure this was a legal move. */
|
|
/* First, figure out the image's position in the grid. */
|
|
gtk_grid_query_child (grid,
|
|
image,
|
|
&image_x, &image_y,
|
|
NULL, NULL);
|
|
|
|
/* If the peg was not moved 2 spaces horizontally or vertically,
|
|
* this was not a valid jump. Reject it.
|
|
*/
|
|
if (!((ABS (image_x - peg->x) == 2 && image_y == peg->y) ||
|
|
(ABS (image_y - peg->y) == 2 && image_x == peg->x)))
|
|
return FALSE;
|
|
|
|
/* Get the widget that was jumped over
|
|
*/
|
|
jumped = gtk_grid_get_child_at (grid,
|
|
(image_x + peg->x) / 2,
|
|
(image_y + peg->y) / 2);
|
|
/* If the jumped widget does not have a peg in it, this move
|
|
* isn't valid.
|
|
*/
|
|
if (!SOLITAIRE_IS_PEG (gtk_image_get_paintable (GTK_IMAGE (jumped))))
|
|
return FALSE;
|
|
|
|
/* Finally, we know it's a legal move. */
|
|
|
|
/* Clear the peg of the jumped-over image */
|
|
gtk_image_clear (GTK_IMAGE (jumped));
|
|
|
|
/* Add the peg to this image */
|
|
solitaire_peg_set_position (peg, image_x, image_y);
|
|
gtk_image_set_from_paintable (GTK_IMAGE (image), GDK_PAINTABLE (peg));
|
|
|
|
/* Maybe we have something to celebrate */
|
|
check_for_end (grid);
|
|
|
|
/* Success! */
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
remove_provider (gpointer data)
|
|
{
|
|
GtkStyleProvider *provider = GTK_STYLE_PROVIDER (data);
|
|
|
|
gtk_style_context_remove_provider_for_display (gdk_display_get_default (), provider);
|
|
g_object_unref (provider);
|
|
}
|
|
|
|
static void
|
|
create_board (GtkWidget *window)
|
|
{
|
|
GtkWidget *grid;
|
|
GtkWidget *image;
|
|
int x, y;
|
|
GtkDragSource *source;
|
|
GtkDropTarget *target;
|
|
GtkCssProvider *provider;
|
|
const char css[] =
|
|
".solitaire-field {"
|
|
" border: 1px solid lightgray;"
|
|
"}";
|
|
|
|
provider = gtk_css_provider_new ();
|
|
gtk_css_provider_load_from_string (provider, css);
|
|
gtk_style_context_add_provider_for_display (gdk_display_get_default (),
|
|
GTK_STYLE_PROVIDER (provider),
|
|
800);
|
|
|
|
grid = gtk_grid_new ();
|
|
gtk_widget_set_halign (grid, GTK_ALIGN_CENTER);
|
|
gtk_widget_set_valign (grid, GTK_ALIGN_CENTER);
|
|
gtk_grid_set_row_spacing (GTK_GRID (grid), 6);
|
|
gtk_grid_set_column_spacing (GTK_GRID (grid), 6);
|
|
gtk_grid_set_row_homogeneous (GTK_GRID (grid), TRUE);
|
|
gtk_grid_set_column_homogeneous (GTK_GRID (grid), TRUE);
|
|
gtk_window_set_child (GTK_WINDOW (window), grid);
|
|
|
|
for (x = 0; x < 7; x++)
|
|
{
|
|
for (y = 0; y < 7; y++)
|
|
{
|
|
if ((x < 2 || x >= 5) && (y < 2 || y >= 5))
|
|
continue;
|
|
|
|
image = gtk_image_new ();
|
|
gtk_widget_add_css_class (image, "solitaire-field");
|
|
gtk_image_set_icon_size (GTK_IMAGE (image), GTK_ICON_SIZE_LARGE);
|
|
if (x != 3 || y != 3)
|
|
{
|
|
SolitairePeg *peg = solitaire_peg_new ();
|
|
solitaire_peg_set_position (peg, x, y);
|
|
gtk_image_set_from_paintable (GTK_IMAGE (image), GDK_PAINTABLE (peg));
|
|
}
|
|
|
|
gtk_grid_attach (GTK_GRID (grid), image, x, y, 1, 1);
|
|
|
|
/* Set up the drag source.
|
|
* This is rather straightforward: Set the supported actions
|
|
* (in our case, pegs can only be moved) and connect all the
|
|
* relevant signals.
|
|
* And because all drag'n'drop handling is done via event controllers,
|
|
* we need to add the controller to the widget.
|
|
*/
|
|
source = gtk_drag_source_new ();
|
|
gtk_drag_source_set_actions (source, GDK_ACTION_MOVE);
|
|
g_signal_connect (source, "prepare", G_CALLBACK (drag_prepare), image);
|
|
g_signal_connect (source, "drag-begin", G_CALLBACK (drag_begin), image);
|
|
g_signal_connect (source, "drag-end", G_CALLBACK (drag_end), image);
|
|
gtk_widget_add_controller (image, GTK_EVENT_CONTROLLER (source));
|
|
|
|
/* Set up the drop target.
|
|
* This is more involved, because the game logic goes here.
|
|
*/
|
|
|
|
/* First we specify the data we accept: pegs.
|
|
* And we only want moves.
|
|
*/
|
|
target = gtk_drop_target_new (SOLITAIRE_TYPE_PEG, GDK_ACTION_MOVE);
|
|
/* Then we connect our signals.
|
|
*/
|
|
g_signal_connect (target, "accept", G_CALLBACK (drop_accept), image);
|
|
g_signal_connect (target, "drop", G_CALLBACK (drop_drop), image);
|
|
/* Finally, like above, we add it to the widget.
|
|
*/
|
|
gtk_widget_add_controller (image, GTK_EVENT_CONTROLLER (target));
|
|
}
|
|
}
|
|
|
|
g_object_set_data_full (G_OBJECT (window), "provider", provider, remove_provider);
|
|
}
|
|
|
|
static void
|
|
restart_game (GtkButton *button,
|
|
GtkWidget *window)
|
|
{
|
|
create_board (window);
|
|
}
|
|
|
|
GtkWidget *
|
|
do_peg_solitaire (GtkWidget *do_widget)
|
|
{
|
|
static GtkWidget *window = NULL;
|
|
|
|
if (!window)
|
|
{
|
|
GtkWidget *header;
|
|
GtkWidget *restart;
|
|
|
|
window = gtk_window_new ();
|
|
|
|
restart = gtk_button_new_from_icon_name ("view-refresh-symbolic");
|
|
g_signal_connect (restart, "clicked", G_CALLBACK (restart_game), window);
|
|
|
|
header = gtk_header_bar_new ();
|
|
gtk_header_bar_pack_start (GTK_HEADER_BAR (header), restart);
|
|
gtk_window_set_display (GTK_WINDOW (window),
|
|
gtk_widget_get_display (do_widget));
|
|
gtk_window_set_title (GTK_WINDOW (window), "Peg Solitaire");
|
|
gtk_window_set_titlebar (GTK_WINDOW (window), header);
|
|
gtk_window_set_default_size (GTK_WINDOW (window), 400, 300);
|
|
g_object_add_weak_pointer (G_OBJECT (window), (gpointer *)&window);
|
|
|
|
create_board (window);
|
|
}
|
|
|
|
if (!gtk_widget_get_visible (window))
|
|
gtk_widget_set_visible (window, TRUE);
|
|
else
|
|
gtk_window_destroy (GTK_WINDOW (window));
|
|
|
|
return window;
|
|
}
|