summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenjamin Otte <otte@redhat.com>2019-10-28 09:47:52 +0100
committerBenjamin Otte <otte@redhat.com>2019-10-28 10:57:27 +0100
commit10911bfaf0e568367cb6d8a96df46a5d28c349d6 (patch)
tree58e86d421a7fd1f5004ff89c316f4f65f2e32692
parentb36a58fa97ae79a87226bfec02146018ac76ef4a (diff)
downloadgtk+-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.xml4
-rw-r--r--demos/gtk-demo/listview_minesweeper.c478
-rw-r--r--demos/gtk-demo/listview_minesweeper.ui42
-rw-r--r--demos/gtk-demo/listview_minesweeper_cell.ui12
-rw-r--r--demos/gtk-demo/meson.build1
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',