[gnome-2048] Introduce GameWindow.



commit 307de90aef1f5a4a38d98b25c18ded2530f56578
Author: Arnaud Bonatti <arnaud bonatti gmail com>
Date:   Thu Feb 7 15:55:08 2019 +0100

    Introduce GameWindow.

 data/mainwindow.ui                           |  17 +-
 meson.build                                  |  14 +-
 po/POTFILES.in                               |   2 +-
 po/POTFILES.skip                             |   2 +-
 src/application.vala                         | 601 +-------------------------
 src/game-window.vala                         | 604 +++++++++++++++++++++++++++
 src/grid.vala                                |   2 +-
 src/meson.build                              |  16 +-
 src/org.gnome.TwentyFortyEight.gresource.xml |   6 +-
 9 files changed, 646 insertions(+), 618 deletions(-)
---
diff --git a/data/mainwindow.ui b/data/mainwindow.ui
index 0ac51fb..4dfe4c8 100644
--- a/data/mainwindow.ui
+++ b/data/mainwindow.ui
@@ -17,22 +17,25 @@
 -->
 <interface>
   <requires lib="gtk+" version="3.12"/>
-  <object class="GtkApplicationWindow" id="applicationwindow">
+  <template class="GameWindow" parent="GtkApplicationWindow">
     <property name="can-focus">False</property>
     <property name="window-position">center</property>
     <property name="default-width">600</property>
     <property name="default-height">600</property>
     <property name="icon-name">org.gnome.TwentyFortyEight</property>
     <property name="show-menubar">False</property>
+    <signal name="key-press-event"      handler="key_press_event_cb"/>
+    <signal name="size-allocate"        handler="size_allocate_cb"/>
+    <signal name="window-state-event"   handler="state_event_cb"/>
     <child type="titlebar">
-      <object class="GtkHeaderBar" id="headerbar">
+      <object class="GtkHeaderBar" id="_header_bar">
         <property name="visible">True</property>
         <property name="can-focus">False</property>
         <!-- Translators: title of the window, displayed in the headerbar -->
         <property name="title" translatable="yes">GNOME 2048</property>
         <property name="show-close-button">True</property>
         <child>
-          <object class="GtkMenuButton" id="new-game-button">
+          <object class="GtkMenuButton" id="_new_game_button">
             <!-- Translators: button in the headerbar (with a mnemonic that appears pressing Alt) -->
             <property name="label" translatable="yes">_New Game</property>
             <property name="visible">True</property>
@@ -44,7 +47,7 @@
           </object>
         </child>
         <child>
-          <object class="GtkMenuButton" id="hamburger-button">
+          <object class="GtkMenuButton" id="_hamburger_button">
             <property name="visible">True</property>
             <property name="halign">end</property>
             <property name="valign">center</property>
@@ -65,7 +68,7 @@
           </packing>
         </child>
         <child>
-          <object class="GtkLabel" id="score">
+          <object class="GtkLabel" id="_score">
             <property name="visible">True</property>
             <property name="can-focus">False</property>
             <property name="label">0</property>
@@ -81,7 +84,7 @@
         <property name="visible">True</property>
         <property name="can-focus">False</property>
         <child>
-          <object class="GtkAspectFrame" id="aspectframe">
+          <object class="GtkAspectFrame" id="_frame">
             <property name="visible">True</property>
             <property name="can-focus">False</property>
             <property name="label-xalign">0</property>
@@ -94,5 +97,5 @@
         </child>
       </object>
     </child>
-  </object>
+  </template>
 </interface>
diff --git a/meson.build b/meson.build
index 7f083f0..81b3f82 100644
--- a/meson.build
+++ b/meson.build
@@ -24,13 +24,13 @@ schemadir = join_paths(datadir, 'glib-2.0', 'schemas')
 podir = join_paths(meson.current_source_dir(), 'po')
 
 # Dependencies
-posix = valac.find_library('posix')
-libm = cc.find_library('m', required: false) # some platforms do not have libm separated from libc
-gtk = dependency('gtk+-3.0', version: '>= 3.12.0')
-clutter = dependency('clutter-1.0', version: '>= 1.12.0')
-clutter_gtk = dependency('clutter-gtk-1.0', version: '>= 1.6.0')
-gee = dependency('gee-0.8', version: '>= 0.14.0')
-libgnome_games_support = dependency('libgnome-games-support-1')
+posix_dependency = valac.find_library('posix')
+libm_dependency = cc.find_library('m', required: false) # some platforms do not have libm separated from libc
+gtk_dependency = dependency('gtk+-3.0', version: '>= 3.12.0')
+clutter_dependency = dependency('clutter-1.0', version: '>= 1.12.0')
+clutter_gtk_dependency = dependency('clutter-gtk-1.0', version: '>= 1.6.0')
+gee_dependency = dependency('gee-0.8', version: '>= 0.14.0')
+libgnome_games_support_dependency = dependency('libgnome-games-support-1')
 
 appstream_util = find_program('appstream-util', required: false)
 desktop_file_validate = find_program('desktop-file-validate', required: false)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 2aac312..b4aa7de 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -6,4 +6,4 @@ data/mainwindow.ui
 data/org.gnome.TwentyFortyEight.appdata.xml.in
 data/org.gnome.TwentyFortyEight.desktop.in
 data/org.gnome.TwentyFortyEight.gschema.xml
-src/application.vala
+src/game-window.vala
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 7155990..a2a2067 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -1 +1 @@
-src/application.c
+src/game-window.c
diff --git a/src/application.vala b/src/application.vala
index 9662ec2..4c3507d 100644
--- a/src/application.vala
+++ b/src/application.vala
@@ -17,51 +17,16 @@
  * along with GNOME 2048; if not, see <http://www.gnu.org/licenses/>.
  */
 
