[gtk/wip/otte/listview: 141/169] gtk-demo: Add a minesweeper demo



commit 1033ebe71634c33edbca3ba2c6100a3ae086661f
Author: Benjamin Otte <otte redhat com>
Date:   Mon Oct 28 09:47:52 2019 +0100

    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.

 demos/gtk-demo/demo.gresource.xml           |   5 +
 demos/gtk-demo/listview_minesweeper.c       | 473 ++++++++++++++++++++++++++++
 demos/gtk-demo/listview_minesweeper.ui      |  48 +++
 demos/gtk-demo/listview_minesweeper_cell.ui |  16 +
 demos/gtk-demo/meson.build                  |   1 +
 5 files changed, 543 insertions(+)
---
diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml
index c6ecbf2fa0..9cd5057a2a 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_settings">
     <file>listview_settings.ui</file>
   </gresource>
@@ -207,6 +211,7 @@
     <file>links.c</file>
     <file>listbox.c</file>
     <file>listview_filebrowser.c</file>
+    <file>listview_minesweeper.c</file>
     <file>listview_settings.c</file>
     <file>listview_weather.c</file>
     <file>list_store.c</file>
diff --git a/demos/gtk-demo/listview_minesweeper.c b/demos/gtk-demo/listview_minesweeper.c
new file mode 100644
index 0000000000..67fc94a69e
--- /dev/null
+++ b/demos/gtk-demo/listview_minesweeper.c
@@ -0,0 +1,473 @@
+/* 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);
+}
+
+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_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..b67fc4148f
--- /dev/null
+++ b/demos/gtk-demo/listview_minesweeper.ui
@@ -0,0 +1,48 @@
+<?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="minesweeper_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">
+              <lookup name="win">game</lookup>
+            </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">
+          <lookup name="width">game</lookup>
+        </binding>
+        <binding name="min-columns">
+          <lookup name="width">game</lookup>
+        </binding>
+        <property name="factory">
+          <object class="GtkBuilderListItemFactory">
+            <property name="resource">/listview_minesweeper/listview_minesweeper_cell.ui</property>
+          </object>
+        </property>
+        <signal name="activate" handler="minesweeper_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..142705d40f
--- /dev/null
+++ b/demos/gtk-demo/listview_minesweeper_cell.ui
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkLabel">
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <binding name="label">
+          <lookup name="label" type="SweeperCell">
+            <lookup name="item">GtkListItem</lookup>
+          </lookup>
+        </binding>
+      </object>
+    </property>
+  </template>
+</interface>
diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build
index 22de1e1d08..9b926209e4 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',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]