[four-in-a-row/arnaudb/new-ui: 5/12] Introduce GameWindow.



commit 40c14c91d59cb89fc889dbe54476fa442d5c0b9c
Author: Arnaud Bonatti <arnaud bonatti gmail com>
Date:   Mon Dec 16 04:26:35 2019 +0100

    Introduce GameWindow.
    
    Copied from Iagno 3.32.

 data/org.gnome.Four-in-a-row.gschema.xml |  56 +++-
 data/ui/fiar-screens.ui                  | 200 ++++++++++++++
 data/ui/four-in-a-row.ui                 | 193 ++++++-------
 po/POTFILES.in                           |   1 +
 src/four-in-a-row.gresource.xml          |   3 +-
 src/four-in-a-row.vala                   | 319 +++++++++++-----------
 src/game-board-view.vala                 |   2 +-
 src/game-window.vala                     | 448 +++++++++++++++++++++++++++++++
 src/meson.build                          |   1 +
 src/prefs-box.vala                       | 109 ++------
 src/prefs.vala                           |  14 -
 src/scorebox.vala                        |  32 ++-
 12 files changed, 1005 insertions(+), 373 deletions(-)
---
diff --git a/data/org.gnome.Four-in-a-row.gschema.xml b/data/org.gnome.Four-in-a-row.gschema.xml
index 56816ac..4d33079 100644
--- a/data/org.gnome.Four-in-a-row.gschema.xml
+++ b/data/org.gnome.Four-in-a-row.gschema.xml
@@ -1,36 +1,88 @@
 <schemalist>
+  <enum id="org.gnome.Four-in-a-row.first-player">
+    <value value="0" nick="human"/>
+    <value value="1" nick="computer"/>
+  </enum>
+
   <schema id="org.gnome.Four-in-a-row" path="/org/gnome/Four-in-a-row/" gettext-domain="four-in-a-row">
+    <key name="first-player" enum="org.gnome.Four-in-a-row.first-player">
+      <default>'human'</default>
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/first-player' 
-->
+      <summary>Who starts</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor 
/org/gnome/Four-in-a-row/first-player' -->
+      <description>Specifies who will start the next one-player game. Ignored for two-player 
games.</description>
+    </key>
+    <key name="num-players" type="i">
+      <default>1</default>
+      <range min="1" max="2" />
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/num-players' 
-->
+      <summary>Number of players</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor 
/org/gnome/Four-in-a-row/num-players' -->
+    </key>
     <key name="opponent" type="i">
-      <range min="0" max="3"/>
+      <range min="1" max="3"/>
       <default>1</default>
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/opponent' -->
       <summary>Opponent</summary>
-      <description>Zero is human; one through three correspond to the level of the computer 
player.</description>
+      <!-- Translators: description of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/opponent' 
-->
+      <description>From 1, the easiest, to 3, the hardest. Ignored for two-player games.</description>
     </key>
     <key name="theme-id" type="i">
       <default>4</default>
       <range min="0" max="4"/>
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/theme-id' -->
       <summary>Theme ID</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/theme-id' 
-->
       <description>A number specifying the preferred theme.</description>
     </key>
     <key name="sound" type="b">
       <default>true</default>
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/sound' -->
       <summary>Sound</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/sound' -->
       <description>Whether or not to play event sounds.</description>
     </key>
     <key name="key-left" type="i">
       <default>65361</default>
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/key-left' -->
       <summary>Move left</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/key-left' 
-->
       <description>Key press to move left.</description>
     </key>
     <key name="key-right" type="i">
       <default>65363</default>
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/key-right' -->
       <summary>Move right</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/key-right' 
-->
       <description>Key press to move right.</description>
     </key>
     <key name="key-drop" type="i">
       <default>65364</default>
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/key-drop' -->
       <summary>Drop marble</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/key-drop' 
-->
       <description>Key press to drop a marble.</description>
     </key>
+    <key name="window-width" type="i">
+      <default>675</default>
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/window-width' 
-->
+      <summary>The width of the window</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor 
/org/gnome/Four-in-a-row/window-width' -->
+      <description>The width of the main window in pixels.</description>
+    </key>
+    <key name="window-height" type="i">
+      <default>550</default>
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Four-in-a-row/window-height' 
-->
+      <summary>The height of the window</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor 
/org/gnome/Four-in-a-row/window-height' -->
+      <description>The height of the main window in pixels.</description>
+    </key>
+    <key name="window-is-maximized" type="b">
+      <default>false</default>
+      <!-- Translators: summary of a settings key, see 'dconf-editor 
/org/gnome/Four-in-a-row/window-is-maximized' -->
+      <summary>A flag to enable maximized mode</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor 
/org/gnome/Four-in-a-row/window-is-maximized' -->
+      <description>If “true”, the main window starts in maximized mode.</description>
+    </key>
   </schema>
 </schemalist>
diff --git a/data/ui/fiar-screens.ui b/data/ui/fiar-screens.ui
new file mode 100644
index 0000000..6c6de7b
--- /dev/null
+++ b/data/ui/fiar-screens.ui
@@ -0,0 +1,200 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   This file is part of GNOME Four-in-a-row.
+
+   Copyright 2015, 2019 Arnaud Bonatti
+
+   GNOME Four-in-a-row 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 Four-in-a-row 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 Four-in-a-row.  If not, see <https://www.gnu.org/licenses/>.
+-->
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <object class="GtkBox" id="new-game-screen">
+    <property name="orientation">vertical</property>
+    <property name="visible">True</property>
+    <property name="homogeneous">True</property>
+    <property name="valign">fill</property>
+    <property name="spacing">18</property>
+    <property name="margin-bottom">25</property><!-- TODO better -->
+    <property name="height-request">263</property>
+    <property name="width-request">400</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="visible">True</property>
+        <property name="spacing">6</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <!-- Translators: when configuring a new game, header of the row for choosing the number of 
players -->
+            <property name="label" translatable="yes">Players</property>
+            <style>
+              <class name="bold-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="homogeneous">True</property>
+            <property name="spacing">12</property>
+            <child>
+              <object class="GtkModelButton">
+                <property name="visible">True</property>
+                <property name="use_underline">True</property>
+                <!-- Translators: when configuring a new game, row "Players", label of the button to choose 
a one-player game (with a mnemonic that appears pressing Alt) -->
+                <property name="text" translatable="yes">_One</property>
+                <property name="action-name">app.num-players</property>
+                <property name="action-target">1</property>
+                <property name="iconic">True</property>
+                <property name="centered">True</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkModelButton">
+                <property name="visible">True</property>
+                <property name="use_underline">True</property>
+                <!-- Translators: when configuring a new game, row "Players", label of the button to choose 
a one-player game (with a mnemonic that appears pressing Alt) -->
+                <property name="text" translatable="yes">_Two</property>
+                <property name="action-name">app.num-players</property>
+                <property name="action-target">2</property>
+                <property name="iconic">True</property>
+                <property name="centered">True</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="visible">True</property>
+        <property name="spacing">6</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <!-- Translators: when configuring a new game, header of the row for choosing the level of the 
artificial intelligence -->
+            <property name="label" translatable="yes">Difficulty</property>
+            <style>
+              <class name="bold-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="difficulty-box">
+            <property name="visible">True</property>
+            <property name="homogeneous">True</property>
+            <property name="spacing">12</property>
+            <child>
+              <object class="GtkModelButton">
+                <property name="visible">True</property>
+                <property name="use_underline">True</property>
+                <!-- Translators: when configuring a new game, row "Difficulty", label of the button to 
choose an easy-level computer adversary (with a mnemonic that appears pressing Alt) -->
+                <property name="text" translatable="yes">_Easy</property>
+                <property name="action-name">app.opponent</property>
+                <property name="action-target">1</property>
+                <property name="iconic">True</property>
+                <property name="centered">True</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkModelButton">
+                <property name="visible">True</property>
+                <property name="use_underline">True</property>
+                <!-- Translators: when configuring a new game, row "Difficulty", label of the button to 
choose a medium-level computer adversary (with a mnemonic that appears pressing Alt) -->
+                <property name="text" translatable="yes">_Medium</property>
+                <property name="action-name">app.opponent</property>
+                <property name="action-target">2</property>
+                <property name="iconic">True</property>
+                <property name="centered">True</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkModelButton">
+                <property name="visible">True</property>
+                <property name="use_underline">True</property>
+                <!-- Translators: when configuring a new game, row "Difficulty", label of the button to 
choose a hard-level computer adversary (with a mnemonic that appears pressing Alt) -->
+                <property name="text" translatable="yes">_Hard</property>
+                <property name="action-name">app.opponent</property>
+                <property name="action-target">3</property>
+                <property name="iconic">True</property>
+                <property name="centered">True</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="visible">True</property>
+        <property name="spacing">6</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <!-- Translators: when configuring a new game, header of the row for choosing whether to start 
or not -->
+            <property name="label" translatable="yes">Game start</property>
+            <style>
+              <class name="bold-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="start-box">
+            <property name="visible">True</property>
+            <property name="homogeneous">True</property>
+            <property name="spacing">12</property>
+            <child>
+              <object class="GtkModelButton">
+                <property name="visible">True</property>
+                <property name="use_underline">True</property>
+                <!-- Translators: when configuring a new game, row "Game start", label of the button to 
start (with a mnemonic that appears pressing Alt) -->
+                <property name="text" translatable="yes">Play _first</property>
+                <property name="action-name">app.first-player</property>
+                <property name="action-target">'human'</property>
+                <property name="iconic">True</property>
+                <property name="centered">True</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkModelButton">
+                <property name="visible">True</property>
+                <property name="use_underline">True</property>
+                <!-- Translators: when configuring a new game, row "Game start", label of the button to play 
after the computer (with a mnemonic that appears pressing Alt) -->
+                <property name="text" translatable="yes">Play _second</property>
+                <property name="action-name">app.first-player</property>
+                <property name="action-target">'computer'</property>
+                <property name="iconic">True</property>
+                <property name="centered">True</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/data/ui/four-in-a-row.ui b/data/ui/four-in-a-row.ui
index 80dfa4d..124144a 100644
--- a/data/ui/four-in-a-row.ui
+++ b/data/ui/four-in-a-row.ui
@@ -17,48 +17,49 @@
 -->
 <interface>
   <requires lib="gtk+" version="3.12"/>
