1 | /* Peg Solitaire |
2 | * #Keywords: GtkGridView, game |
3 | * |
4 | * This demo demonstrates how to use drag-and-drop to implement peg solitaire. |
5 | * |
6 | */ |
7 | |
8 | #include "config.h" |
9 | #include <gtk/gtk.h> |
10 | |
11 | /* Create an object for the pegs that get moved around in the game. |
12 | * |
13 | * We implement the GdkPaintable interface for them, so we can use GtkPicture |
14 | * objects for the wholes we put the pegs into. |
15 | */ |
16 | #define SOLITAIRE_TYPE_PEG (solitaire_peg_get_type ()) |
17 | G_DECLARE_FINAL_TYPE (SolitairePeg, solitaire_peg, SOLITAIRE, PEG, GObject) |
18 | |
19 | /* Declare the struct. */ |
20 | struct _SolitairePeg |
21 | { |
22 | GObject parent_instance; |
23 | |
24 | int x; |
25 | int y; |
26 | }; |
27 | |
28 | struct _SolitairePegClass |
29 | { |
30 | GObjectClass parent_class; |
31 | }; |
32 | |
33 | /* Here, we implement the functionality required by the GdkPaintable interface */ |
34 | static void |
35 | solitaire_peg_snapshot (GdkPaintable *paintable, |
36 | GdkSnapshot *snapshot, |
37 | double width, |
38 | double height) |
39 | { |
40 | /* The snapshot function is the only function we need to implement. |
41 | * It does the actual drawing of the paintable. |
42 | */ |
43 | gtk_snapshot_append_color (snapshot, |
44 | color: &(GdkRGBA) { 0.6, 0.3, 0.0, 1.0 }, |
45 | bounds: &GRAPHENE_RECT_INIT (0, 0, width, height)); |
46 | } |
47 | |
48 | static GdkPaintableFlags |
49 | solitaire_peg_get_flags (GdkPaintable *paintable) |
50 | { |
51 | /* The flags are very useful to let GTK know that this image |
52 | * is never going to change. |
53 | * This allows many optimizations and should therefore always |
54 | * be set. |
55 | */ |
56 | return GDK_PAINTABLE_STATIC_CONTENTS | GDK_PAINTABLE_STATIC_SIZE; |
57 | } |
58 | |
59 | static int |
60 | solitaire_peg_get_intrinsic_width (GdkPaintable *paintable) |
61 | { |
62 | return 32; |
63 | } |
64 | |
65 | static int |
66 | solitaire_peg_get_intrinsic_height (GdkPaintable *paintable) |
67 | { |
68 | return 32; |
69 | } |
70 | |
71 | static void |
72 | solitaire_peg_paintable_init (GdkPaintableInterface *iface) |
73 | { |
74 | iface->snapshot = solitaire_peg_snapshot; |
75 | iface->get_flags = solitaire_peg_get_flags; |
76 | iface->get_intrinsic_width = solitaire_peg_get_intrinsic_width; |
77 | iface->get_intrinsic_height = solitaire_peg_get_intrinsic_height; |
78 | } |
79 | |
80 | /* When defining the GType, we need to implement the GdkPaintable interface */ |
81 | G_DEFINE_TYPE_WITH_CODE (SolitairePeg, solitaire_peg, G_TYPE_OBJECT, |
82 | G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, |
83 | solitaire_peg_paintable_init)) |
84 | |
85 | /* Here's the boilerplate for the GObject declaration. |
86 | * We don't need to do anything special here, because we keep no |
87 | * data of our own. |
88 | */ |
89 | static void |
90 | solitaire_peg_class_init (SolitairePegClass *klass) |
91 | { |
92 | } |
93 | |
94 | static void |
95 | solitaire_peg_init (SolitairePeg *peg) |
96 | { |
97 | } |
98 | |
99 | /* Add a little setter for the peg's position. |
100 | * We want to track those so that we can check for legal moves |
101 | * during drag-and-drop operations. |
102 | */ |
103 | static void |
104 | solitaire_peg_set_position (SolitairePeg *peg, |
105 | guint x, |
106 | guint y) |
107 | { |
108 | peg->x = x; |
109 | peg->y = y; |
110 | } |
111 | |
112 | /* And finally, we add a simple constructor. |
113 | */ |
114 | static SolitairePeg * |
115 | solitaire_peg_new (void) |
116 | { |
117 | return g_object_new (SOLITAIRE_TYPE_PEG, NULL); |
118 | } |
119 | |
120 | /*** Helper for finding a win ***/ |
121 | |
122 | static void |
123 | ended (GObject *object) |
124 | { |
125 | g_object_unref (object); |
126 | } |
127 | |
128 | static void |
129 | celebrate (gboolean win) |
130 | { |
131 | char *path; |
132 | GtkMediaStream *stream; |
133 | |
134 | if (win) |
135 | path = g_build_filename (GTK_DATADIR, "sounds" , "freedesktop" , "stereo" , "complete.oga" , NULL); |
136 | else |
137 | path = g_build_filename (GTK_DATADIR, "sounds" , "freedesktop" , "stereo" , "dialog-error.oga" , NULL); |
138 | stream = gtk_media_file_new_for_filename (filename: path); |
139 | gtk_media_stream_set_volume (self: stream, volume: 1.0); |
140 | gtk_media_stream_play (self: stream); |
141 | |
142 | g_signal_connect (stream, "notify::ended" , G_CALLBACK (ended), NULL); |
143 | g_free (mem: path); |
144 | } |
145 | |
146 | static int |
147 | check_move (GtkGrid *grid, |
148 | int x, |
149 | int y, |
150 | int dx, |
151 | int dy) |
152 | { |
153 | GtkWidget *image; |
154 | /* We have a peg at x, y. |
155 | * Check if we can move the peg to x + 2*dx, y + 2*dy |
156 | */ |
157 | image = gtk_grid_get_child_at (grid, column: x + dx, row: y + dy); |
158 | if (!GTK_IS_IMAGE (image) || |
159 | !SOLITAIRE_IS_PEG (ptr: gtk_image_get_paintable (GTK_IMAGE (image)))) |
160 | return 0; |
161 | |
162 | image = gtk_grid_get_child_at (grid, column: x + 2*dx, row: y + 2*dy); |
163 | if (!GTK_IMAGE (image) || |
164 | SOLITAIRE_IS_PEG (ptr: gtk_image_get_paintable (GTK_IMAGE (image)))) |
165 | return 0; |
166 | |
167 | return 1; |
168 | } |
169 | |
170 | static void |
171 | check_for_end (GtkGrid *grid) |
172 | { |
173 | GtkWidget *image; |
174 | int x, y; |
175 | int pegs; |
176 | int moves; |
177 | |
178 | pegs = 0; |
179 | moves = 0; |
180 | for (x = 0; x < 7; x++) |
181 | { |
182 | for (y = 0; y < 7; y++) |
183 | { |
184 | image = gtk_grid_get_child_at (grid, column: x, row: y); |
185 | if (GTK_IS_IMAGE (image) && |
186 | SOLITAIRE_IS_PEG (ptr: gtk_image_get_paintable (GTK_IMAGE (image)))) |
187 | { |
188 | pegs++; |
189 | moves += check_move (grid, x, y, dx: 1, dy: 0); |
190 | moves += check_move (grid, x, y, dx: -1, dy: 0); |
191 | moves += check_move (grid, x, y, dx: 0, dy: 1); |
192 | moves += check_move (grid, x, y, dx: 0, dy: -1); |
193 | } |
194 | |
195 | if (pegs > 1 && moves > 0) |
196 | break; |
197 | } |
198 | } |
199 | |
200 | image = gtk_grid_get_child_at (grid, column: 3, row: 3); |
201 | if (pegs == 1 && |
202 | SOLITAIRE_IS_PEG (ptr: gtk_image_get_paintable (GTK_IMAGE (image)))) |
203 | celebrate (TRUE); |
204 | else if (moves == 0) |
205 | celebrate (FALSE); |
206 | } |
207 | |
208 | |
209 | /*** DRAG AND DROP ***/ |
210 | |
211 | /* The user tries to start a drag operation. |
212 | * We check if the image contains a peg, and if so, we return the |
213 | * peg as the content to be dragged. |
214 | */ |
215 | static GdkContentProvider * |
216 | drag_prepare (GtkDragSource *source, |
217 | double x, |
218 | double y, |
219 | GtkWidget *image) |
220 | { |
221 | GdkPaintable *paintable = gtk_image_get_paintable (GTK_IMAGE (image)); |
222 | |
223 | if (!SOLITAIRE_IS_PEG (ptr: paintable)) |
224 | return NULL; |
225 | |
226 | return gdk_content_provider_new_typed (SOLITAIRE_TYPE_PEG, paintable); |
227 | } |
228 | |
229 | /* This notifies us that the drag has begun. |
230 | * We can now set up the icon and the widget for the ongoing drag. |
231 | */ |
232 | static void |
233 | drag_begin (GtkDragSource *source, |
234 | GdkDrag *drag, |
235 | GtkWidget *image) |
236 | { |
237 | GdkPaintable *paintable = gtk_image_get_paintable (GTK_IMAGE (image)); |
238 | |
239 | /* We guaranteed in the drag_prepare function above that we |
240 | * only start a drag if a peg is available. |
241 | * So let's make sure we did not screw that up. |
242 | */ |
243 | g_assert (SOLITAIRE_IS_PEG (paintable)); |
244 | |
245 | /* We use the peg as the drag icon. |
246 | */ |
247 | gtk_drag_source_set_icon (source, paintable, hot_x: -2, hot_y: -2); |
248 | |
249 | /* We also attach it to the drag operation as custom user data, |
250 | * so that we can get it back later if the drag fails. |
251 | */ |
252 | g_object_set_data (G_OBJECT (drag), key: "the peg" , data: paintable); |
253 | |
254 | /* Because we are busy dragging the peg, we want to unset it |
255 | * on the image. |
256 | */ |
257 | gtk_image_clear (GTK_IMAGE (image)); |
258 | } |
259 | |
260 | /* This is called once a drag operation has ended (successfully or not). |
261 | * We want to undo what we did in drag_begin() above and react |
262 | * to a potential move of the peg. |
263 | */ |
264 | static void |
265 | drag_end (GtkDragSource *source, |
266 | GdkDrag *drag, |
267 | gboolean delete_data, |
268 | GtkWidget *image) |
269 | { |
270 | SolitairePeg *peg; |
271 | |
272 | /* If the drag was successful, we should now delete the peg. |
273 | * We did this in drag_begin() above to prepare for the drag, so |
274 | * there's no need to do anything anymore. |
275 | */ |
276 | if (delete_data) |
277 | return; |
278 | |
279 | /* However, if the drag did not succeed, we need to undo what |
280 | * we did in drag_begin() and reinsert the peg here. |
281 | * Because we used it as the drag data |
282 | */ |
283 | peg = g_object_get_data (G_OBJECT (drag), key: "the peg" ); |
284 | gtk_image_set_from_paintable (GTK_IMAGE (image), paintable: GDK_PAINTABLE (ptr: peg)); |
285 | } |
286 | |
287 | /* Whenever a new drop operation starts, we need to check if we can |
288 | * accept it. |
289 | * The default check unfortunately is not good enough, because it only |
290 | * checks the data type. But we also need to check if our image can |
291 | * even accept data. |
292 | */ |
293 | static gboolean |
294 | drop_accept (GtkDropTarget *target, |
295 | GdkDrop *drop, |
296 | GtkWidget *image) |
297 | { |
298 | /* First, check the drop is actually trying to drop a peg */ |
299 | if (!gdk_content_formats_contain_gtype (formats: gdk_drop_get_formats (self: drop), SOLITAIRE_TYPE_PEG)) |
300 | return FALSE; |
301 | |
302 | /* If the image already contains a peg, we cannot accept another one */ |
303 | if (SOLITAIRE_IS_PEG (ptr: gtk_image_get_paintable (GTK_IMAGE (image)))) |
304 | return FALSE; |
305 | |
306 | return TRUE; |
307 | } |
308 | |
309 | static gboolean |
310 | drop_drop (GtkDropTarget *target, |
311 | const GValue *value, |
312 | double x, |
313 | double y, |
314 | GtkWidget *image) |
315 | { |
316 | GtkGrid *grid; |
317 | SolitairePeg *peg; |
318 | int image_x, image_y; |
319 | GtkWidget *jumped; |
320 | |
321 | grid = GTK_GRID (gtk_widget_get_parent (image)); |
322 | /* The value contains the data in the type we demanded. |
323 | * We demanded a SolitairePeg, so that's what we get. |
324 | */ |
325 | peg = g_value_get_object (value); |
326 | |
327 | /* Make sure this was a legal move. */ |
328 | /* First, figure out the image's position in the grid. */ |
329 | gtk_grid_query_child (grid, |
330 | child: image, |
331 | column: &image_x, row: &image_y, |
332 | NULL, NULL); |
333 | |
334 | /* If the peg was not moved 2 spaces horizontally or vertically, |
335 | * this was not a valid jump. Reject it. |
336 | */ |
337 | if (!((ABS (image_x - peg->x) == 2 && image_y == peg->y) || |
338 | (ABS (image_y - peg->y) == 2 && image_x == peg->x))) |
339 | return FALSE; |
340 | |
341 | /* Get the widget that was jumped over |
342 | */ |
343 | jumped = gtk_grid_get_child_at (grid, |
344 | column: (image_x + peg->x) / 2, |
345 | row: (image_y + peg->y) / 2); |
346 | /* If the jumped widget does not have a peg in it, this move |
347 | * isn't valid. |
348 | */ |
349 | if (!SOLITAIRE_IS_PEG (ptr: gtk_image_get_paintable (GTK_IMAGE (jumped)))) |
350 | return FALSE; |
351 | |
352 | /* Finally, we know it's a legal move. */ |
353 | |
354 | /* Clear the peg of the jumped-over image */ |
355 | gtk_image_clear (GTK_IMAGE (jumped)); |
356 | |
357 | /* Add the peg to this image */ |
358 | solitaire_peg_set_position (peg, x: image_x, y: image_y); |
359 | gtk_image_set_from_paintable (GTK_IMAGE (image), paintable: GDK_PAINTABLE (ptr: peg)); |
360 | |
361 | /* Maybe we have something to celebrate */ |
362 | check_for_end (grid); |
363 | |
364 | /* Success! */ |
365 | return TRUE; |
366 | } |
367 | |
368 | static void |
369 | create_board (GtkWidget *window) |
370 | { |
371 | GtkWidget *grid; |
372 | GtkWidget *image; |
373 | int x, y; |
374 | GtkDragSource *source; |
375 | GtkDropTarget *target; |
376 | GtkCssProvider *provider; |
377 | const char css[] = |
378 | ".solitaire-field {" |
379 | " border: 1px solid lightgray;" |
380 | "}" ; |
381 | |
382 | provider = gtk_css_provider_new (); |
383 | gtk_css_provider_load_from_data (css_provider: provider, data: css, length: -1); |
384 | |
385 | grid = gtk_grid_new (); |
386 | gtk_widget_set_halign (widget: grid, align: GTK_ALIGN_CENTER); |
387 | gtk_widget_set_valign (widget: grid, align: GTK_ALIGN_CENTER); |
388 | gtk_grid_set_row_spacing (GTK_GRID (grid), spacing: 6); |
389 | gtk_grid_set_column_spacing (GTK_GRID (grid), spacing: 6); |
390 | gtk_grid_set_row_homogeneous (GTK_GRID (grid), TRUE); |
391 | gtk_grid_set_column_homogeneous (GTK_GRID (grid), TRUE); |
392 | gtk_window_set_child (GTK_WINDOW (window), child: grid); |
393 | |
394 | for (x = 0; x < 7; x++) |
395 | { |
396 | for (y = 0; y < 7; y++) |
397 | { |
398 | if ((x < 2 || x >= 5) && (y < 2 || y >= 5)) |
399 | continue; |
400 | |
401 | image = gtk_image_new (); |
402 | gtk_style_context_add_provider (context: gtk_widget_get_style_context (widget: image), |
403 | GTK_STYLE_PROVIDER (provider), |
404 | priority: 800); |
405 | gtk_widget_add_css_class (widget: image, css_class: "solitaire-field" ); |
406 | gtk_image_set_icon_size (GTK_IMAGE (image), icon_size: GTK_ICON_SIZE_LARGE); |
407 | if (x != 3 || y != 3) |
408 | { |
409 | SolitairePeg *peg = solitaire_peg_new (); |
410 | solitaire_peg_set_position (peg, x, y); |
411 | gtk_image_set_from_paintable (GTK_IMAGE (image), paintable: GDK_PAINTABLE (ptr: peg)); |
412 | } |
413 | |
414 | gtk_grid_attach (GTK_GRID (grid), child: image, column: x, row: y, width: 1, height: 1); |
415 | |
416 | /* Set up the drag source. |
417 | * This is rather straightforward: Set the supported actions |
418 | * (in our case, pegs can only be moved) and connect all the |
419 | * relevant signals. |
420 | * And because all drag'n'drop handling is done via event controllers, |
421 | * we need to add the controller to the widget. |
422 | */ |
423 | source = gtk_drag_source_new (); |
424 | gtk_drag_source_set_actions (source, actions: GDK_ACTION_MOVE); |
425 | g_signal_connect (source, "prepare" , G_CALLBACK (drag_prepare), image); |
426 | g_signal_connect (source, "drag-begin" , G_CALLBACK (drag_begin), image); |
427 | g_signal_connect (source, "drag-end" , G_CALLBACK (drag_end), image); |
428 | gtk_widget_add_controller (widget: image, GTK_EVENT_CONTROLLER (source)); |
429 | |
430 | /* Set up the drop target. |
431 | * This is more involved, because the game logic goes here. |
432 | */ |
433 | |
434 | /* First we specify the data we accept: pegs. |
435 | * And we only want moves. |
436 | */ |
437 | target = gtk_drop_target_new (SOLITAIRE_TYPE_PEG, actions: GDK_ACTION_MOVE); |
438 | /* Then we connect our signals. |
439 | */ |
440 | g_signal_connect (target, "accept" , G_CALLBACK (drop_accept), image); |
441 | g_signal_connect (target, "drop" , G_CALLBACK (drop_drop), image); |
442 | /* Finally, like above, we add it to the widget. |
443 | */ |
444 | gtk_widget_add_controller (widget: image, GTK_EVENT_CONTROLLER (target)); |
445 | } |
446 | } |
447 | |
448 | g_object_unref (object: provider); |
449 | } |
450 | |
451 | static void |
452 | restart_game (GtkButton *button, |
453 | GtkWidget *window) |
454 | { |
455 | create_board (window); |
456 | } |
457 | |
458 | GtkWidget * |
459 | do_peg_solitaire (GtkWidget *do_widget) |
460 | { |
461 | static GtkWidget *window = NULL; |
462 | |
463 | if (!window) |
464 | { |
465 | GtkWidget *; |
466 | GtkWidget *restart; |
467 | |
468 | window = gtk_window_new (); |
469 | |
470 | restart = gtk_button_new_from_icon_name (icon_name: "view-refresh-symbolic" ); |
471 | g_signal_connect (restart, "clicked" , G_CALLBACK (restart_game), window); |
472 | |
473 | header = gtk_header_bar_new (); |
474 | gtk_header_bar_pack_start (GTK_HEADER_BAR (header), child: restart); |
475 | gtk_window_set_display (GTK_WINDOW (window), |
476 | display: gtk_widget_get_display (widget: do_widget)); |
477 | gtk_window_set_title (GTK_WINDOW (window), title: "Peg Solitaire" ); |
478 | gtk_window_set_titlebar (GTK_WINDOW (window), titlebar: header); |
479 | gtk_window_set_default_size (GTK_WINDOW (window), width: 400, height: 300); |
480 | g_object_add_weak_pointer (G_OBJECT (window), weak_pointer_location: (gpointer *)&window); |
481 | |
482 | create_board (window); |
483 | } |
484 | |
485 | if (!gtk_widget_get_visible (widget: window)) |
486 | gtk_widget_show (widget: window); |
487 | else |
488 | gtk_window_destroy (GTK_WINDOW (window)); |
489 | |
490 | return window; |
491 | } |
492 | |