diff options
author | Benjamin Otte <otte@redhat.com> | 2019-10-28 09:47:52 +0100 |
---|---|---|
committer | Benjamin Otte <otte@redhat.com> | 2019-10-28 10:57:27 +0100 |
commit | 10911bfaf0e568367cb6d8a96df46a5d28c349d6 (patch) | |
tree | 58e86d421a7fd1f5004ff89c316f4f65f2e32692 | |
parent | b36a58fa97ae79a87226bfec02146018ac76ef4a (diff) | |
download | gtk+-10911bfaf0e568367cb6d8a96df46a5d28c349d6.tar.gz |
gtk-demo: Add a minesweeper demo
The demo shows creating ones own listmodel and using it to fill a grid.
I am totally getting the hang of React btw:
500 lines of logic with no UI code and 100 lines of GtkBuilder XML and
I get a sweet UI.
-rw-r--r-- | demos/gtk-demo/demo.gresource.xml | 4 | ||||
-rw-r--r-- | demos/gtk-demo/listview_minesweeper.c | 478 | ||||
-rw-r--r-- | demos/gtk-demo/listview_minesweeper.ui | 42 | ||||
-rw-r--r-- | demos/gtk-demo/listview_minesweeper_cell.ui | 12 | ||||
-rw-r--r-- | demos/gtk-demo/meson.build | 1 |
5 files changed, 537 insertions, 0 deletions
diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml index 3781f72d3a..9658cb59fc 100644 --- a/demos/gtk-demo/demo.gresource.xml +++ b/demos/gtk-demo/demo.gresource.xml @@ -121,6 +121,10 @@ <gresource prefix="/listview_filebrowser"> <file>listview_filebrowser.ui</file> </gresource> + <gresource prefix="/listview_minesweeper"> + <file>listview_minesweeper.ui</file> + <file>listview_minesweeper_cell.ui</file> + </gresource> <gresource prefix="/listview_weather"> <file compressed="true">listview_weather.txt</file> </gresource> diff --git a/demos/gtk-demo/listview_minesweeper.c b/demos/gtk-demo/listview_minesweeper.c new file mode 100644 index 0000000000..3c0fdd64c2 --- /dev/null +++ b/demos/gtk-demo/listview_minesweeper.c @@ -0,0 +1,478 @@ +/* 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 <glib/gi18n.h> +#include <gtk/gtk.h> + +/*** 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 () +{ + 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); +} + +static void +cell_clicked_cb (GtkGridView *gridview, + guint pos, + SweeperGame *game) +{ + sweeper_game_reveal_cell (game, pos); +} + +static void +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"); + gtk_builder_add_callback_symbols (builder, + "cell_clicked_cb", G_CALLBACK (cell_clicked_cb), + "new_game_cb", G_CALLBACK (new_game_cb), + NULL); + gtk_builder_connect_signals (builder, NULL); + window = GTK_WIDGET (gtk_builder_get_object (builder, "window")); + gtk_window_set_display (GTK_WINDOW (window), + gtk_widget_get_display (do_widget)); + g_signal_connect (window, "destroy", + G_CALLBACK (gtk_widget_destroyed), &window); + + g_object_unref (builder); + } + + if (!gtk_widget_get_visible (window)) + gtk_widget_show (window); + else + gtk_widget_destroy (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..cba0dcb1fe --- /dev/null +++ b/demos/gtk-demo/listview_minesweeper.ui @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <object class="SweeperGame" id="game"> + </object> + <object class="GtkWindow" id="window"> + <property name="title" translatable="yes">Minesweeper</property> + <child type="titlebar"> + <object class="GtkHeaderBar" id=""> + <property name="show-title-buttons">1</property> + <child> + <object class="GtkButton"> + <property name="label">New Game</property> + <signal name="clicked" handler="new_game_cb" object="game" swapped="no"/> + </object> + </child> + <child type="title"> + <object class="GtkImage"> + <property name="icon-name">trophy-gold</property> + <binding name="visible">game.win</binding> + </object> + </child> + </object> + </child> + <child> + <object class="GtkGridView" id="view"> + <property name="model"> + <object class="GtkNoSelection"> + <property name="model">game</property> + </object> + </property> + <binding name="max-columns">game.width</binding> + <binding name="min-columns">game.width</binding> + <property name="factory"> + <object class="GtkBuilderListItemFactory"> + <property name="resource">/listview_minesweeper/listview_minesweeper_cell.ui</property> + </object> + </property> + <signal name="activate" handler="cell_clicked_cb" object="game" swapped="no"/> + </object> + </child> + </object> +</interface> diff --git a/demos/gtk-demo/listview_minesweeper_cell.ui b/demos/gtk-demo/listview_minesweeper_cell.ui new file mode 100644 index 0000000000..51a853087a --- /dev/null +++ b/demos/gtk-demo/listview_minesweeper_cell.ui @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GtkListItem"> + <child> + <object class="GtkLabel"> + <property name="halign">center</property> + <property name="valign">center</property> + <binding name="label">GtkListItem.item:SweeperCell.label</binding> + </object> + </child> + </template> +</interface> diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build index 410d26d202..eaab1d0ad2 100644 --- a/demos/gtk-demo/meson.build +++ b/demos/gtk-demo/meson.build @@ -45,6 +45,7 @@ demos = files([ 'flowbox.c', 'list_store.c', 'listview_filebrowser.c', + 'listview_minesweeper.c', 'listview_settings.c', 'listview_weather.c', 'markup.c', |