-  <object class="GtkApplicationWindow" id="fiar-window">
-    <property name="can-focus">False</property>
-    <property name="title" translatable="yes">Four-in-a-row</property>
+  <template class="GameWindow" parent="GtkApplicationWindow">
+    <!-- <initial-focus name="view"/> -->
     <child type="titlebar">
       <object class="GtkHeaderBar" id="headerbar">
         <property name="visible">True</property>
-        <property name="can-focus">False</property>
-        <property name="title" translatable="yes">Four-in-a-row</property>
         <property name="show-close-button">True</property>
         <child>
-          <object class="GtkButton">
-            <property name="visible">True</property>
-            <property name="sensitive">False</property>
-            <property name="can-focus">True</property>
-            <property name="focus-on-click">False</property>
-            <property name="receives-default">False</property>
-            <property name="tooltip-text" translatable="yes">Undo your most recent move</property>
+          <object class="GtkBox" id="controls_box">
+            <property name="visible">False</property>
+            <property name="orientation">horizontal</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="back_button">
+            <property name="visible">False</property>
+            <property name="halign">center</property>
             <property name="valign">center</property>
-            <property name="action-name">app.undo-move</property>
+            <!-- Translators: when configuring a new game, if the user has a started game, tooltip text of 
the Go back button -->
+            <property name="tooltip-text" translatable="yes">Go back to the current game</property>
+            <property name="use-underline">True</property>
+            <property name="action-name">ui.back</property>
+            <property name="focus-on-click">False</property>
+            <style>
+              <class name="image-button"/>
+            </style>
             <child>
               <object class="GtkImage">
+                <property name="icon-name">go-previous-symbolic</property>
                 <property name="visible">True</property>
-                <property name="can-focus">False</property>
-                <property name="icon-name">edit-undo-symbolic</property>
                 <property name="icon-size">1</property>
               </object>
             </child>
-            <style>
-              <class name="image-button"/>
-            </style>
           </object>
         </child>
         <child>
-          <object class="GtkMenuButton" id="menu_button">
+          <object class="GtkMenuButton" id="info_button">
             <property name="visible">True</property>
+            <property name="valign">center</property>
             <property name="can-focus">True</property>
-            <property name="receives-default">True</property>
-            <accelerator key="F10" signal="activate"/>
+            <property name="focus-on-click">False</property>
             <child>
               <object class="GtkImage">
                 <property name="visible">True</property>
-                <property name="can-focus">False</property>
                 <property name="icon-name">open-menu-symbolic</property>
                 <property name="icon-size">1</property>
               </object>
@@ -75,91 +76,97 @@
       <object class="GtkOverlay">
         <property name="visible">True</property>
         <child>
-          <object class="GtkAspectFrame">
+          <object class="GtkStack" id="stack">
             <property name="visible">True</property>
-            <property name="can-focus">False</property>
-            <property name="label-xalign">0</property>
-            <property name="shadow-type">none</property>
-            <property name="ratio">1.3999999761581421</property>
-            <property name="obey-child">False</property>
-            <property name="border-width">25</property>
+            <property name="homogeneous">True</property>
             <child>
-              <object class="GtkBox">
+              <object class="GtkBox" id="new_game_box">
+                <property name="orientation">vertical</property>
                 <property name="visible">True</property>
-                <property name="can-focus">False</property>
-                <property name="spacing">25</property>
-                <child>
-                  <object class="GtkAspectFrame" id="frame">
-                    <property name="visible">True</property>
-                    <property name="can-focus">False</property>
-                    <property name="label-xalign">0</property>
-                    <property name="shadow-type">none</property>
-                    <property name="obey-child">False</property>
-                    <child>
-                      <placeholder/>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="expand">True</property>
-                    <property name="fill">True</property>
-                    <property name="position">0</property>
-                  </packing>
-                </child>
+                <property name="halign">center</property>
+                <property name="valign">center</property>
+                <property name="margin">25</property>
+                <property name="width-request">350</property>
+                <property name="height-request">350</property>
+                <property name="spacing">6</property>
+              </object>
+              <packing>
+                <property name="name">start-box</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkAspectFrame">
+                <property name="visible">True</property>
+                <property name="shadow-type">GTK_SHADOW_NONE</property>
+                <property name="obey-child">false</property>
+                <property name="ratio">1.4</property>
+                <property name="margin">25</property>
                 <child>
-                  <object class="GtkButtonBox">
+                  <object class="GtkBox" id="game_box">
                     <property name="visible">True</property>
-                    <property name="can-focus">False</property>
-                    <property name="valign">end</property>
-                    <property name="orientation">vertical</property>
-                    <property name="spacing">6</property>
+                    <property name="orientation">horizontal</property>
+                    <property name="spacing">25</property>
                     <child>
-                      <object class="GtkButton">
-                        <property name="label" translatable="yes">_Hint</property>
-                        <property name="width-request">120</property>
-                        <property name="height-request">60</property>
+                      <object class="GtkBox" id="side_box">
                         <property name="visible">True</property>