-using Games;
 using Gtk;
 
 private class Application : Gtk.Application
 {
-    /* settings */
-    private GLib.Settings _settings;
-
-    private int _window_width;
-    private int _window_height;
-    private bool _window_maximized;
-    private bool _window_is_tiled;
-
-    private int WINDOW_MINIMUM_SIZE_HEIGHT = 600;
-    private int WINDOW_MINIMUM_SIZE_WIDTH = 600;
-
-    /* private widgets */
-    private Window _window;
-    private HeaderBar _header_bar;
-    private Label _score;
-    private MenuButton _new_game_button;
-    private MenuButton _hamburger_button;
-    private GtkClutter.Embed _embed;
-
-    private bool _game_restored;
-    private bool _game_should_init = true;
-
-    private Game _game;
+    private GameWindow _window;
 
     /* actions */
-    private const GLib.ActionEntry[] action_entries =
+    private const GLib.ActionEntry [] action_entries =
     {
-        { "undo",               undo_cb                     },
-
-        { "new-game",           new_game_cb                 },
-        { "toggle-new-game",    toggle_new_game_cb          },
-        { "new-game-sized",     new_game_sized_cb, "(ii)"   },
-
-        { "quit",               quit_cb                     },
-
-        // hamburger-menu
-        { "toggle-hamburger",   toggle_hamburger_menu       },
-
-        { "scores",             scores_cb                   },
-        { "about",              about_cb                    },
+        { "quit", quit_cb }
     };
 
     private static int main (string [] args)
@@ -116,35 +81,21 @@ private class Application : Gtk.Application
 
         add_action_entries (action_entries, this);
 
-        _settings = new GLib.Settings ("org.gnome.TwentyFortyEight");
-
-        _init_game ();
-
-        _create_window ();
-
-        _create_scores_dialog ();   // the library forbids to delay the dialog creation
+        _window = new GameWindow ();
+        add_window (_window);
 
-        set_accels_for_action ("app.toggle-new-game",   {        "<Primary>n"       });
-        set_accels_for_action ("app.new-game",          { "<Shift><Primary>n"       });
+        set_accels_for_action ("ui.toggle-new-game",    {        "<Primary>n"       });
+        set_accels_for_action ("ui.new-game",           { "<Shift><Primary>n"       });
         set_accels_for_action ("app.quit",              {        "<Primary>q"       });
-        set_accels_for_action ("app.undo",              {        "<Primary>z"       });
-        set_accels_for_action ("app.about",             {          "<Shift>F1",
+        set_accels_for_action ("ui.undo",               {        "<Primary>z"       });
+        set_accels_for_action ("ui.about",              {          "<Shift>F1",
                                                           "<Shift><Primary>F1"      }); // as usual, this 
second shortcut does not work
         set_accels_for_action ("win.show-help-overlay", {                 "F1",
                                                                  "<Primary>F1",
                                                                  "<Primary>question",
                                                           "<Shift><Primary>question"});
-        set_accels_for_action ("app.toggle-hamburger",  {                 "F10",
+        set_accels_for_action ("ui.toggle-hamburger",   {                 "F10",
                                                                           "Menu"    });
-
-        _window.notify ["has-toplevel-focus"].connect (() => _embed.grab_focus ());
-        _window.show_all ();
-        _init_gesture ();
-
-        _game_restored = _game.restore_game (ref _settings);
-        if (!_game_restored)
-            new_game_cb ();
-        _game_should_init = false;
     }
 
     protected override void activate ()
@@ -154,542 +105,12 @@ private class Application : Gtk.Application
 
     protected override void shutdown ()
     {
+        _window.before_shutdown ();
         base.shutdown ();
-
-        _game.save_game ();
-
-        _settings.delay ();
-        _settings.set_int ("window-width", _window_width);
-        _settings.set_int ("window-height", _window_height);
-        _settings.set_boolean ("window-maximized", _window_maximized);
-        _settings.apply ();
-    }
-
-    private void _init_game ()
-    {
-        _game = new Game (ref _settings);
-        _game.notify ["score"].connect ((s, p) => {
-                _score.label = _game.score.to_string ();
-            });
-        _game.finished.connect ((s) => {
-                _header_bar.set_has_subtitle (true);
-                /* Translators: subtitle of the headerbar, when the user cannot move anymore */
-                _header_bar.subtitle = _("Game Over");
-
-                if (!_game_restored)
-                    _show_best_scores ();
-
-                debug ("finished");
-            });
-        _game.target_value_reached.connect (target_value_reached_cb);
-        _game.undo_enabled.connect ((s) => {
-                ((SimpleAction) lookup_action ("undo")).set_enabled (true);
-            });
-        _game.undo_disabled.connect ((s) => {
-                ((SimpleAction) lookup_action ("undo")).set_enabled (false);
-            });
-    }
-
-    private void _create_window ()
-    {
-        Builder builder = new Builder.from_resource ("/org/gnome/TwentyFortyEight/ui/mainwindow.ui");
-
-        _window = (ApplicationWindow) builder.get_object ("applicationwindow");
-        _window.set_default_size (_settings.get_int ("window-width"), _settings.get_int ("window-height"));
-        if (_settings.get_boolean ("window-maximized"))
-            _window.maximize ();
-
-        add_window (_window);
-
-        _create_header_bar (builder, out _header_bar, out _score, out _new_game_button, out 
_hamburger_button);
-        _connect_and_update ();
-        _create_game_view (builder, out _embed, ref _game);
-
-        _window.set_events (_window.get_events () | Gdk.EventMask.STRUCTURE_MASK | 
Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK);
-        _window.key_press_event.connect (key_press_event_cb);
-        _window.size_allocate.connect (window_size_allocate_cb);
-        _window.window_state_event.connect (window_state_event_cb);
-
-        Gdk.Geometry geom = Gdk.Geometry ();
-        geom.min_height = WINDOW_MINIMUM_SIZE_HEIGHT;
-        geom.min_width = WINDOW_MINIMUM_SIZE_WIDTH;
-        _window.set_geometry_hints (_window, geom, Gdk.WindowHints.MIN_SIZE);
-    }
-
-    private static inline void _create_header_bar (Builder     builder,
-                                               out HeaderBar   header_bar,
-                                               out Label       score,
-                                               out MenuButton  new_game_button,
-                                               out MenuButton  hamburger_button)
-    {
-        header_bar          = (HeaderBar)   builder.get_object ("headerbar");
-        score               = (Label)       builder.get_object ("score");
-        new_game_button     = (MenuButton)  builder.get_object ("new-game-button");
-        hamburger_button    = (MenuButton)  builder.get_object ("hamburger-button");
-    }
-
-    private inline void _connect_and_update ()
-    {
-        ((SimpleAction) lookup_action ("undo")).set_enabled (false);
-
-        _hamburger_button.notify ["active"].connect (() => {
-                if (!_hamburger_button.active)
-                    _embed.grab_focus ();
-            });
-        _settings.changed.connect ((settings, key_name) => {
-                switch (key_name)
-                {
-                    case "cols":
-                    case "rows":
-                        _update_new_game_menu ();
-                        return;
-                    case "allow-undo":
-                        _update_hamburger_menu ();
-                        _game.load_settings (ref _settings);
-                        return;
-                    case "allow-undo-max":
-                    case "animations-speed":
-                        _game.load_settings (ref _settings);
-                        return;
-                }
-            });
-        _update_new_game_menu ();
-        _update_hamburger_menu ();
-        _game.load_settings (ref _settings);
-    }
-
-    private static inline void _create_game_view (Builder          builder,
-                                              out GtkClutter.Embed embed,
-                                              ref Game             game)
-    {
-        embed = new GtkClutter.Embed ();
-        AspectFrame _frame = (AspectFrame) builder.get_object ("aspectframe");
-        _frame.add (embed);
-        game.view = embed.get_stage ();
-    }
-
-    /*\
-    * * hamburger menu (and undo action) callbacks
-    \*/
-
-    private void _update_hamburger_menu ()
-    {
-        GLib.Menu menu = new GLib.Menu ();
-
-        if (_settings.get_boolean ("allow-undo"))
-            _append_undo_section (ref menu);
-        _append_scores_section (ref menu);
-        _append_app_actions_section (ref menu);
-
-        menu.freeze ();
-        _hamburger_button.set_menu_model ((MenuModel) menu);
-    }
-
-    private static inline void _append_undo_section (ref GLib.Menu menu)
-    {
-        GLib.Menu section = new GLib.Menu ();
-
-        /* Translators: entry in the hamburger menu, if the "Allow undo" option is set to true */
-        section.append (_("Undo"), "app.undo");
-
-        section.freeze ();
-        menu.append_section (null, section);
-    }
-
-    private static inline void _append_scores_section (ref GLib.Menu menu)
-    {
-        GLib.Menu section = new GLib.Menu ();
-
-        /* Translators: entry in the hamburger menu; opens a window showing best scores */
-        section.append (_("Scores"), "app.scores");
-
-        section.freeze ();
-        menu.append_section (null, section);
-    }
-
-    private static inline void _append_app_actions_section (ref GLib.Menu menu)
-    {
-        GLib.Menu section = new GLib.Menu ();
-
-        /* Translators: usual menu entry of the hamburger menu */
-        section.append (_("Keyboard Shortcuts"), "win.show-help-overlay");
-
-        /* Translators: entry in the hamburger menu */
-        section.append (_("About 2048"), "app.about");
-
-        section.freeze ();
-        menu.append_section (null, section);
-    }
-
-    private void toggle_hamburger_menu (/* SimpleAction action, Variant? variant */)
-    {
-        _hamburger_button.active = !_hamburger_button.active;
-    }
-
-    private void undo_cb (/* SimpleAction action, Variant? variant */)
-    {
-        if (!_settings.get_boolean ("allow-undo"))   // for the keyboard shortcut
-            return;
-
-        _header_bar.set_subtitle (null);
-        _header_bar.set_has_subtitle (false);
-
-        _game.undo ();
-    }
-
-    private void new_game_cb (/* SimpleAction action, Variant? variant */)
-    {
-        _header_bar.set_subtitle (null);
-        _header_bar.set_has_subtitle (false);
-        _game_restored = false;
-
-        _game.new_game (ref _settings);
-
-        _embed.grab_focus ();
-    }
-
-    private void toggle_new_game_cb (/* SimpleAction action, Variant? variant */)
-    {
-        _new_game_button.active = !_new_game_button.active;
-    }
-
-    private void new_game_sized_cb (SimpleAction action, Variant? variant)
-        requires (variant != null)
-    {
-        int rows, cols;
-        ((!) variant).@get ("(ii)", out rows, out cols);
-        _settings.delay ();
-        _settings.set_int ("rows", rows);
-        _settings.set_int ("cols", cols);
-        _settings.apply ();
-
-        new_game_cb ();
-    }
-
-    private void about_cb (/* SimpleAction action, Variant? variant */)
-    {
-        string [] authors = { "Juan R. García Blanco", "Arnaud Bonatti" };
-        show_about_dialog (_window,
-                           /* Translators: about dialog text; the program name */
-                           "program-name", _("2048"),
-                           "version", VERSION,
-
-                           /* Translators: about dialog text; a introduction to the game */
-                           "comments", _("A clone of 2048 for GNOME"),
-                           "license-type", License.GPL_3_0,
-
-                           /* Translators: about dialog text; the main copyright holders */
-                           "copyright", _("Copyright \xc2\xa9 2014-2015 – Juan R. García Blanco\nCopyright 
\xc2\xa9 2016-2019 – Arnaud Bonatti"),
-                           "wrap-license", true,
-                           "authors", authors,
-                           /* Translators: about dialog text; this string should be replaced by a text 
crediting yourselves and your translation team, or should be left empty. Do not translate literally! */
-                           "translator-credits", _("translator-credits"),
-                           "logo-icon-name", "org.gnome.TwentyFortyEight",
-                           "website", "https://wiki.gnome.org/Apps/2048";,
-                           /* Translators: about dialog text; label of the website link */
-                           "website-label", _("Page on GNOME wiki"),
-                           null);
     }
 
     private void quit_cb (/* SimpleAction action, Variant? variant */)
     {
         _window.destroy ();
     }
-
-    /*\
-    * * new-game menu
-    \*/
-
-    private void _update_new_game_menu ()
-    {
-        GLib.Menu menu = new GLib.Menu ();
-
-        /* Translators: on main window, entry of the menu when clicking on the "New Game" button; to change 
grid size to 3 × 3 */
-        _append_new_game_item (_("3 × 3"),
-                    /* rows */ 3,
-                    /* cols */ 3,
-                           ref menu);
-
-        /* Translators: on main window, entry of the menu when clicking on the "New Game" button; to change 
grid size to 4 × 4 */
-        _append_new_game_item (_("4 × 4"),
-                    /* rows */ 4,
-                    /* cols */ 4,
-                           ref menu);
-
-        /* Translators: on main window, entry of the menu when clicking on the "New Game" button; to change 
grid size to 5 × 5 */
-        _append_new_game_item (_("5 × 5"),
-                    /* rows */ 5,
-                    /* cols */ 5,
-                           ref menu);
-
-        int rows = _settings.get_int ("rows");
-        int cols = _settings.get_int ("cols");
-        bool is_square = rows == cols;
-        bool disallowed_grid = is_disallowed_grid_size (ref rows, ref cols);
-        if (disallowed_grid && !is_square)
-            /* Translators: command-line warning displayed if the user manually sets a invalid grid size */
-            warning (_("Grids of size 1 by 2 are disallowed."));
-
-        if (!disallowed_grid && (!is_square || (is_square && rows != 4 && rows != 3 && rows != 5)))
-            /* Translators: on main window, entry of the menu when clicking on the "New Game" button; 
appears only if the user has set rows and cols manually */
-            _append_new_game_item (_("Custom"), /* rows */ rows, /* cols */ cols, ref menu);
-
-        menu.freeze ();
-        _new_game_button.set_menu_model ((MenuModel) menu);
-    }
-    private static void _append_new_game_item (string label, int rows, int cols, ref GLib.Menu menu)
-    {
-        Variant variant = new Variant ("(ii)", rows, cols);
-        menu.append (label, "app.new-game-sized(" + variant.print (/* annotate types */ true) + ")");
-    }
-
-    internal static bool is_disallowed_grid_size (ref int rows, ref int cols)
-        requires (rows >= 1)
-        requires (rows <= 9)
-        requires (cols >= 1)
-        requires (cols <= 9)
-    {
-        return (rows == 1 && cols == 1) || (rows == 1 && cols == 2) || (rows == 2 && cols == 1);
-    }
-
-    /*\
-    * * window management callbacks
-    \*/
-
-    private const uint16 KEYCODE_W = 25;
-    private const uint16 KEYCODE_A = 38;
-    private const uint16 KEYCODE_S = 39;
-    private const uint16 KEYCODE_D = 40;
-
-    private bool key_press_event_cb (Widget widget, Gdk.EventKey event)
-    {
-        if (_hamburger_button.active || (_window.focus_visible && !_embed.is_focus))
-            return false;
-        if (_game.cannot_move ())
-            return false;
-
-        switch (event.hardware_keycode)
-        {
-            case KEYCODE_W:     _request_move (MoveRequest.UP);     return true;    // or KEYCODE_UP    = 
111;
-            case KEYCODE_A:     _request_move (MoveRequest.LEFT);   return true;    // or KEYCODE_LEFT  = 
113;
-            case KEYCODE_S:     _request_move (MoveRequest.DOWN);   return true;    // or KEYCODE_DOWN  = 
116;
-            case KEYCODE_D:     _request_move (MoveRequest.RIGHT);  return true;    // or KEYCODE_RIGHT = 
114;
-        }
-        switch (_upper_key (event.keyval))
-        {
-            case Gdk.Key.Up:    _request_move (MoveRequest.UP);     return true;
-            case Gdk.Key.Left:  _request_move (MoveRequest.LEFT);   return true;
-            case Gdk.Key.Down:  _request_move (MoveRequest.DOWN);   return true;
-            case Gdk.Key.Right: _request_move (MoveRequest.RIGHT);  return true;
-        }
-        return false;
-    }
-    private static inline uint _upper_key (uint keyval)
-    {
-        return (keyval > 255) ? keyval : ((char) keyval).toupper ();
-    }
-
-    private void window_size_allocate_cb ()
-    {
-        if (_window_maximized || _window_is_tiled)
-            return;
-        int? window_width = null;
-        int? window_height = null;
-        _window.get_size (out window_width, out window_height);
-        if (window_width == null || window_height == null)
-            return;
-        _window_width = (!) window_width;
-        _window_height = (!) window_height;
-    }
-
-    private bool window_state_event_cb (Gdk.EventWindowState event)
-    {
-        if ((event.changed_mask & Gdk.WindowState.MAXIMIZED) != 0)
-            _window_maximized = (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0;
-        /* We don’t save this state, but track it for saving size allocation */
-        if ((event.changed_mask & Gdk.WindowState.TILED) != 0)
-            _window_is_tiled = (event.new_window_state & Gdk.WindowState.TILED) != 0;
-
-        return false;
-    }
-
-    /*\
-    * * congratulations dialog
-    \*/
-
-    private MessageDialog _congrats_dialog;
-
-    private bool _should_create_congrats_dialog = true;
-    private inline void _create_congrats_dialog ()
-    {
-        Builder builder = new Builder.from_resource ("/org/gnome/TwentyFortyEight/ui/congrats.ui");
-
-        _congrats_dialog = (MessageDialog) builder.get_object ("congratsdialog");
-        _congrats_dialog.set_transient_for (_window);
-
-        _congrats_dialog.response.connect ((response_id) => {
-                if (response_id == 0)
-                    new_game_cb ();
-                _congrats_dialog.hide ();
-            });
-        _congrats_dialog.delete_event.connect ((response_id) => {
-                return _congrats_dialog.hide_on_delete ();
-            });
-    }
-
-    private inline void target_value_reached_cb (uint target_value)
-    {
-        if (_settings.get_boolean ("do-congrat"))
-        {
-            if (_should_create_congrats_dialog)
-            {
-                _create_congrats_dialog ();
-                _should_create_congrats_dialog = false;
-            }
-
-            /* Translators: text of the dialog that appears when the user obtains the first 2048 tile in the 
game; the %u is replaced by the number the user wanted to reach (usually, 2048) */
-            _congrats_dialog.format_secondary_text (_("You have obtained the %u tile for the first time!"), 
target_value);
-            _congrats_dialog.present ();
-            _settings.set_boolean ("do-congrat", false);
-        }
-        debug ("target value reached");
-    }
-
-    /*\
-    * * scores dialog
-    \*/
-
-    private Scores.Context _scores_ctx;
-    private Scores.Category _grid4_cat;
-    private Scores.Category _grid3_cat;
-    private Scores.Category _grid5_cat;
-
-    private inline void _create_scores_dialog ()
-    {
-        /* Translators: combobox entry in the dialog that appears when the user clicks the "Scores" entry in 
the hamburger menu, if the user has already finished at least one 3 × 3 game and one of other size */
-        _grid3_cat = new Scores.Category ("grid3", _("Grid 3 × 3"));
-
-        /* Translators: combobox entry in the dialog that appears when the user clicks the "Scores" entry in 
the hamburger menu, if the user has already finished at least one 4 × 4 game and one of other size */
-        _grid4_cat = new Scores.Category ("grid4", _("Grid 4 × 4"));
-
-        /* Translators: combobox entry in the dialog that appears when the user clicks the "Scores" entry in 
the hamburger menu, if the user has already finished at least one 5 × 5 game and one of other size */
-        _grid5_cat = new Scores.Category ("grid5", _("Grid 5 × 5"));
-
-        /* Translators: label introducing a combobox in the dialog that appears when the user clicks the 
"Scores" entry in the hamburger menu, if the user has already finished at least two games of different size 
(between 3 × 3, 4 × 4 and 5 × 5) */
-        _scores_ctx = new Scores.Context ("gnome-2048", _("Grid Size:"), _window, category_request, 
Scores.Style.POINTS_GREATER_IS_BETTER);
-    }
-    private inline Games.Scores.Category category_request (string key)
-    {
-        switch (key)
-        {
-            case "grid4": return _grid4_cat;
-            case "grid3": return _grid3_cat;
-            case "grid5": return _grid5_cat;
-            default: assert_not_reached ();
-        }
-    }
-
-    private inline void scores_cb (/* SimpleAction action, Variant? variant */)
-    {
-        _scores_ctx.run_dialog ();  // TODO open it for current Scores.Category
-    }
-
-    private inline void _show_best_scores ()
-    {
-        int rows = _settings.get_int ("rows");
-        int cols = _settings.get_int ("cols");
-        if (rows != cols)
-            return;                 // FIXME add categories for non-square grids
-        Scores.Category cat;
-        switch (rows)
-        {
-            case 4: cat = _grid4_cat; break;
-            case 3: cat = _grid3_cat; break;
-            case 5: cat = _grid5_cat; break;
-            default: return; // FIXME add categories for non-usual square grids
-        }
-        _scores_ctx.add_score.begin (_game.score, cat, null, (object, result) => {
-                try {
-                    _scores_ctx.add_score.end (result);
-                } catch (GLib.Error e) {
-                    stderr.printf ("%s\n", e.message);
-                }
-                _scores_ctx.run_dialog ();
-                debug ("score added");
-            });
-    }
-
-    /*\
-    * * gesture
-    \*/
-
-    private GestureSwipe gesture;
-    private inline void _init_gesture ()
-    {
-        gesture = new GestureSwipe (_embed); // _window works, but problems with headerbar; the main grid or 
the aspectframe do as _embed
-        gesture.set_propagation_phase (PropagationPhase.CAPTURE);
-        gesture.set_button (/* all events */ 0);
-        gesture.swipe.connect (_on_swipe);
-    }
-
-    private inline void _on_swipe (GestureSwipe gesture, double velocity_x, double velocity_y)
-    {
-        if (_game.cannot_move ())
-            return;
-
-        double abs_x = velocity_x.abs ();
-        double abs_y = velocity_y.abs ();
-        if (abs_x * abs_x + abs_y * abs_y < 400.0)
-            return;
-        bool left_or_right = abs_y * 4.0 < abs_x;
-        bool up_or_down = abs_x * 4.0 < abs_y;
-        if (left_or_right)
-        {
-            if (velocity_x < -10.0)
-                _request_move (MoveRequest.LEFT);
-            else if (velocity_x > 10.0)
-                _request_move (MoveRequest.RIGHT);
-        }
-        else if (up_or_down)
-        {
-            if (velocity_y < -10.0)
-                _request_move (MoveRequest.UP);
-            else if (velocity_y > 10.0)
-                _request_move (MoveRequest.DOWN);
-        }
-        else
-            return;
-    }
-
-    /*\
-    * * move requests
-    \*/
-
-    private void _request_move (MoveRequest request)
-    {
-        if (_game_should_init)
-            return;
-
-        _game_restored = false;
-        _game.move (request);
-    }
-}
-
-private enum MoveRequest {
-    UP,
-    RIGHT,
-    DOWN,
-    LEFT;
-
-    internal static string debug_string (MoveRequest request)
-    {
-        switch (request)
-        {
-            case UP:    return "move up";
-            case RIGHT: return "move right";
-            case DOWN:  return "move down";
-            case LEFT:  return "move left";
-            default:    assert_not_reached ();
-        }
-    }
 }
diff --git a/src/game-window.vala b/src/game-window.vala
new file mode 100644
index 0000000..ff636f0
--- /dev/null
+++ b/src/game-window.vala
@@ -0,0 +1,604 @@
+/* Copyright (C) 2014-2015 Juan R. García Blanco <juanrgar gmail com>
+ * Copyright (C) 2016-2019 Arnaud Bonatti <arnaud bonatti gmail com>
+ *
+ * This file is part of GNOME 2048.
+ *
+ * GNOME 2048 is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GNOME 2048 is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GNOME 2048; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+using Games;
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/TwentyFortyEight/ui/game-window.ui")]
+private class GameWindow : ApplicationWindow
+{
+    private GLib.Settings _settings;
+
+    private const int WINDOW_MINIMUM_SIZE_HEIGHT = 600;
+    private const int WINDOW_MINIMUM_SIZE_WIDTH = 600;
+
+    private int _window_width;
+    private int _window_height;
+    private bool _window_maximized;
+    private bool _window_is_tiled;
+
+    [GtkChild] private HeaderBar    _header_bar;
+    [GtkChild] private Label        _score;
+    [GtkChild] private MenuButton   _new_game_button;
+    [GtkChild] private MenuButton   _hamburger_button;
+    [GtkChild] private AspectFrame  _frame;
+    private GtkClutter.Embed _embed;
+
+    private Game _game;
+    private bool _game_restored;
+    private bool _game_should_init = true;
+
+    construct
+    {
+        _settings = new GLib.Settings ("org.gnome.TwentyFortyEight");
+
+        install_ui_action_entries ();
+
+        _init_game ();
+
+        _init_window ();
+        _create_scores_dialog ();   // the library forbids to delay the dialog creation
+
+        notify ["has-toplevel-focus"].connect (() => _embed.grab_focus ());
+        show_all ();
+        _init_gesture ();
+
+        _game_restored = _game.restore_game (ref _settings);
+        if (!_game_restored)
+            new_game_cb ();
+        _game_should_init = false;
+    }
+
+    /*\
+    * * actions
+    \*/
+
+    private SimpleAction undo_action;
+
+    private void install_ui_action_entries ()
+    {
+        SimpleActionGroup action_group = new SimpleActionGroup ();
+        action_group.add_action_entries (ui_action_entries, this);
+        insert_action_group ("ui", action_group);
+
+        undo_action = (SimpleAction) action_group.lookup_action ("undo");
+        undo_action.set_enabled (false);
+    }
+
+    private const GLib.ActionEntry [] ui_action_entries =
+    {
+        { "undo",               undo_cb                     },
+
+        { "new-game",           new_game_cb                 },
+        { "toggle-new-game",    toggle_new_game_cb          },
+        { "new-game-sized",     new_game_sized_cb, "(ii)"   },
+
+        // hamburger-menu
+        { "toggle-hamburger",   toggle_hamburger_menu       },
+
+        { "scores",             scores_cb                   },
+        { "about",              about_cb                    }
+    };
+
+    /*\
+    * * game
+    \*/
+
+    private void _init_game ()
+    {
+        _game = new Game (ref _settings);
+        _game.notify ["score"].connect ((s, p) => {
+                _score.label = _game.score.to_string ();
+            });
+        _game.finished.connect ((s) => {
+                _header_bar.set_has_subtitle (true);
+                /* Translators: subtitle of the headerbar, when the user cannot move anymore */
+                _header_bar.subtitle = _("Game Over");
+
+                if (!_game_restored)
+                    _show_best_scores ();
+
+                debug ("finished");
+            });
+        _game.target_value_reached.connect (target_value_reached_cb);
+        _game.undo_enabled.connect (() => { undo_action.set_enabled (true); });
+        _game.undo_disabled.connect (() => { undo_action.set_enabled (false); });
+    }
+
+    /*\
+    * * window
+    \*/
+
+    private void _init_window ()
+    {
+        set_default_size (_settings.get_int ("window-width"), _settings.get_int ("window-height"));
+        if (_settings.get_boolean ("window-maximized"))
+            maximize ();
+
+        _hamburger_button.notify ["active"].connect (() => {
+                if (!_hamburger_button.active)
+                    _embed.grab_focus ();
+            });
+        _settings.changed.connect ((settings, key_name) => {
+                switch (key_name)
+                {
+                    case "cols":
+                    case "rows":
+                        _update_new_game_menu ();
+                        return;
+                    case "allow-undo":
+                        _update_hamburger_menu ();
+                        _game.load_settings (ref _settings);
+                        return;
+                    case "allow-undo-max":
+                    case "animations-speed":
+                        _game.load_settings (ref _settings);
+                        return;
+                }
+            });
+        _update_new_game_menu ();
+        _update_hamburger_menu ();
+        _game.load_settings (ref _settings);
+
+        _embed = new GtkClutter.Embed ();
+        _frame.add (_embed);
+        _game.view = _embed.get_stage ();
+
+        set_events (get_events () | Gdk.EventMask.STRUCTURE_MASK | Gdk.EventMask.KEY_PRESS_MASK | 
Gdk.EventMask.KEY_RELEASE_MASK);
+
+        Gdk.Geometry geom = Gdk.Geometry ();
+        geom.min_height = WINDOW_MINIMUM_SIZE_HEIGHT;
+        geom.min_width = WINDOW_MINIMUM_SIZE_WIDTH;
+        set_geometry_hints (this, geom, Gdk.WindowHints.MIN_SIZE);
+    }
+
+    /*\
+    * * hamburger menu (and undo action) callbacks
+    \*/
+
+    private void _update_hamburger_menu ()
+    {
+        GLib.Menu menu = new GLib.Menu ();
+
+        if (_settings.get_boolean ("allow-undo"))
+            _append_undo_section (ref menu);
+        _append_scores_section (ref menu);
+        _append_app_actions_section (ref menu);
+
+        menu.freeze ();
+        _hamburger_button.set_menu_model ((MenuModel) menu);
+    }
+
+    private static inline void _append_undo_section (ref GLib.Menu menu)
+    {
+        GLib.Menu section = new GLib.Menu ();
+
+        /* Translators: entry in the hamburger menu, if the "Allow undo" option is set to true */
+        section.append (_("Undo"), "ui.undo");
+
+        section.freeze ();
+        menu.append_section (null, section);
+    }
+
+    private static inline void _append_scores_section (ref GLib.Menu menu)
+    {
+        GLib.Menu section = new GLib.Menu ();
+
+        /* Translators: entry in the hamburger menu; opens a window showing best scores */
+        section.append (_("Scores"), "ui.scores");
+
+        section.freeze ();
+        menu.append_section (null, section);
+    }
+
+    private static inline void _append_app_actions_section (ref GLib.Menu menu)
+    {
+        GLib.Menu section = new GLib.Menu ();
+
+        /* Translators: usual menu entry of the hamburger menu */
+        section.append (_("Keyboard Shortcuts"), "win.show-help-overlay");
+
+        /* Translators: entry in the hamburger menu */
+        section.append (_("About 2048"), "ui.about");
+
+        section.freeze ();
+        menu.append_section (null, section);
+    }
+
+    private void toggle_hamburger_menu (/* SimpleAction action, Variant? variant */)
+    {
+        _hamburger_button.active = !_hamburger_button.active;
+    }
+
+    private void undo_cb (/* SimpleAction action, Variant? variant */)
+    {
+        if (!_settings.get_boolean ("allow-undo"))   // for the keyboard shortcut
+            return;
+
+        _header_bar.set_subtitle (null);
+        _header_bar.set_has_subtitle (false);
+
+        _game.undo ();
+    }
+
+    private void new_game_cb (/* SimpleAction action, Variant? variant */)
+    {
+        _header_bar.set_subtitle (null);
+        _header_bar.set_has_subtitle (false);
+        _game_restored = false;
+
+        _game.new_game (ref _settings);
+
+        _embed.grab_focus ();
+    }
+
+    private void toggle_new_game_cb (/* SimpleAction action, Variant? variant */)
+    {
+        _new_game_button.active = !_new_game_button.active;
+    }
+
+    private void new_game_sized_cb (SimpleAction action, Variant? variant)
+        requires (variant != null)
+    {
+        int rows, cols;
+        ((!) variant).@get ("(ii)", out rows, out cols);
+        _settings.delay ();
+        _settings.set_int ("rows", rows);
+        _settings.set_int ("cols", cols);
+        _settings.apply ();
+
+        new_game_cb ();
+    }
+
+    private void about_cb (/* SimpleAction action, Variant? variant */)
+    {
+        string [] authors = { "Juan R. García Blanco", "Arnaud Bonatti" };
+        show_about_dialog (this,
+                           /* Translators: about dialog text; the program name */
+                           "program-name", _("2048"),
+                           "version", VERSION,
+
+                           /* Translators: about dialog text; a introduction to the game */
+                           "comments", _("A clone of 2048 for GNOME"),
+                           "license-type", License.GPL_3_0,
+
+                           /* Translators: about dialog text; the main copyright holders */
+                           "copyright", _("Copyright \xc2\xa9 2014-2015 – Juan R. García Blanco\nCopyright 
\xc2\xa9 2016-2019 – Arnaud Bonatti"),
+                           "wrap-license", true,
+                           "authors", authors,
+                           /* Translators: about dialog text; this string should be replaced by a text 
crediting yourselves and your translation team, or should be left empty. Do not translate literally! */
+                           "translator-credits", _("translator-credits"),
+                           "logo-icon-name", "org.gnome.TwentyFortyEight",
+                           "website", "https://wiki.gnome.org/Apps/2048";,
+                           /* Translators: about dialog text; label of the website link */
+                           "website-label", _("Page on GNOME wiki"),
+                           null);
+    }
+
+    /*\
+    * * new-game menu
+    \*/
+
+    private void _update_new_game_menu ()
+    {
+        GLib.Menu menu = new GLib.Menu ();
+
+        /* Translators: on main window, entry of the menu when clicking on the "New Game" button; to change 
grid size to 3 × 3 */
+        _append_new_game_item (_("3 × 3"),
+                    /* rows */ 3,
+                    /* cols */ 3,
+                           ref menu);
+
+        /* Translators: on main window, entry of the menu when clicking on the "New Game" button; to change 
grid size to 4 × 4 */
+        _append_new_game_item (_("4 × 4"),
+                    /* rows */ 4,
+                    /* cols */ 4,
+                           ref menu);
+
+        /* Translators: on main window, entry of the menu when clicking on the "New Game" button; to change 
grid size to 5 × 5 */
+        _append_new_game_item (_("5 × 5"),
+                    /* rows */ 5,
+                    /* cols */ 5,
+                           ref menu);
+
+        int rows = _settings.get_int ("rows");
+        int cols = _settings.get_int ("cols");
+        bool is_square = rows == cols;
+        bool disallowed_grid = is_disallowed_grid_size (ref rows, ref cols);
+        if (disallowed_grid && !is_square)
+            /* Translators: command-line warning displayed if the user manually sets a invalid grid size */
+            warning (_("Grids of size 1 by 2 are disallowed."));
+
+        if (!disallowed_grid && (!is_square || (is_square && rows != 4 && rows != 3 && rows != 5)))
+            /* Translators: on main window, entry of the menu when clicking on the "New Game" button; 
appears only if the user has set rows and cols manually */
+            _append_new_game_item (_("Custom"), /* rows */ rows, /* cols */ cols, ref menu);
+
+        menu.freeze ();
+        _new_game_button.set_menu_model ((MenuModel) menu);
+    }
+    private static void _append_new_game_item (string label, int rows, int cols, ref GLib.Menu menu)
+    {
+        Variant variant = new Variant ("(ii)", rows, cols);
+        menu.append (label, "ui.new-game-sized(" + variant.print (/* annotate types */ true) + ")");
+    }
+
+    internal static bool is_disallowed_grid_size (ref int rows, ref int cols)
+        requires (rows >= 1)
+        requires (rows <= 9)
+        requires (cols >= 1)
+        requires (cols <= 9)
+    {
+        return (rows == 1 && cols == 1) || (rows == 1 && cols == 2) || (rows == 2 && cols == 1);
+    }
+
+    /*\
+    * * window management callbacks
+    \*/
+
+    private const uint16 KEYCODE_W = 25;
+    private const uint16 KEYCODE_A = 38;
+    private const uint16 KEYCODE_S = 39;
+    private const uint16 KEYCODE_D = 40;
+
+    [GtkCallback]
+    private bool key_press_event_cb (Widget widget, Gdk.EventKey event)
+    {
+        if (_hamburger_button.active || (((Window) widget).focus_visible && !_embed.is_focus))
+            return false;
+        if (_game.cannot_move ())
+            return false;
+
+        switch (event.hardware_keycode)
+        {
+            case KEYCODE_W:     _request_move (MoveRequest.UP);     return true;    // or KEYCODE_UP    = 
111;
+            case KEYCODE_A:     _request_move (MoveRequest.LEFT);   return true;    // or KEYCODE_LEFT  = 
113;
+            case KEYCODE_S:     _request_move (MoveRequest.DOWN);   return true;    // or KEYCODE_DOWN  = 
116;
+            case KEYCODE_D:     _request_move (MoveRequest.RIGHT);  return true;    // or KEYCODE_RIGHT = 
114;
+        }
+        switch (_upper_key (event.keyval))
+        {
+            case Gdk.Key.Up:    _request_move (MoveRequest.UP);     return true;
+            case Gdk.Key.Left:  _request_move (MoveRequest.LEFT);   return true;
+            case Gdk.Key.Down:  _request_move (MoveRequest.DOWN);   return true;
+            case Gdk.Key.Right: _request_move (MoveRequest.RIGHT);  return true;
+        }
+        return false;
+    }
+    private static inline uint _upper_key (uint keyval)
+    {
+        return (keyval > 255) ? keyval : ((char) keyval).toupper ();
+    }
+
+    [GtkCallback]
+    private void size_allocate_cb ()
+    {
+        if (_window_maximized || _window_is_tiled)
+            return;
+        int? window_width = null;
+        int? window_height = null;
+        get_size (out window_width, out window_height);
+        if (window_width == null || window_height == null)
+            return;
+        _window_width = (!) window_width;
+        _window_height = (!) window_height;
+    }
+
+    [GtkCallback]
+    private bool state_event_cb (Gdk.EventWindowState event)
+    {
+        if ((event.changed_mask & Gdk.WindowState.MAXIMIZED) != 0)
+            _window_maximized = (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0;
+        /* We don’t save this state, but track it for saving size allocation */
+        if ((event.changed_mask & Gdk.WindowState.TILED) != 0)
+            _window_is_tiled = (event.new_window_state & Gdk.WindowState.TILED) != 0;
+
+        return false;
+    }
+
+    internal void before_shutdown ()
+    {
+        _game.save_game ();
+
+        _settings.delay ();
+        _settings.set_int ("window-width", _window_width);
+        _settings.set_int ("window-height", _window_height);
+        _settings.set_boolean ("window-maximized", _window_maximized);
+        _settings.apply ();
+    }
+
+    /*\
+    * * congratulations dialog
+    \*/
+
+    private MessageDialog _congrats_dialog;
+
+    private bool _should_create_congrats_dialog = true;
+    private inline void _create_congrats_dialog ()
+    {
+        Builder builder = new Builder.from_resource ("/org/gnome/TwentyFortyEight/ui/congrats.ui");
+
+        _congrats_dialog = (MessageDialog) builder.get_object ("congratsdialog");
+        _congrats_dialog.set_transient_for (this);
+
+        _congrats_dialog.response.connect ((response_id) => {
+                if (response_id == 0)
+                    new_game_cb ();
+                _congrats_dialog.hide ();
+            });
+        _congrats_dialog.delete_event.connect ((response_id) => {
+                return _congrats_dialog.hide_on_delete ();
+            });
+    }
+
+    private inline void target_value_reached_cb (uint target_value)
+    {
+        if (_settings.get_boolean ("do-congrat"))
+        {
+            if (_should_create_congrats_dialog)
+            {
+                _create_congrats_dialog ();
+                _should_create_congrats_dialog = false;
+            }
+
+            /* Translators: text of the dialog that appears when the user obtains the first 2048 tile in the 
game; the %u is replaced by the number the user wanted to reach (usually, 2048) */
+            _congrats_dialog.format_secondary_text (_("You have obtained the %u tile for the first time!"), 
target_value);
+            _congrats_dialog.present ();
+            _settings.set_boolean ("do-congrat", false);
+        }
+        debug ("target value reached");
+    }
+
+    /*\
+    * * scores dialog
+    \*/
+
+    private Scores.Context _scores_ctx;
+    private Scores.Category _grid4_cat;
+    private Scores.Category _grid3_cat;
+    private Scores.Category _grid5_cat;
+
+    private inline void _create_scores_dialog ()
+    {
+        /* Translators: combobox entry in the dialog that appears when the user clicks the "Scores" entry in 
the hamburger menu, if the user has already finished at least one 3 × 3 game and one of other size */
+        _grid3_cat = new Scores.Category ("grid3", _("Grid 3 × 3"));
+
+        /* Translators: combobox entry in the dialog that appears when the user clicks the "Scores" entry in 
the hamburger menu, if the user has already finished at least one 4 × 4 game and one of other size */
+        _grid4_cat = new Scores.Category ("grid4", _("Grid 4 × 4"));
+
+        /* Translators: combobox entry in the dialog that appears when the user clicks the "Scores" entry in 
the hamburger menu, if the user has already finished at least one 5 × 5 game and one of other size */
+        _grid5_cat = new Scores.Category ("grid5", _("Grid 5 × 5"));
+
+        /* Translators: label introducing a combobox in the dialog that appears when the user clicks the 
"Scores" entry in the hamburger menu, if the user has already finished at least two games of different size 
(between 3 × 3, 4 × 4 and 5 × 5) */
+        _scores_ctx = new Scores.Context ("gnome-2048", _("Grid Size:"), this, category_request, 
Scores.Style.POINTS_GREATER_IS_BETTER);
+    }
+    private inline Games.Scores.Category category_request (string key)
+    {
+        switch (key)
+        {
+            case "grid4": return _grid4_cat;
+            case "grid3": return _grid3_cat;
+            case "grid5": return _grid5_cat;
+            default: assert_not_reached ();
+        }
+    }
+
+    private inline void scores_cb (/* SimpleAction action, Variant? variant */)
+    {
+        _scores_ctx.run_dialog ();  // TODO open it for current Scores.Category
+    }
+
+    private inline void _show_best_scores ()
+    {
+        int rows = _settings.get_int ("rows");
+        int cols = _settings.get_int ("cols");
+        if (rows != cols)
+            return;                 // FIXME add categories for non-square grids
+        Scores.Category cat;
+        switch (rows)
+        {
+            case 4: cat = _grid4_cat; break;
+            case 3: cat = _grid3_cat; break;
+            case 5: cat = _grid5_cat; break;
+            default: return; // FIXME add categories for non-usual square grids
+        }
+        _scores_ctx.add_score.begin (_game.score, cat, null, (object, result) => {
+                try {
+                    _scores_ctx.add_score.end (result);
+                } catch (GLib.Error e) {
+                    stderr.printf ("%s\n", e.message);
+                }
+                _scores_ctx.run_dialog ();
+                debug ("score added");
+            });
+    }
+
+    /*\
+    * * gesture
+    \*/
+
+    private GestureSwipe gesture;
+    private inline void _init_gesture ()
+    {
+        gesture = new GestureSwipe (_embed); // _window works, but problems with headerbar; the main grid or 
the aspectframe do as _embed
+        gesture.set_propagation_phase (PropagationPhase.CAPTURE);
+        gesture.set_button (/* all events */ 0);
+        gesture.swipe.connect (_on_swipe);
+    }
+
+    private inline void _on_swipe (GestureSwipe gesture, double velocity_x, double velocity_y)
+    {
+        if (_game.cannot_move ())
+            return;
+
+        double abs_x = velocity_x.abs ();
+        double abs_y = velocity_y.abs ();
+        if (abs_x * abs_x + abs_y * abs_y < 400.0)
+            return;
+        bool left_or_right = abs_y * 4.0 < abs_x;
+        bool up_or_down = abs_x * 4.0 < abs_y;
+        if (left_or_right)
+        {
+            if (velocity_x < -10.0)
+                _request_move (MoveRequest.LEFT);
+            else if (velocity_x > 10.0)
+                _request_move (MoveRequest.RIGHT);
+        }
+        else if (up_or_down)
+        {
+            if (velocity_y < -10.0)
+                _request_move (MoveRequest.UP);
+            else if (velocity_y > 10.0)
+                _request_move (MoveRequest.DOWN);
+        }
+        else
+            return;
+    }
+
+    /*\
+    * * move requests
+    \*/
+
+    private void _request_move (MoveRequest request)
+    {
+        if (_game_should_init)
+            return;
+
+        _game_restored = false;
+        _game.move (request);
+    }
+}
+
+private enum MoveRequest {
+    UP,
+    RIGHT,
+    DOWN,
+    LEFT;
+
+    internal static string debug_string (MoveRequest request)
+    {
+        switch (request)
+        {
+            case UP:    return "move up";
+            case RIGHT: return "move right";
+            case DOWN:  return "move down";
+            case LEFT:  return "move left";
+            default:    assert_not_reached ();
+        }
+    }
+}
diff --git a/src/grid.vala b/src/grid.vala
index fb09fc6..6b47030 100644
--- a/src/grid.vala
+++ b/src/grid.vala
@@ -557,7 +557,7 @@ private class Grid : Object
             return false;
         int cols = (int) number_64;
 
-        if (Application.is_disallowed_grid_size (ref rows, ref cols))
+        if (GameWindow.is_disallowed_grid_size (ref rows, ref cols))
             return false;
         // number of rows + 1 for size + 1 for score; maybe an empty line at end
         if (lines.length < rows + 2)
diff --git a/src/meson.build b/src/meson.build
index 3a26550..83d3302 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,7 +1,6 @@
 resources = gnome.compile_resources(
   'resources',
   'org.gnome.TwentyFortyEight.gresource.xml',
-  source_dir: '..',
   c_name: 'resources',
 )
 
@@ -9,6 +8,7 @@ gnome_2048_sources = [
   'application.vala',
   'config.vapi',
   'game.vala',
+  'game-window.vala',
   'grid.vala',
   'view.vala',
 ] + resources
@@ -17,13 +17,13 @@ gnome_2048 = executable(
   'gnome-2048',
   gnome_2048_sources,
   dependencies: [
-    posix,
-    libm,
-    gtk,
-    clutter,
-    clutter_gtk,
-    gee,
-    libgnome_games_support,
+    posix_dependency,
+    libm_dependency,
+    gtk_dependency,
+    clutter_dependency,
+    clutter_gtk_dependency,
+    gee_dependency,
+    libgnome_games_support_dependency,
   ],
   c_args: [
     '-DVERSION="@0@"'.format(meson.project_version()),
diff --git a/src/org.gnome.TwentyFortyEight.gresource.xml b/src/org.gnome.TwentyFortyEight.gresource.xml
index 9892b57..fbb336b 100644
--- a/src/org.gnome.TwentyFortyEight.gresource.xml
+++ b/src/org.gnome.TwentyFortyEight.gresource.xml
@@ -1,11 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/TwentyFortyEight/ui">
-    <file preprocess="xml-stripblanks" alias="congrats.ui">data/congrats.ui</file>
-    <file preprocess="xml-stripblanks" alias="mainwindow.ui">data/mainwindow.ui</file>
+    <file preprocess="xml-stripblanks" alias="congrats.ui">../data/congrats.ui</file>
+    <file preprocess="xml-stripblanks" alias="game-window.ui">../data/mainwindow.ui</file>
     <!-- file>data/style.css</file -->
   </gresource>
   <gresource prefix="/org/gnome/TwentyFortyEight/gtk">
-    <file preprocess="xml-stripblanks" alias="help-overlay.ui">data/help-overlay.ui</file>
+    <file preprocess="xml-stripblanks" alias="help-overlay.ui">../data/help-overlay.ui</file>
   </gresource>
 </gresources>


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