/* 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 /* 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; }