-                        <property name="can-focus">False</property>
-                        <property name="receives-default">False</property>
-                        <property name="tooltip-text" translatable="yes">Receive a hint for your next 
move</property>
-                        <property name="valign">center</property>
-                        <property name="action-name">app.hint</property>
-                        <property name="use-underline">True</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">6</property>
+                        <child>
+                          <object class="GtkButton" id="new_game_button">
+                            <property name="visible">True</property>
+                            <property name="use-underline">True</property>
+                            <!-- Translators: during a game, label of the Start Over button (with a mnemonic 
that appears pressing Alt) -->
+                            <property name="label" translatable="yes">_Start Over</property>
+                            <property name="halign">fill</property>
+                            <property name="valign">center</property>
+                            <property name="action-name">ui.new-game</property>
+                            <!-- Translators: during a game, tooltip text of the Start Over button -->
+                            <property name="tooltip-text" translatable="yes">Start a new game</property>
+                            <property name="width-request">120</property>
+                            <property name="height-request">60</property>
+                          </object>
+                          <packing>
+                            <property name="pack-type">end</property>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkButton" id="hint_button">
+                            <property name="visible">True</property>
+                            <property name="use-underline">True</property>
+                            <!-- Translators: during a game, label of the Hint button (with a mnemonic that 
appears pressing Alt) -->
+                            <property name="label" translatable="yes">_Hint</property>
+                            <property name="halign">fill</property>
+                            <property name="valign">center</property>
+                            <property name="action-name">ui.hint</property>
+                            <!-- Translators: during a game, tooltip text of the Hint button -->
+                            <property name="tooltip-text" translatable="yes">Suggest a play</property>
+                            <property name="width-request">120</property>
+                            <property name="height-request">60</property>
+                          </object>
+                          <packing>
+                            <property name="pack-type">end</property>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
                       </object>
                       <packing>
+                        <property name="pack-type">end</property>
                         <property name="expand">False</property>
                         <property name="fill">True</property>
-                        <property name="position">0</property>
-                      </packing>
-                    </child>
-                    <child>
-                      <object class="GtkButton">
-                        <property name="label" translatable="yes">_Start Over</property>
-                        <property name="width-request">120</property>
-                        <property name="height-request">60</property>
-                        <property name="visible">True</property>
-                        <property name="can-focus">False</property>
-                        <property name="receives-default">False</property>
-                        <property name="tooltip-text" translatable="yes">Start a new game</property>
-                        <property name="valign">center</property>
-                        <property name="action-name">app.new-game</property>
-                        <property name="use-underline">True</property>
-                      </object>
-                      <packing>
-                        <property name="expand">False</property>
-                        <property name="fill">False</property>
-                        <property name="pack-type">end</property>
-                        <property name="position">1</property>
+                        <property name="padding">0</property>
                       </packing>
                     </child>
                   </object>
-                  <packing>
-                    <property name="expand">False</property>
-                    <property name="fill">True</property>
-                    <property name="pack-type">end</property>
-                    <property name="position">1</property>
-                  </packing>
                 </child>
               </object>
+              <packing>
+                <property name="name">frame</property>
+              </packing>
             </child>
           </object>
         </child>
@@ -168,7 +175,7 @@
             <property name="visible">False</property>
             <property name="halign">end</property>
             <property name="valign">start</property>
-            <property name="action-name">app.unfullscreen</property>
+            <property name="action-name">ui.unfullscreen</property>
             <style>
               <class name="image-button"/>
               <class name="unfullscreen-button"/>
@@ -185,5 +192,5 @@
         </child>
       </object>
     </child>
-  </object>
+  </template>
 </interface>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index b57d29f..e094d2e 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,5 +1,6 @@
 # List of source files containing translatable strings.
 # Please keep this file in alphabetical order.
+data/ui/fiar-screens.ui
 data/ui/four-in-a-row.ui
 data/org.gnome.Four-in-a-row.appdata.xml.in
 data/org.gnome.Four-in-a-row.desktop.in
diff --git a/src/four-in-a-row.gresource.xml b/src/four-in-a-row.gresource.xml
index a574494..492aeed 100644
--- a/src/four-in-a-row.gresource.xml
+++ b/src/four-in-a-row.gresource.xml
@@ -10,6 +10,7 @@
   </gresource>
   <gresource prefix="/org/gnome/Four-in-a-row/ui">
     <file alias="four-in-a-row.css">../data/four-in-a-row.css</file>
-    <file preprocess="xml-stripblanks" compressed="true" 
alias="four-in-a-row.ui">../data/ui/four-in-a-row.ui</file>
+    <file preprocess="xml-stripblanks" compressed="true" 
alias="fiar-screens.ui">../data/ui/fiar-screens.ui</file>
+    <file preprocess="xml-stripblanks" compressed="true" 
alias="game-window.ui">../data/ui/four-in-a-row.ui</file>
   </gresource>
 </gresources>
diff --git a/src/four-in-a-row.vala b/src/four-in-a-row.vala
index a19aaf9..6ee3745 100644
--- a/src/four-in-a-row.vala
+++ b/src/four-in-a-row.vala
@@ -3,6 +3,7 @@
    This file is part of GNOME Four-in-a-row.
 
    Copyright © 2018 Jacob Humphrey
+   Copyright © 2019 Arnaud Bonatti
 
    GNOME Four-in-a-row is free software: you can redistribute it and/or
    modify it under the terms of the GNU General Public License as published
