diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml index ede5926fb3..c8bdde102f 100644 --- a/demos/gtk-demo/demo.gresource.xml +++ b/demos/gtk-demo/demo.gresource.xml @@ -128,6 +128,10 @@ listview_filebrowser.ui + + listview_minesweeper.ui + listview_minesweeper_cell.ui + listview_settings.ui @@ -212,6 +216,7 @@ links.c listbox.c listview_filebrowser.c + listview_minesweeper.c listview_settings.c listview_weather.c list_store.c diff --git a/demos/gtk-demo/listview_minesweeper.c b/demos/gtk-demo/listview_minesweeper.c new file mode 100644 index 0000000000..e43eaceb0b --- /dev/null +++ b/demos/gtk-demo/listview_minesweeper.c @@ -0,0 +1,472 @@ +/* Lists/Minesweeper + * + * This demo shows how to develop a user interface for small game using a + * gridview. + * + * It demonstrates how to use the activate signal and single-press behavior + * to implement rather different interaction behavior to a typical list. + */ + +#include +#include + +/*** The cell object ***/ + +/* Create an object that holds the data for a cell in the game */ +typedef struct _SweeperCell SweeperCell; +struct _SweeperCell +{ + GObject parent_instance; + + gboolean is_mine; + gboolean is_visible; + guint neighbor_mines; +}; + +enum { + CELL_PROP_0, + CELL_PROP_LABEL, + + N_CELL_PROPS +}; + +#define SWEEPER_TYPE_CELL (sweeper_cell_get_type ()) +G_DECLARE_FINAL_TYPE (SweeperCell, sweeper_cell, SWEEPER, CELL, GObject); + +G_DEFINE_TYPE (SweeperCell, sweeper_cell, G_TYPE_OBJECT); +static GParamSpec *cell_properties[N_CELL_PROPS] = { NULL, }; + +static const char * +sweeper_cell_get_label (SweeperCell *self) +{ + static const char *minecount_labels[10] = { "", "1", "2", "3", "4", "5", "6", "7", "8", "9" }; + + if (!self->is_visible) + return "?"; + + if (self->is_mine) + return "💣"; + + return minecount_labels[self->neighbor_mines]; +} + +static void +sweeper_cell_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + SweeperCell *self = SWEEPER_CELL (object); + + switch (property_id) + { + case CELL_PROP_LABEL: + g_value_set_string (value, sweeper_cell_get_label (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +sweeper_cell_class_init (SweeperCellClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = sweeper_cell_get_property; + + cell_properties[CELL_PROP_LABEL] = + g_param_spec_string ("label", + "label", + "label to display for this row", + NULL, + G_PARAM_READABLE); + + g_object_class_install_properties (gobject_class, N_CELL_PROPS, cell_properties); +} + +static void +sweeper_cell_init (SweeperCell *self) +{ +} + +static void +sweeper_cell_reveal (SweeperCell *self) +{ + if (self->is_visible) + return; + + self->is_visible = TRUE; + + g_object_notify_by_pspec (G_OBJECT (self), cell_properties[CELL_PROP_LABEL]); +} + +static SweeperCell * +sweeper_cell_new (void) +{ + return g_object_new (SWEEPER_TYPE_CELL, NULL); +} + +/*** The board object ***/ + +/* Create an object that holds the data for the game */ +typedef struct _SweeperGame SweeperGame; +struct _SweeperGame +{ + GObject parent_instance; + + GPtrArray *cells; + guint width; + guint height; + gboolean playing; + gboolean win; +}; + +enum { + GAME_PROP_0, + GAME_PROP_HEIGHT, + GAME_PROP_PLAYING, + GAME_PROP_WIDTH, + GAME_PROP_WIN, + + N_GAME_PROPS +}; + +#define SWEEPER_TYPE_GAME (sweeper_game_get_type ()) +G_DECLARE_FINAL_TYPE (SweeperGame, sweeper_game, SWEEPER, GAME, GObject); + +static GType +sweeper_game_list_model_get_item_type (GListModel *model) +{ + return SWEEPER_TYPE_GAME; +} + +static guint +sweeper_game_list_model_get_n_items (GListModel *model) +{ + SweeperGame *self = SWEEPER_GAME (model); + + return self->width * self->height; +} + +static gpointer +sweeper_game_list_model_get_item (GListModel *model, + guint position) +{ + SweeperGame *self = SWEEPER_GAME (model); + + return g_object_ref (g_ptr_array_index (self->cells, position)); +} + +static void +sweeper_game_list_model_init (GListModelInterface *iface) +{ + iface->get_item_type = sweeper_game_list_model_get_item_type; + iface->get_n_items = sweeper_game_list_model_get_n_items; + iface->get_item = sweeper_game_list_model_get_item; +} + +G_DEFINE_TYPE_WITH_CODE (SweeperGame, sweeper_game, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, sweeper_game_list_model_init)) + +static GParamSpec *game_properties[N_GAME_PROPS] = { NULL, }; + +static void +sweeper_game_dispose (GObject *object) +{ + SweeperGame *self = SWEEPER_GAME (object); + + g_clear_pointer (&self->cells, g_ptr_array_unref); + + G_OBJECT_CLASS (sweeper_game_parent_class)->dispose (object); +} + +static void +sweeper_game_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + SweeperGame *self = SWEEPER_GAME (object); + + switch (property_id) + { + case GAME_PROP_HEIGHT: + g_value_set_uint (value, self->height); + break; + + case GAME_PROP_PLAYING: + g_value_set_boolean (value, self->playing); + break; + + case GAME_PROP_WIDTH: + g_value_set_uint (value, self->width); + break; + + case GAME_PROP_WIN: + g_value_set_boolean (value, self->win); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +sweeper_game_class_init (SweeperGameClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->dispose = sweeper_game_dispose; + gobject_class->get_property = sweeper_game_get_property; + + game_properties[GAME_PROP_HEIGHT] = + g_param_spec_uint ("height", + "height", + "height of the game grid", + 1, G_MAXUINT, 8, + G_PARAM_READABLE); + + game_properties[GAME_PROP_PLAYING] = + g_param_spec_boolean ("playing", + "playing", + "if the game is still going on", + FALSE, + G_PARAM_READABLE); + + game_properties[GAME_PROP_WIDTH] = + g_param_spec_uint ("width", + "width", + "width of the game grid", + 1, G_MAXUINT, 8, + G_PARAM_READABLE); + + game_properties[GAME_PROP_WIN] = + g_param_spec_boolean ("win", + "win", + "if the game was won", + FALSE, + G_PARAM_READABLE); + + g_object_class_install_properties (gobject_class, N_GAME_PROPS, game_properties); +} + +static void +sweeper_game_reset_board (SweeperGame *self, + guint width, + guint height) +{ + guint i; + + g_ptr_array_set_size (self->cells, 0); + + for (i = 0; i < width * height; i++) + { + g_ptr_array_add (self->cells, sweeper_cell_new ()); + } + + if (self->width != width) + { + self->width = width; + g_object_notify_by_pspec (G_OBJECT (self), game_properties[GAME_PROP_WIDTH]); + } + if (self->height != height) + { + self->height = height; + g_object_notify_by_pspec (G_OBJECT (self), game_properties[GAME_PROP_HEIGHT]); + } + if (!self->playing) + { + self->playing = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), game_properties[GAME_PROP_PLAYING]); + } + if (self->win) + { + self->win = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), game_properties[GAME_PROP_WIN]); + } +} + +static void +sweeper_game_place_mines (SweeperGame *self, + guint n_mines) +{ + guint i; + + for (i = 0; i < n_mines; i++) + { + SweeperCell *cell; + + do { + cell = g_ptr_array_index (self->cells, g_random_int_range (0, self->cells->len)); + } while (cell->is_mine); + + cell->is_mine = TRUE; + } +} + +static SweeperCell * +get_cell (SweeperGame *self, + guint x, + guint y) +{ + return g_ptr_array_index (self->cells, y * self->width + x); +} + +static void +sweeper_game_count_neighbor_mines (SweeperGame *self, + guint width, + guint height) +{ + guint x, y, x2, y2; + + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + SweeperCell *cell = get_cell (self, x, y); + + for (y2 = MAX (1, y) - 1; y2 < MIN (height, y + 2); y2++) + { + for (x2 = MAX (1, x) - 1; x2 < MIN (width, x + 2); x2++) + { + SweeperCell *other = get_cell (self, x2, y2); + + if (other->is_mine) + cell->neighbor_mines++; + } + } + } + } +} + +static void +sweeper_game_new_game (SweeperGame *self, + guint width, + guint height, + guint n_mines) +{ + guint n_items_before; + + g_return_if_fail (n_mines <= width * height); + + n_items_before = self->width * self->height; + + g_object_freeze_notify (G_OBJECT (self)); + + sweeper_game_reset_board (self, width, height); + sweeper_game_place_mines (self, n_mines); + sweeper_game_count_neighbor_mines (self, width, height); + + g_list_model_items_changed (G_LIST_MODEL (self), 0, n_items_before, width * height); + + g_object_thaw_notify (G_OBJECT (self)); +} + +static void +sweeper_game_init (SweeperGame *self) +{ + self->cells = g_ptr_array_new_with_free_func (g_object_unref); + + sweeper_game_new_game (self, 8, 8, 10); +} + +static void +sweeper_game_end (SweeperGame *self, + gboolean win) +{ + if (self->playing) + { + self->playing = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), game_properties[GAME_PROP_PLAYING]); + } + if (self->win != win) + { + self->win = win; + g_object_notify_by_pspec (G_OBJECT (self), game_properties[GAME_PROP_WIN]); + } +} + +static void +sweeper_game_check_finished (SweeperGame *self) +{ + guint i; + + if (!self->playing) + return; + + for (i = 0; i < self->cells->len; i++) + { + SweeperCell *cell = g_ptr_array_index (self->cells, i); + + /* There's still a non-revealed cell that isn't a mine */ + if (!cell->is_visible && !cell->is_mine) + return; + } + + sweeper_game_end (self, TRUE); +} + +static void +sweeper_game_reveal_cell (SweeperGame *self, + guint position) +{ + SweeperCell *cell; + + if (!self->playing) + return; + + cell = g_ptr_array_index (self->cells, position); + sweeper_cell_reveal (cell); + + if (cell->is_mine) + sweeper_game_end (self, FALSE); + + sweeper_game_check_finished (self); +} + +void +minesweeper_cell_clicked_cb (GtkGridView *gridview, + guint pos, + SweeperGame *game) +{ + sweeper_game_reveal_cell (game, pos); +} + +void +minesweeper_new_game_cb (GtkButton *button, + SweeperGame *game) +{ + sweeper_game_new_game (game, 8, 8, 10); +} + +static GtkWidget *window = NULL; + +GtkWidget * +do_listview_minesweeper (GtkWidget *do_widget) +{ + if (window == NULL) + { + GtkBuilder *builder; + + g_type_ensure (SWEEPER_TYPE_GAME); + + builder = gtk_builder_new_from_resource ("/listview_minesweeper/listview_minesweeper.ui"); + window = GTK_WIDGET (gtk_builder_get_object (builder, "window")); + gtk_window_set_display (GTK_WINDOW (window), + gtk_widget_get_display (do_widget)); + g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); + + g_object_unref (builder); + } + + if (!gtk_widget_get_visible (window)) + gtk_widget_show (window); + else + gtk_window_destroy (GTK_WINDOW (window)); + + return window; +} diff --git a/demos/gtk-demo/listview_minesweeper.ui b/demos/gtk-demo/listview_minesweeper.ui new file mode 100644 index 0000000000..b67fc4148f --- /dev/null +++ b/demos/gtk-demo/listview_minesweeper.ui @@ -0,0 +1,48 @@ + + + + + + Minesweeper + + + 1 + + + New Game + + + + + + trophy-gold + + game + + + + + + + + + + game + + + + game + + + game + + + + /listview_minesweeper/listview_minesweeper_cell.ui + + + + + + + diff --git a/demos/gtk-demo/listview_minesweeper_cell.ui b/demos/gtk-demo/listview_minesweeper_cell.ui new file mode 100644 index 0000000000..142705d40f --- /dev/null +++ b/demos/gtk-demo/listview_minesweeper_cell.ui @@ -0,0 +1,16 @@ + + + + diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build index e4aac15e41..0fe18ebbbe 100644 --- a/demos/gtk-demo/meson.build +++ b/demos/gtk-demo/meson.build @@ -43,6 +43,7 @@ demos = files([ 'flowbox.c', 'list_store.c', 'listview_filebrowser.c', + 'listview_minesweeper.c', 'listview_settings.c', 'listview_weather.c', 'markup.c',