@@ -31,6 +32,9 @@ private const string APPNAME_LONG = "Four-in-a-row";
 
 private class FourInARow : Gtk.Application
 {
+    /* Translators: application name, as used in the window manager, the window title, the about dialog... */
+    private const string PROGRAM_NAME = _("Four-in-a-row");
+
     private static int main (string [] args)
     {
         Intl.setlocale ();
@@ -52,31 +56,28 @@ private class FourInARow : Gtk.Application
         HINT
     }
 
-    // actions
-    private SimpleAction hint_action;
-    private SimpleAction undo_action;
-    private SimpleAction new_game_action;
-
     // game status
     private bool gameover;
     private bool player_active;
     private PlayerID player;
     private PlayerID winner;
-    internal PlayerID who_starts;
+    private PlayerID last_first_player = PlayerID.NOBODY;
+    private bool one_player_game;
+    private int ai_level;
     /**
      * score:
      *
      * The scores for the current instance (Player 1, Player 2, Draw)
      */
     private int score [3];
+    private bool reset_score = false;
 
     // widgets
     private PrefsBox? prefsbox = null;
     private Scorebox scorebox;
     private GameBoardView game_board_view;
     private Board game_board;
-    private ApplicationWindow window;
-    private Button unfullscreen_button;
+    private GameWindow window;
 
     // game state
     private char vstr [53];
@@ -99,7 +100,6 @@ private class FourInARow : Gtk.Application
 
     private const GLib.ActionEntry app_entries [] =  // see also add_actions()
     {
-        { "unfullscreen",   on_unfullscreen         },
         { "scores",         on_game_scores          },
         { "quit",           on_game_exit            },
         { "preferences",    on_settings_preferences },
@@ -111,11 +111,32 @@ private class FourInARow : Gtk.Application
     {
         stop_anim ();
 
-        undo_action.set_enabled (false);
-        hint_action.set_enabled (false);
+        window.allow_undo (false);
+        window.allow_hint (false);
 
-        who_starts = (who_starts == PlayerID.PLAYER1) ? PlayerID.PLAYER2 : PlayerID.PLAYER1;
-        player = who_starts;
+        one_player_game = Prefs.instance.settings.get_int ("num-players") == 1;
+        if (reset_score)
+        {
+            score = { 0, 0, 0 };
+            scorebox.update (score, one_player_game);
+            reset_score = false;
+        }
+        if (one_player_game)
+        {
+            player = Prefs.instance.settings.get_string ("first-player") == "computer" ? PlayerID.PLAYER2 : 
PlayerID.PLAYER1;
+            Prefs.instance.settings.set_string ("first-player", player == PlayerID.PLAYER1 ? "computer" : 
"human");
+            ai_level = Prefs.instance.settings.get_int ("opponent");
+        }
+        else
+        {
+            switch (last_first_player)
+            {
+                case PlayerID.PLAYER1: player = PlayerID.PLAYER2; break;
+                case PlayerID.PLAYER2:
+                case PlayerID.NOBODY : player = PlayerID.PLAYER1; break;
+            }
+            last_first_player = player;
+        }
 
         gameover = true;
         player_active = false;
@@ -134,8 +155,7 @@ private class FourInARow : Gtk.Application
         prompt_player ();
         if (!is_player_human ())
         {
-            vstr [0] = player == PLAYER1 ? vlevel [Prefs.instance.level [PlayerID.PLAYER1]]
-                                         : vlevel [Prefs.instance.level [PlayerID.PLAYER2]];
+            vstr [0] = vlevel [ai_level];
             game_process_move (playgame ((string) vstr) - 1);
         }
     }
@@ -165,24 +185,20 @@ private class FourInARow : Gtk.Application
     {
         add_action (Prefs.instance.settings.create_action ("sound"));
         add_action (Prefs.instance.settings.create_action ("theme-id"));
-
-        new_game_action = new SimpleAction ("new-game", null);
-        new_game_action.activate.connect (on_game_new);
-        add_action (new_game_action);
-
-        hint_action = new SimpleAction ("hint", null);
-        hint_action.activate.connect (on_game_hint);
-        add_action (hint_action);
-
-        undo_action = new SimpleAction ("undo-move", null);
-        undo_action.activate.connect (on_game_undo);
-        add_action (undo_action);
-
-        set_accels_for_action ("app.new-game",  { "<Primary>n" });
-        set_accels_for_action ("app.hint",      { "<Primary>h" });
-        set_accels_for_action ("app.undo-move", { "<Primary>z" });
-        set_accels_for_action ("app.quit",      { "<Primary>q" });
-        set_accels_for_action ("app.help",      {          "F1"});
+        add_action (Prefs.instance.settings.create_action ("num-players"));
+        add_action (Prefs.instance.settings.create_action ("first-player"));
+        add_action (Prefs.instance.settings.create_action ("opponent"));
+
+        set_accels_for_action ("ui.new-game",           {        "<Primary>n"       });
+        set_accels_for_action ("ui.start-game",         { "<Shift><Primary>n"       });
+        set_accels_for_action ("app.quit",              {        "<Primary>q"       });
+        set_accels_for_action ("ui.hint",               {        "<Primary>h"       });
+        set_accels_for_action ("ui.undo",               {        "<Primary>z"       });
+     // set_accels_for_action ("ui.redo",               { "<Shift><Primary>z"       });
+        set_accels_for_action ("ui.back",               {                 "Escape"  });
+        set_accels_for_action ("ui.toggle-hamburger",   {                 "F10"     });
+        set_accels_for_action ("app.help",              {                 "F1"      });
+        set_accels_for_action ("app.about",             {          "<Shift>F1"      });
 
         add_action_entries (app_entries, this);
     }
@@ -244,11 +260,8 @@ private class FourInARow : Gtk.Application
         player_active = false;
         player = PlayerID.PLAYER1;
         winner = PlayerID.NOBODY;
-        score [PlayerID.PLAYER1] = 0;
-        score [PlayerID.PLAYER2] = 0;
-        score [PlayerID.NOBODY]  = 0;
+        score = { 0, 0, 0 };
         game_board = new Board ();
-        who_starts = PlayerID.PLAYER2;     /* This gets reversed immediately. */
 
         clear_board ();
     }
@@ -261,32 +274,29 @@ private class FourInARow : Gtk.Application
         window.show ();
         game_board_view.refresh_pixmaps ();
         game_board_view.queue_draw ();
-        scorebox.update (score);    /* update visible player descriptions */
+        scorebox.update (score, one_player_game);    /* update visible player descriptions */
         prompt_player ();
         game_reset ();
     }
 
+    protected override void shutdown ()
+    {
+        window.shutdown (Prefs.instance.settings);
+        base.shutdown ();
+    }
+
     private void prompt_player ()
     {
-        int players = Prefs.instance.get_n_human_players ();
         bool human = is_player_human ();
         string who;
         string str;
 
-        hint_action.set_enabled (human && !gameover);
+        window.allow_hint (human && !gameover);
 
-        switch (players)
-        {
-            case 0:
-                undo_action.set_enabled (false);
-                break;
-            case 1:
-                undo_action.set_enabled ((human && moves >1) || (!human && gameover));
-                break;
-            case 2:
-                undo_action.set_enabled (moves > 0);
-                break;
-        }
+        if (one_player_game)
+            window.allow_undo ((human && moves >1) || (!human && gameover));
+        else
+            window.allow_undo (moves > 0);
 
         if (gameover && winner == PlayerID.NOBODY)
         {
@@ -297,47 +307,44 @@ private class FourInARow : Gtk.Application
             return;
         }
 
-        switch (players)
+        if (one_player_game)
         {
-            case 1:
-                if (human)
-                {
-                    if (gameover)
-                        set_status_message (_("You win!"));
-                    else
-                        set_status_message (_("Your Turn"));
-                }
-                else
-                {
-                    if (gameover)
-                        set_status_message (_("I win!"));
-                    else
-                        set_status_message (_("I’m Thinking…"));
-                }
-                break;
-
-            case 2:
-            case 0:
+            if (human)
+            {
                 if (gameover)
-                {
-                    who = player == PLAYER1 ? theme_get_player_win (PlayerID.PLAYER1)
-                                            : theme_get_player_win (PlayerID.PLAYER2);
-                    str = _(who);
-                }
-                else if (player_active)
-                {
+                    set_status_message (_("You win!"));
+                else
                     set_status_message (_("Your Turn"));
-                    return;
-                }
+            }
+            else
+            {
+                if (gameover)
+                    set_status_message (_("I win!"));
                 else
-                {
-                    who = player == PLAYER1 ? theme_get_player_turn (PlayerID.PLAYER1)
-                                            : theme_get_player_turn (PlayerID.PLAYER2);
-                    str = _(who);
-                }
-
-                set_status_message (str);
-                break;
+                    set_status_message (_("I’m Thinking…"));
+            }
+        }
+        else
+        {
+            if (gameover)
+            {
+                who = player == PLAYER1 ? theme_get_player_win (PlayerID.PLAYER1)
+                                        : theme_get_player_win (PlayerID.PLAYER2);
+                str = _(who);
+            }
+            else if (player_active)
+            {
+                set_status_message (_("Your Turn"));
+                return;
+            }
+            else
+            {
+                who = player == PLAYER1 ? theme_get_player_turn (PlayerID.PLAYER1)
+                                        : theme_get_player_turn (PlayerID.PLAYER2);
+                str = _(who);
+            }
+
+            set_status_message (str);
         }
     }
 
@@ -365,7 +372,7 @@ private class FourInARow : Gtk.Application
         if (gameover)
         {
             score [winner]++;
-            scorebox.update (score);
+            scorebox.update (score, one_player_game);
             prompt_player ();
         }
         else
@@ -373,8 +380,7 @@ private class FourInARow : Gtk.Application
             swap_player ();
             if (!is_player_human ())
             {
-                vstr [0] = player == PlayerID.PLAYER1 ? vlevel [Prefs.instance.level [PlayerID.PLAYER1]]
-                                                      : vlevel [Prefs.instance.level [PlayerID.PLAYER2]];
+                vstr [0] = vlevel [ai_level];
                 c = playgame ((string) vstr) - 1;
                 if (c < 0)
                     gameover = true;
@@ -386,8 +392,10 @@ private class FourInARow : Gtk.Application
 
     private bool is_player_human ()
     {
-        return player == PLAYER1 ? Prefs.instance.level [PlayerID.PLAYER1] == Level.HUMAN
-                                 : Prefs.instance.level [PlayerID.PLAYER2] == Level.HUMAN;
+        if (one_player_game)
+            return player == PlayerID.PLAYER1;
+        else
+            return true;
     }
 
     private void process_move2 (int c)
@@ -452,7 +460,7 @@ private class FourInARow : Gtk.Application
 
     private void set_status_message (string? message)
     {
-        headerbar.set_title (message);
+        window.set_subtitle (message);
     }
 
     private class NextMove
@@ -520,8 +528,8 @@ private class FourInARow : Gtk.Application
         if (gameover)
             return;
 
-        hint_action.set_enabled (false);
-        undo_action.set_enabled (false);
+        window.allow_hint (false);
+        window.allow_undo (false);
 
         set_status_message (_("I’m Thinking…"));
 
@@ -541,9 +549,9 @@ private class FourInARow : Gtk.Application
         set_status_message (s);
 
         if (moves <= 0 || (moves == 1 && is_player_human ()))
-            undo_action.set_enabled (false);
+            window.allow_undo (false);
         else
-            undo_action.set_enabled (true);
+            window.allow_undo (true);
     }
 
     private inline void on_game_scores (/* SimpleAction action, Variant? parameter */)
@@ -552,24 +560,6 @@ private class FourInARow : Gtk.Application
         return;
     }
 
-    private bool window_is_fullscreen = false;
-    private bool window_state_event_cb (Gdk.EventWindowState event)
-    {
-        bool window_was_fullscreen = window_is_fullscreen;
-        if ((event.changed_mask & Gdk.WindowState.FULLSCREEN) != 0)
-            window_is_fullscreen = (event.new_window_state & Gdk.WindowState.FULLSCREEN) != 0;
-        if (window_was_fullscreen && !window_is_fullscreen)
-            unfullscreen_button.hide ();
-        else if (!window_was_fullscreen && window_is_fullscreen)
-            unfullscreen_button.show ();
-        return false;
-    }
-
-    private inline void on_unfullscreen (/* SimpleAction action, Variant? parameter */)
-    {
-        window.unfullscreen ();
-    }
-
     private inline void on_game_exit (/* SimpleAction action, Variant? parameter */)
     {
         stop_anim ();
@@ -644,7 +634,7 @@ private class FourInARow : Gtk.Application
         }
     }
 
-    private inline void on_game_undo (SimpleAction action, Variant? parameter)
+    private inline void on_game_undo ()
     {
         if (timeout != 0)
             return;
@@ -658,7 +648,7 @@ private class FourInARow : Gtk.Application
         if (gameover)
         {
             score [winner]--;
-            scorebox.update (score);
+            scorebox.update (score, one_player_game);
             gameover = false;
             prompt_player ();
         }
@@ -669,7 +659,7 @@ private class FourInARow : Gtk.Application
         game_board [r, c] = Tile.CLEAR;
         game_board_view.draw_tile (r, c);
 
-        if (Prefs.instance.get_n_human_players () == 1
+        if (one_player_game
          && !is_player_human ()
          && moves > 0)
         {
@@ -738,16 +728,11 @@ private class FourInARow : Gtk.Application
         {
             gameover = true;
             winner = player;
-            switch (Prefs.instance.get_n_human_players ())
-            {
-                case 1:
-                    play_sound (is_player_human () ? SoundID.YOU_WIN : SoundID.I_WIN);
-                    break;
-                case 0:
-                case 2:
-                    play_sound (SoundID.PLAYER_WIN);
-                    break;
-            }
+            if (one_player_game)
+                play_sound (is_player_human () ? SoundID.YOU_WIN : SoundID.I_WIN);
+            else
+                play_sound (SoundID.PLAYER_WIN);
+            window.allow_hint (false);
             blink_winner (6);
         }
         else if (moves == 42)
@@ -762,30 +747,12 @@ private class FourInARow : Gtk.Application
     {
         base.startup ();
 
-        CssProvider css_provider = new CssProvider ();
-        css_provider.load_from_resource ("/org/gnome/Four-in-a-row/ui/four-in-a-row.css");
-        Gdk.Screen? gdk_screen = Gdk.Screen.get_default ();
-        if (gdk_screen != null) // else..?
-            StyleContext.add_provider_for_screen ((!) gdk_screen, css_provider, 
STYLE_PROVIDER_PRIORITY_APPLICATION);
+        /* UI parts */
+        Builder builder = new Builder.from_resource ("/org/gnome/Four-in-a-row/ui/fiar-screens.ui");
 
         game_board_view = new GameBoardView (game_board);
         game_board_view.show ();
-        Builder builder = new Builder.from_resource ("/org/gnome/Four-in-a-row/ui/four-in-a-row.ui");
-
-        window = (ApplicationWindow) builder.get_object ("fiar-window");
-        window.application = this;
-        window.window_state_event.connect (window_state_event_cb);
-        window.set_default_size (DEFAULT_WIDTH, DEFAULT_HEIGHT); /* TODO save size & state */
-
-        unfullscreen_button = (Button) builder.get_object ("unfullscreen_button");
-        headerbar = (HeaderBar) builder.get_object ("headerbar");
-
-        scorebox = new Scorebox (window, this);
 
-        add_actions ();
-
-        /* hamburger button */
-        MenuButton menu_button = (MenuButton) builder.get_object ("menu_button");
         GLib.Menu app_menu = new GLib.Menu ();
 
         GLib.Menu appearance_menu = new GLib.Menu ();
@@ -814,20 +781,56 @@ private class FourInARow : Gtk.Application
         app_menu.append_section (null, section);
 
         app_menu.freeze ();
-        menu_button.set_menu_model (app_menu);
 
-        /* various */
-        Gtk.AspectFrame frame = builder.get_object("frame") as Gtk.AspectFrame;
+        /* Window */
+        window = new GameWindow ("/org/gnome/Four-in-a-row/ui/four-in-a-row.css",
+                                 PROGRAM_NAME,
+                                 Prefs.instance.settings.get_int ("window-width"),
+                                 Prefs.instance.settings.get_int ("window-height"),
+                                 Prefs.instance.settings.get_boolean ("window-is-maximized"),
+                                 /* start_now */ true,
+                                 GameWindowFlags.SHOW_UNDO | GameWindowFlags.SHOW_START_BUTTON,
+                                 (Box) builder.get_object ("new-game-screen"),
+                                 game_board_view,
+                                 app_menu);
+
+        scorebox = new Scorebox (window, this);
+
+        add_actions ();
+
+        Widget level_box = (Widget) (!) builder.get_object ("difficulty-box");
+        Widget start_box = (Widget) (!) builder.get_object ("start-box");
+        Prefs.instance.settings.changed ["num-players"].connect (() => {
+                bool solo = Prefs.instance.settings.get_int ("num-players") == 1;
+                level_box.sensitive = solo;
+                start_box.sensitive = solo;
+                reset_score = true;
+                if (solo)
+                    last_first_player = PlayerID.NOBODY;
+            });
+        bool solo = Prefs.instance.settings.get_int ("num-players") == 1;
+        level_box.sensitive = solo;
+        start_box.sensitive = solo;
+
+        Prefs.instance.settings.changed ["opponent"].connect (() => {
+                if (Prefs.instance.settings.get_int ("num-players") != 1)
+                    return;
+                reset_score = true;
+            });
 
-        frame.add (game_board_view);
+        /* various */
         game_board_view.column_clicked.connect (column_clicked_cb);
         window.key_press_event.connect (on_key_press);
 
-        hint_action.set_enabled (false);
-        undo_action.set_enabled (false);
-    }
+        window.play.connect (on_game_new);
+        window.undo.connect (on_game_undo);
+        window.hint.connect (on_game_hint);
 
-    private HeaderBar headerbar;
+        window.allow_hint (false);
+        window.allow_undo (false);
+
+        add_window (window);
+    }
 
     private inline bool on_key_press (Gdk.EventKey e)
     {
diff --git a/src/game-board-view.vala b/src/game-board-view.vala
index 8f3835e..ecbe16f 100644
--- a/src/game-board-view.vala
+++ b/src/game-board-view.vala
@@ -46,7 +46,7 @@ private class GameBoardView : Gtk.DrawingArea {
 
     private inline int get_column(int xpos) {
         /* Derive column from pixel position */
-        int c = xpos / tile_size;
+        int c = (xpos - board_x) / tile_size;
         if (c > 6)
             c = 6;
         if (c < 0)
diff --git a/src/game-window.vala b/src/game-window.vala
new file mode 100644
index 0000000..5478652
--- /dev/null
+++ b/src/game-window.vala
@@ -0,0 +1,448 @@
+/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+   This file is part of GNOME Four-in-a-row.
+
+   Copyright © 2015, 2016, 2019 Arnaud Bonatti
+
+   GNOME Four-in-a-row 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 Four-in-a-row 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 Four-in-a-row.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+using Gtk;
+
+[Flags]
+private enum GameWindowFlags {
+    SHOW_UNDO,
+ // SHOW_REDO,
+ // SHOW_HINT,
+    SHOW_START_BUTTON;
+}
+
+[GtkTemplate (ui = "/org/gnome/Four-in-a-row/ui/game-window.ui")]
+private class GameWindow : ApplicationWindow
+{
+    /* settings */
+    private bool window_is_tiled;
+    private bool window_is_maximized;
+    private bool window_is_fullscreen;
+    private int window_width;
+    private int window_height;
+
+    private bool game_finished = false;
+
+    private string program_name = "";
+
+    /* private widgets */
+    [GtkChild] private HeaderBar headerbar;
+    [GtkChild] private Stack stack;
+
+    private Button? start_game_button = null;
+    [GtkChild] private Button new_game_button;
+    [GtkChild] private Button back_button;
+    [GtkChild] private Button unfullscreen_button;
+
+    [GtkChild] private Box controls_box;
+    [GtkChild] private Box game_box;
+    [GtkChild] private Box new_game_box;
+    [GtkChild] private Box side_box;
+
+    private Widget view;
+
+    /* signals */
+    internal signal void play ();
+    internal signal void wait ();
+    internal signal void back ();
+
+    internal signal void undo ();
+ // internal signal void redo ();
+    internal signal void hint ();
+
+    internal GameWindow (string? css_resource, string name, int width, int height, bool maximized, bool 
start_now, GameWindowFlags flags, Box new_game_screen, Widget _view, GLib.Menu app_menu)
+    {
+        if (css_resource != null)
+        {
+            CssProvider css_provider = new CssProvider ();
+            css_provider.load_from_resource ((!) css_resource);
+            Gdk.Screen? gdk_screen = Gdk.Screen.get_default ();
+            if (gdk_screen != null) // else..?
+                StyleContext.add_provider_for_screen ((!) gdk_screen, css_provider, 
STYLE_PROVIDER_PRIORITY_APPLICATION);
+        }
+
+        view = _view;
+
+        /* window config */
+        install_ui_action_entries ();
+        program_name = name;
+        set_title (name);
+        headerbar.set_title (name);
+        info_button.set_menu_model (app_menu);
+
+        set_default_size (width, height);
+        if (maximized)
+            maximize ();
+
+        size_allocate.connect (size_allocate_cb);
+        window_state_event.connect (window_state_event_cb);
+
+        /* add widgets */
+        new_game_box.pack_start (new_game_screen, true, true, 0);
+        if (GameWindowFlags.SHOW_START_BUTTON in flags)
+        {
+            /* Translators: when configuring a new game, label of the blue Start button (with a mnemonic 
that appears pressing Alt) */
+            Button _start_game_button = new Button.with_mnemonic (_("_Start Game"));
+            _start_game_button.width_request = 222;
+            _start_game_button.height_request = 60;
+            _start_game_button.halign = Align.CENTER;
+            _start_game_button.set_action_name ("ui.start-game");
+            /* Translators: when configuring a new game, tooltip text of the blue Start button */
+            // _start_game_button.set_tooltip_text (_("Start a new game as configured"));
+            ((StyleContext) _start_game_button.get_style_context ()).add_class ("suggested-action");
+            _start_game_button.show ();
+            new_game_box.pack_end (_start_game_button, false, false, 0);
+            start_game_button = _start_game_button;
+        }
+
+        game_box.pack_start (view, true, true, 0);
+        game_box.set_focus_child (view);            // TODO test if necessary; note: view could grab focus 
from application
+        view.halign = Align.FILL;
+        view.can_focus = true;
+        view.show ();
+
+        /* add controls */
+        if (GameWindowFlags.SHOW_UNDO in flags)
+        {
+            Box history_box = new Box (Orientation.HORIZONTAL, 0);
+            history_box.get_style_context ().add_class ("linked");
+
+            Button undo_button = new Button.from_icon_name ("edit-undo-symbolic", Gtk.IconSize.BUTTON);
+            undo_button.action_name = "ui.undo";
+            /* Translators: during a game, tooltip text of the Undo button */
+            undo_button.set_tooltip_text (_("Undo your most recent move"));
+            undo_button.valign = Align.CENTER;
+            undo_button.show ();
+            history_box.pack_start (undo_button, true, true, 0);
+
+            /* if (GameWindowFlags.SHOW_REDO in flags)
+            {
+                Button redo_button = new Button.from_icon_name ("edit-redo-symbolic", Gtk.IconSize.BUTTON);
+                redo_button.action_name = "app.redo";
+                / Translators: during a game, tooltip text of the Redo button /
+                redo_button.set_tooltip_text (_("Redo your most recent undone move"));
+                redo_button.valign = Align.CENTER;
+                redo_button.show ();
+                history_box.pack_start (redo_button, true, true, 0);
+            } */
+
+            history_box.show ();
+            controls_box.pack_start (history_box, true, true, 0);
+        }
+        /* if (GameWindowFlags.SHOW_HINT in flags)
+        {
+            Button hint_button = new Button.from_icon_name ("dialog-question-symbolic", Gtk.IconSize.BUTTON);
+            hint_button.action_name = "app.hint";
+            / Translators: during a game, tooltip text of the Hint button /
+            hint_button.set_tooltip_text (_("Receive a hint for your next move"));
+            hint_button.valign = Align.CENTER;
+            hint_button.show ();
+            controls_box.pack_start (hint_button, true, true, 0);
+        } */
+
+        /* start or not */
+        if (start_now)
+            show_view ();
+        else
+            show_new_game_screen ();
+    }
+
+    /*\
+    * * actions
+    \*/
+
+    private SimpleAction back_action;
+    private SimpleAction undo_action;
+ // private SimpleAction redo_action;
+    private SimpleAction hint_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);
+
+        back_action = (SimpleAction) action_group.lookup_action ("back");
+        undo_action = (SimpleAction) action_group.lookup_action ("undo");
+     // redo_action = (SimpleAction) action_group.lookup_action ("redo");
+        hint_action = (SimpleAction) action_group.lookup_action ("hint");
+
+        back_action.set_enabled (false);
+        undo_action.set_enabled (false);
+     // redo_action.set_enabled (false);
+        hint_action.set_enabled (false);
+    }
+
+    private const GLib.ActionEntry [] ui_action_entries =
+    {
+        { "new-game", new_game_cb },
+        { "start-game", start_game_cb },
+        { "back", back_cb },
+
+        { "undo", undo_cb },
+     // { "redo", redo_cb },
+        { "hint", hint_cb },
+
+        { "toggle-hamburger", toggle_hamburger },
+        { "unfullscreen", unfullscreen }
+    };
+
+    /*\
+    * * Window events
+    \*/
+
+    private void size_allocate_cb ()
+    {
+        if (window_is_maximized || window_is_tiled || window_is_fullscreen)
+            return;
+        get_size (out window_width, out window_height);
+    }
+
+    private bool window_state_event_cb (Gdk.EventWindowState event)
+    {
+        if ((event.changed_mask & Gdk.WindowState.MAXIMIZED) != 0)
+            window_is_maximized = (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0;
+
+        /* fullscreen: saved as maximized */
+        bool window_was_fullscreen = window_is_fullscreen;
+        if ((event.changed_mask & Gdk.WindowState.FULLSCREEN) != 0)
+            window_is_fullscreen = (event.new_window_state & Gdk.WindowState.FULLSCREEN) != 0;
+        if (window_was_fullscreen && !window_is_fullscreen)
+            unfullscreen_button.hide ();
+        else if (!window_was_fullscreen && window_is_fullscreen)
+            unfullscreen_button.show ();
+
+        /* tiled: not saved, but should not change saved window size */
+        Gdk.WindowState tiled_state = Gdk.WindowState.TILED
+                                    | Gdk.WindowState.TOP_TILED
+                                    | Gdk.WindowState.BOTTOM_TILED
+                                    | Gdk.WindowState.LEFT_TILED
+                                    | Gdk.WindowState.RIGHT_TILED;
+        if ((event.changed_mask & tiled_state) != 0)
+            window_is_tiled = (event.new_window_state & tiled_state) != 0;
+
+        return false;
+    }
+
+    internal void shutdown (GLib.Settings settings)
+    {
+        settings.delay ();
+        settings.set_int ("window-width", window_width);
+        settings.set_int ("window-height", window_height);
+        settings.set_boolean ("window-is-maximized", window_is_maximized || window_is_fullscreen);
+        settings.apply ();
+        destroy ();
+    }
+
+    /*\
+    * * Some internal calls
+    \*/
+
+    internal void add_to_sidebox (Widget widget)
+    {
+        side_box.pack_start (widget, false, false, 0);
+    }
+
+    internal void cannot_undo_more ()
+    {
+        undo_action.set_enabled (false);
+        view.grab_focus ();
+    }
+
+//    internal void new_turn_start (bool can_undo)
+//    {
+//        undo_action.set_enabled (can_undo);
+//        headerbar.set_subtitle (null);
+//    }
+
+    internal void set_subtitle (string? subtitle)
+    {
+        headerbar.set_title (subtitle);
+        last_subtitle = subtitle;
+    }
+
+    internal void finish_game ()
+    {
+        game_finished = true;
+        new_game_button.grab_focus ();
+    }
+
+    /* internal void about ()
+    {
+        TODO
+    } */
+
+    internal void allow_hint (bool allow)
+    {
+        string? stack_child = stack.get_visible_child_name ();
+        if (stack_child == null || (!) stack_child != "frame")
+            return;
+        hint_action.set_enabled (allow);
+    }
+
+    internal void allow_undo (bool allow)
+    {
+        string? stack_child = stack.get_visible_child_name ();
+        if (stack_child == null || (!) stack_child != "frame")
+            return;
+        undo_action.set_enabled (allow);
+    }
+
+    /*\
+    * * Showing the Stack
+    \*/
+
+    private string? last_subtitle = null;
+    private void show_new_game_screen ()
+    {
+        headerbar.set_title (program_name);
+
+        stack.set_visible_child_name ("start-box");
+        controls_box.hide ();
+
+        if (!game_finished && back_button.visible)
+            back_button.grab_focus ();
+        else if (start_game_button != null)
+            ((!) start_game_button).grab_focus ();
+    }
+
+    private void show_view ()
+    {
+        headerbar.set_title (last_subtitle);
+
+        stack.set_visible_child_name ("frame");
+        back_button.hide ();        // TODO transition?
+        controls_box.show ();
+
+        if (game_finished)
+            new_game_button.grab_focus ();
+        else
+            view.grab_focus ();
+    }
+
+    /*\
+    * * Switching the Stack
+    \*/
+
+    private void new_game_cb ()
+    {
+        string? stack_child = stack.get_visible_child_name ();
+        if (stack_child == null || (!) stack_child != "frame")
+            return;
+
+        wait ();
+
+        stack.set_transition_type (StackTransitionType.SLIDE_LEFT);
+        stack.set_transition_duration (800);
+
+        back_button.show ();
+        back_action.set_enabled (true);
+
+        show_new_game_screen ();
+    }
+
+    private void start_game_cb ()
+    {
+        string? stack_child = stack.get_visible_child_name ();
+        if (stack_child == null || (!) stack_child != "start-box")
+            return;
+
+        last_subtitle = null;
+        game_finished = false;
+
+        undo_action.set_enabled (false);
+     // redo_action.set_enabled (false);
+        hint_action.set_enabled (true);
+
+        play ();        // FIXME lag (see in Taquin…)
+
+        stack.set_transition_type (StackTransitionType.SLIDE_DOWN);
+        stack.set_transition_duration (1000);
+        show_view ();
+    }
+
+    private void back_cb ()
+    {
+        string? stack_child = stack.get_visible_child_name ();
+        if (stack_child == null || (!) stack_child != "start-box")
+            return;
+        // TODO change back headerbar subtitle?
+        stack.set_transition_type (StackTransitionType.SLIDE_RIGHT);
+        stack.set_transition_duration (800);
+        show_view ();
+
+        back ();
+    }
+
+    /*\
+    * * Controls_box actions
+    \*/
+
+    private void undo_cb ()
+    {
+        string? stack_child = stack.get_visible_child_name ();
+        if (stack_child == null)
+            return;
+        if ((!) stack_child != "frame")
+        {
+            if (back_action.get_enabled ())
+                back_cb ();
+            return;
+        }
+
+        game_finished = false;
+
+        if (!back_button.is_focus)
+            view.grab_focus();
+     // redo_action.set_enabled (true);
+        undo ();
+    }
+
+/*    private void redo_cb ()
+    {
+        string? stack_child = stack.get_visible_child_name ();
+        if (stack_child == null || (!) stack_child != "frame")
+            return;
+
+        if (!back_button.is_focus)
+            view.grab_focus();
+        undo_action.set_enabled (true);
+        redo ();
+    } */
+
+    private void hint_cb ()
+    {
+        string? stack_child = stack.get_visible_child_name ();
+        if (stack_child == null || (!) stack_child != "frame")
+            return;
+        hint ();
+    }
+
+    /*\
+    * * hamburger menu
+    \*/
+
+    [GtkChild] private MenuButton info_button;
+
+    private void toggle_hamburger (/* SimpleAction action, Variant? variant */)
+    {
+        info_button.active = !info_button.active;
+    }
+}
diff --git a/src/meson.build b/src/meson.build
index 52166bc..bf0c9d2 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -22,6 +22,7 @@ sources = files(
     'four-in-a-row.vala',
     'game-board-view.vala',
     'game-board.vala',
+    'game-window.vala',
     'games-controls-list.vala',
     'prefs-box.vala',
     'prefs.vala',
diff --git a/src/prefs-box.vala b/src/prefs-box.vala
index 921ff43..cc20f2d 100644
--- a/src/prefs-box.vala
+++ b/src/prefs-box.vala
@@ -20,102 +20,31 @@
 
 using Gtk;
 
-private class PrefsBox : Dialog {
-    private const uint DEFAULT_KEY_LEFT = Gdk.Key.Left;
+private class PrefsBox : Dialog
+{
+    private const uint DEFAULT_KEY_LEFT  = Gdk.Key.Left;
     private const uint DEFAULT_KEY_RIGHT = Gdk.Key.Right;
-    private const uint DEFAULT_KEY_DROP = Gdk.Key.Down;
+    private const uint DEFAULT_KEY_DROP  = Gdk.Key.Down;
 
-    internal PrefsBox(Window parent) {
-        Notebook notebook;
-        ComboBox combobox;
-
-        Grid grid;
-        GamesControlsList controls_list;
-        Label label;
-        CellRendererText renderer;
-        Gtk.ListStore model;
-        TreeIter iter;
-
-        Object(
-            title: _("Preferences"),
-            destroy_with_parent: true);
-        set_transient_for(parent);
+    internal PrefsBox (Window parent)
+    {
+        Object (title: _("Keyboard Controls"), destroy_with_parent: true);
+        set_transient_for (parent);
         this.application = parent.application;
         modal = true;
-        border_width = 5;
-        get_content_area().spacing = 2;
-        notebook = new Notebook();
-        notebook.set_border_width(5);
-        get_content_area().pack_start(notebook, true, true, 0);
-
-        /* game tab */
-        grid = new Grid();
-        grid.set_row_spacing(6);
-        grid.set_column_spacing(12);
-        grid.set_border_width(12);
-
-        label = new Label(_("Game"));
-        notebook.append_page(grid, label);
-
-        label = new Label(_("Opponent:"));  // TODO add a mnemonic?
-        label.set_xalign((float)0.0);
-        label.set_yalign((float)0.5);
-        label.set_hexpand(true);
-        grid.attach(label,0,0 ,1, 1);
-
-        combobox = new ComboBox();
-        renderer = new CellRendererText();
-        combobox.pack_start(renderer, true);
-        combobox.add_attribute(renderer, "text", 0);
-        model = new Gtk.ListStore(2, typeof(string), typeof(int));
-        combobox.set_model(model);
-        model.append(out iter);
-        model.@set(iter, 0, _("Human"), 1, Level.HUMAN);
-        if (Prefs.instance.level[PlayerID.PLAYER2] == Level.HUMAN)
-            combobox.set_active_iter(iter);
-        model.append(out iter);
-        model.@set(iter, 0, _("Level one"), 1, Level.WEAK);
-        if (Prefs.instance.level[PlayerID.PLAYER2] == Level.WEAK)
-            combobox.set_active_iter(iter);
-        model.append(out iter);
-        model.@set(iter, 0, _("Level two"), 1, Level.MEDIUM);
-        if (Prefs.instance.level[PlayerID.PLAYER2] == Level.MEDIUM)
-            combobox.set_active_iter(iter);
-        model.append(out iter);
-        model.@set(iter, 0, _("Level three"), 1, Level.STRONG);
-        if (Prefs.instance.level[PlayerID.PLAYER2] == Level.STRONG)
-            combobox.set_active_iter(iter);
-
-        combobox.changed.connect(on_select_opponent);
-        grid.attach(combobox, 1, 0, 1, 1);
-
-        /* keyboard tab */
-        label = new Label.with_mnemonic(_("Keyboard Controls"));
-
-        controls_list = new GamesControlsList(Prefs.instance.settings);
-        controls_list.add_controls("key-left",  _("Move left"),     DEFAULT_KEY_LEFT,
-                                   "key-right", _("Move right"),    DEFAULT_KEY_RIGHT,
-                                   "key-drop",  _("Drop marble"),   DEFAULT_KEY_DROP);
-        controls_list.border_width = 12;
-        notebook.append_page(controls_list, label);
+        get_content_area ().border_width = 0;   // defaults on 2
+
+        GamesControlsList controls_list = new GamesControlsList (Prefs.instance.settings);
+        controls_list.shadow_type = ShadowType.NONE;
+        controls_list.add_controls ("key-left",     _("Move left"),     DEFAULT_KEY_LEFT,
+                                    "key-right",    _("Move right"),    DEFAULT_KEY_RIGHT,
+                                    "key-drop",     _("Drop marble"),   DEFAULT_KEY_DROP);
+        get_content_area ().pack_start (controls_list, true, true, 0);
     }
 
-    protected override bool delete_event(Gdk.EventAny event) {  // TODO use hide_on_delete (Gtk3) or 
hide-on-close (Gtk4) 2/2
-        hide();
+    protected override bool delete_event (Gdk.EventAny event)   // TODO use hide_on_delete (Gtk3) or 
hide-on-close (Gtk4) 2/2
+    {
+        hide ();
         return true;
     }
-
-    private inline void on_select_opponent(ComboBox combobox) {
-        FourInARow app = (FourInARow)application;
-        TreeIter iter;
-        int iter_value;
-
-        combobox.get_active_iter(out iter);
-        combobox.get_model().@get(iter, 1, out iter_value);
-
-        Prefs.instance.level[PlayerID.PLAYER2] = (Level)iter_value;
-        Prefs.instance.settings.set_int("opponent", iter_value);
-        app.who_starts = PlayerID.PLAYER2; /* This gets reversed in game_reset. */
-        app.game_reset();
-    }
 }
diff --git a/src/prefs.vala b/src/prefs.vala
index 6fbfd01..656e024 100644
--- a/src/prefs.vala
+++ b/src/prefs.vala
@@ -23,8 +23,6 @@ private class Prefs : Object
     internal Settings settings;
 
     [CCode (notify = true)]  internal int theme_id       { internal get; internal set; }
-
-    internal Level level [2];
     [CCode (notify = false)] internal int keypress_drop  { internal get; internal set; }
     [CCode (notify = false)] internal int keypress_right { internal get; internal set; }
     [CCode (notify = false)] internal int keypress_left  { internal get; internal set; }
@@ -37,22 +35,10 @@ private class Prefs : Object
     internal Prefs ()
     {
         settings = new GLib.Settings ("org.gnome.Four-in-a-row");
-        level [PlayerID.PLAYER1] = Level.HUMAN; /* Human. Always human. */
-        level [PlayerID.PLAYER2] = (Level) settings.get_int ("opponent");
 
         settings.bind ("theme-id",  this, "theme-id",       SettingsBindFlags.DEFAULT);
         settings.bind ("key-drop",  this, "keypress_drop",  SettingsBindFlags.DEFAULT);
         settings.bind ("key-right", this, "keypress_right", SettingsBindFlags.DEFAULT);
         settings.bind ("key-left",  this, "keypress_left",  SettingsBindFlags.DEFAULT);
     }
-
-    internal int get_n_human_players ()
-    {
-        if (level [PlayerID.PLAYER1] != Level.HUMAN && level [PlayerID.PLAYER2] != Level.HUMAN)
-            assert_not_reached ();
-        if (level [PlayerID.PLAYER1] != Level.HUMAN || level [PlayerID.PLAYER2] != Level.HUMAN)
-            return 1;
-        else
-            return 2;
-    }
 }
diff --git a/src/scorebox.vala b/src/scorebox.vala
index 2b18934..b316f01 100644
--- a/src/scorebox.vala
+++ b/src/scorebox.vala
@@ -89,24 +89,28 @@ private class Scorebox : Dialog {
      *
      * updates the scorebox with the latest scores
      */
-    internal void update(int[] scores) {
-        if (Prefs.instance.get_n_human_players() == 1) {
-            if (Prefs.instance.level[PlayerID.PLAYER1] == Level.HUMAN) {    // FIXME shouldn't it be 
Player1&Player2?
-
-
-
-                label_name[PlayerID.PLAYER1].set_text(_("You:"));
-                label_name[PlayerID.PLAYER2].label = _("Me:");
+    internal void update(int[] scores, bool one_player_game) {
+        if (one_player_game) {
+            if (scores[PlayerID.PLAYER1] >= scores[PlayerID.PLAYER2]) {
+                label_name[0].set_text(_("You:"));
+                label_name[1].set_text(_("Me:"));
+
+                label_score[0].label = scores[PlayerID.PLAYER1].to_string();
+                label_score[1].label = scores[PlayerID.PLAYER2].to_string();
             } else {
-                label_name[PlayerID.PLAYER2].set_text(_("You:"));
-                label_name[PlayerID.PLAYER1].label = _("Me:");
+                label_name[0].set_text(_("Me:"));
+                label_name[1].set_text(_("You:"));
+
+                label_score[0].label = scores[1].to_string();
+                label_score[1].label = scores[0].to_string();
             }
         } else {
-            label_name[PlayerID.PLAYER1].label = theme_get_player(PlayerID.PLAYER1);
-            label_name[PlayerID.PLAYER2].label = theme_get_player(PlayerID.PLAYER2);
+            label_name[0].label = theme_get_player(PlayerID.PLAYER1);    // FIXME missing ":" at end
+            label_name[1].label = theme_get_player(PlayerID.PLAYER2);    // idem
+
+            label_score[PlayerID.PLAYER1].label = scores[PlayerID.PLAYER1].to_string();
+            label_score[PlayerID.PLAYER2].label = scores[PlayerID.PLAYER2].to_string();
         }
-        label_score[PlayerID.PLAYER1].label = scores[PlayerID.PLAYER1].to_string();
-        label_score[PlayerID.PLAYER2].label = scores[PlayerID.PLAYER2].to_string();
         label_score[PlayerID.NOBODY].label  = scores[PlayerID.NOBODY].to_string();
 
     }


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