[gnome-tetravex/arnaudb/new-ux: 1/3] Improve end-of-game UX.



commit a86a2b4207d519d12f43f78e52fed8e579cc380e
Author: Arnaud Bonatti <arnaud bonatti gmail com>
Date:   Sun Sep 22 20:21:18 2019 +0200

    Improve end-of-game UX.

 po/POTFILES.in                   |   2 +
 po/POTFILES.skip                 |   1 +
 src/gnome-tetravex.gresource.xml |   5 +-
 src/gnome-tetravex.vala          |  87 ++++++++++++-------
 src/history.vala                 | 175 +++++++++++++++++++++++++++++++++------
 src/meson.build                  |  19 +++--
 src/puzzle-view.vala             |  69 ++++++++++++---
 src/score-dialog.vala            |  44 ++--------
 src/score-overlay-entry.ui       |  34 ++++++++
 src/score-overlay.ui             |  68 +++++++++++++++
 src/score-overlay.vala           | 164 ++++++++++++++++++++++++++++++++++++
 src/tetravex.css                 |  38 +++++++++
 src/theme.vala                   |  22 +++--
 13 files changed, 607 insertions(+), 121 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 634ef6a..c3c0cbd 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -10,4 +10,6 @@ src/help-overlay.ui
 src/puzzle.vala
 src/puzzle-view.vala
 src/score-dialog.vala
+src/score-overlay.vala
+src/score-overlay.ui
 src/theme.vala
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 61008a4..4f4527e 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -2,4 +2,5 @@ src/gnome-tetravex.c
 src/puzzle.c
 src/puzzle-view.c
 src/score-dialog.c
+src/score-overlay.c
 src/theme.c
diff --git a/src/gnome-tetravex.gresource.xml b/src/gnome-tetravex.gresource.xml
index e405d53..4ee78ee 100644
--- a/src/gnome-tetravex.gresource.xml
+++ b/src/gnome-tetravex.gresource.xml
@@ -1,8 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/Tetravex">
-    <file preprocess="xml-stripblanks">gnome-tetravex.ui</file>
     <file preprocess="xml-stripblanks">app-menu.ui</file>
+    <file preprocess="xml-stripblanks">gnome-tetravex.ui</file>
+    <file preprocess="xml-stripblanks">score-overlay.ui</file>
+    <file preprocess="xml-stripblanks">score-overlay-entry.ui</file>
+    <file>tetravex.css</file>
   </gresource>
   <gresource prefix="/org/gnome/Tetravex/gtk">
     <file preprocess="xml-stripblanks">help-overlay.ui</file>
diff --git a/src/gnome-tetravex.vala b/src/gnome-tetravex.vala
index 34e21e5..1fec2c0 100644
--- a/src/gnome-tetravex.vala
+++ b/src/gnome-tetravex.vala
@@ -45,6 +45,8 @@ private class Tetravex : Gtk.Application
     private SimpleAction solve_action;
     private SimpleAction finish_action;
 
+    private ScoreOverlay score_overlay;
+
     private const OptionEntry [] option_entries =
     {
         /* Translators: command-line option description, see 'gnome-tetravex --help' */
@@ -123,6 +125,12 @@ private class Tetravex : Gtk.Application
 
         history = new History (Path.build_filename (Environment.get_user_data_dir (), "gnome-tetravex", 
"history"));
 
+        CssProvider css_provider = new CssProvider ();
+        css_provider.load_from_resource ("/org/gnome/Tetravex/tetravex.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);
+
         window = (ApplicationWindow) builder.get_object ("gnome-tetravex-window");
         this.add_window (window);
         window.key_press_event.connect (on_key_press_event);
@@ -172,7 +180,22 @@ private class Tetravex : Gtk.Application
         view.hexpand = true;
         view.vexpand = true;
         view.button_release_event.connect (view_button_release_event);
-        grid.attach (view, 0, 0, 3, 1);
+
+        Overlay overlay = new Overlay ();
+        overlay.add (view);
+        overlay.show ();
+
+        score_overlay = new ScoreOverlay ();
+        overlay.add_overlay (score_overlay);
+        overlay.set_overlay_pass_through (score_overlay, true);
+
+        view.bind_property ("board-height",     score_overlay,  "board-height",     BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+        view.bind_property ("x-offset-right",   score_overlay,  "margin-left",      BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+        view.bind_property ("right-margin",     score_overlay,  "margin-right",     BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+        view.bind_property ("y-offset",         score_overlay,  "margin-top",       BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+        view.bind_property ("y-offset",         score_overlay,  "margin-bottom",    BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+
+        grid.attach (overlay, 0, 0, 3, 1);
 
         settings.bind ("mouse-use-extra-buttons",   view,
                        "mouse-use-extra-buttons",   SettingsBindFlags.GET | 
SettingsBindFlags.NO_SENSITIVITY);
@@ -358,6 +381,7 @@ private class Tetravex : Gtk.Application
         solve_action.set_enabled (true);
         finish_action.set_enabled (false);
         new_game_solve_stack.set_visible_child_name ("solve");
+        score_overlay.hide ();
 
         if (puzzle_init_done)
             SignalHandler.disconnect_by_func (puzzle, null, this);
@@ -428,35 +452,22 @@ private class Tetravex : Gtk.Application
     {
         DateTime date = new DateTime.now_local ();
         uint duration = (uint) (puzzle.elapsed + 0.5);
-        HistoryEntry entry = new HistoryEntry (date, puzzle.size, duration);
-        history.add (entry);
-        history.save ();
-
-        int score_dialog_action = show_scores (entry, true);
-        if (score_dialog_action == ResponseType.CLOSE)
-            window.destroy ();
-        else if (score_dialog_action == ResponseType.OK)
-            new_game ();
-        else if (score_dialog_action != ResponseType.REJECT)
-            new_game_solve_stack.set_visible_child_name ("new-game");
-    }
-
-    private bool scores_dialog_visible = false; // security for #5
-    private int show_scores (HistoryEntry? selected_entry = null, bool show_quit = false)
-    {
-        if (scores_dialog_visible)
-            return ResponseType.REJECT;
-
-        scores_dialog_visible = true;
-        ScoreDialog dialog = new ScoreDialog (history, puzzle.size, selected_entry, show_quit);
-        dialog.modal = true;
-        dialog.transient_for = window;
-
-        int result = dialog.run ();
-        dialog.destroy ();
-        scores_dialog_visible = false;
-
-        return result;
+        last_history_entry = new HistoryEntry (date, puzzle.size, duration);
+
+        HistoryEntry? other_score_0;
+        HistoryEntry? other_score_1;
+        HistoryEntry? other_score_2;
+        uint position = history.get_place ((!) last_history_entry,
+                                           puzzle.size,
+                                       out other_score_0,
+                                       out other_score_1,
+                                       out other_score_2);
+        score_overlay.set_score (puzzle.size, position, (!) last_history_entry, other_score_0, 
other_score_1, other_score_2);
+
+        new_game_solve_stack.set_visible_child_name ("new-game");
+        view.hide_right_sockets ();
+
+        score_overlay.show ();
     }
 
     private void new_game_cb ()
@@ -486,9 +497,21 @@ private class Tetravex : Gtk.Application
         new_game ();
     }
 
-    private void scores_cb ()
+    private HistoryEntry? last_history_entry = null;
+    private bool scores_dialog_visible = false; // security for #5
+    private void scores_cb (/* SimpleAction action, Variant? variant */)
     {
-        show_scores ();
+        if (scores_dialog_visible)
+            return;
+
+        scores_dialog_visible = true;
+        ScoreDialog dialog = new ScoreDialog (history, puzzle.size, puzzle.is_solved ? last_history_entry : 
null);
+        dialog.modal = true;
+        dialog.transient_for = window;
+
+        dialog.run ();
+        dialog.destroy ();
+        scores_dialog_visible = false;
     }
 
     private bool view_button_release_event (Widget widget, Gdk.EventButton event)
diff --git a/src/history.vala b/src/history.vala
index 822df20..aab7d41 100644
--- a/src/history.vala
+++ b/src/history.vala
@@ -14,21 +14,108 @@ private class History : Object
     [CCode (notify = false)] public string filename { private get; protected construct; }
     internal List<HistoryEntry> entries = new List<HistoryEntry> ();
 
+    /*\
+    * * getting
+    \*/
+
     internal signal void entry_added (HistoryEntry entry);
 
-    internal History (string filename)
+    internal uint get_place (HistoryEntry   entry,
+                             uint8          puzzle_size,
+                         out HistoryEntry?  other_entry_0,
+                         out HistoryEntry?  other_entry_1,
+                         out HistoryEntry?  other_entry_2)
     {
-        Object (filename: filename);
-        load ();
+        entries.insert_sorted (entry, HistoryEntry.compare_entries);
+        entry_added (entry);
+        save ();
+
+        unowned List<HistoryEntry> entry_item = entries.find (entry);
+        unowned List<HistoryEntry> best_time_item;
+        uint best_position = get_best_time_position (entry_item, out best_time_item);
+        uint position = entries.position (entry_item) - best_position + 1;
+        switch (position)
+        {
+            case 1:
+                unowned List<HistoryEntry>? tmp_item = entry_item.next;
+                if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+                {
+                    other_entry_0 = null;
+                    other_entry_1 = null;
+                    other_entry_2 = null;
+                    break;
+                }
+                other_entry_0 = ((!) tmp_item).data;
+                tmp_item = ((!) tmp_item).next;
+                if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+                {
+                    other_entry_1 = null;
+                    other_entry_2 = null;
+                    break;
+                }
+                other_entry_1 = ((!) tmp_item).data;
+                tmp_item = ((!) tmp_item).next;
+                if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+                    other_entry_2 = null;
+                else
+                    other_entry_2 = ((!) tmp_item).data;
+                break;
+
+            case 2:
+                other_entry_0 = best_time_item.data;
+                unowned List<HistoryEntry>? tmp_item = entry_item.next;
+                if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+                {
+                    other_entry_1 = null;
+                    other_entry_2 = null;
+                    break;
+                }
+                other_entry_1 = ((!) tmp_item).data;
+                tmp_item = ((!) tmp_item).next;
+                if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+                    other_entry_2 = null;
+                else
+                    other_entry_2 = ((!) tmp_item).data;
+                break;
+
+            default:
+                other_entry_0 = best_time_item.data;
+                other_entry_1 = entry_item.prev.data;
+                unowned List<HistoryEntry>? next_entry_item = entry_item.next;
+                if (next_entry_item == null || ((!) next_entry_item).data.size != puzzle_size)
+                    other_entry_2 = null;
+                else
+                    other_entry_2 = ((!) next_entry_item).data;
+                break;
+        }
+        return position;
     }
 
-    internal void add (HistoryEntry entry)
+    private uint get_best_time_position (List<HistoryEntry> entry_item, out unowned List<HistoryEntry> 
best_time_item)
     {
-        entries.append (entry);
-        entry_added (entry);
+        uint8 puzzle_size = entry_item.data.size;
+        best_time_item = entries.first ();
+        if (puzzle_size == 2 || entry_item == best_time_item)
+            return 0;
+
+        best_time_item = entry_item;
+        do { best_time_item = best_time_item.prev; }
+        while (best_time_item != entries && best_time_item.data.size == puzzle_size);
+        best_time_item = best_time_item.next;
+        return entries.position (best_time_item);
+    }
+
+    /*\
+    * * loading
+    \*/
+
+    internal History (string filename)
+    {
+        Object (filename: filename);
+        load ();
     }
 
-    internal void load ()
+    private inline void load ()
     {
         string contents = "";
         try
@@ -57,11 +144,34 @@ private class History : Object
 
             // FIXME use try_parse
 
-            add (new HistoryEntry ((!) date, size, duration));
+            entries.prepend (new HistoryEntry ((!) date, size, duration));
         }
+        entries.sort (HistoryEntry.compare_entries);
+    }
+
+    private inline DateTime? parse_date (string date)
+    {
+        if (date.length < 19 || date[4] != '-' || date[7] != '-' || date[10] != 'T' || date[13] != ':' || 
date[16] != ':')
+            return null;
+
+        // FIXME use try_parse
+
+        int year        = int.parse (date.substring (0, 4));
+        int month       = int.parse (date.substring (5, 2));
+        int day         = int.parse (date.substring (8, 2));
+        int hour        = int.parse (date.substring (11, 2));
+        int minute      = int.parse (date.substring (14, 2));
+        int seconds     = int.parse (date.substring (17, 2));
+        string timezone = date.substring (19);
+
+        return new DateTime (new TimeZone (timezone), year, month, day, hour, minute, seconds);
     }
 
-    internal void save ()
+    /*\
+    * * saving
+    \*/
+
+    private inline void save ()
     {
         string contents = "";
 
@@ -81,24 +191,6 @@ private class History : Object
             warning ("Failed to save history: %s", e.message);
         }
     }
-
-    private DateTime? parse_date (string date)
-    {
-        if (date.length < 19 || date[4] != '-' || date[7] != '-' || date[10] != 'T' || date[13] != ':' || 
date[16] != ':')
-            return null;
-
-        // FIXME use try_parse
-
-        int year        = int.parse (date.substring (0, 4));
-        int month       = int.parse (date.substring (5, 2));
-        int day         = int.parse (date.substring (8, 2));
-        int hour        = int.parse (date.substring (11, 2));
-        int minute      = int.parse (date.substring (14, 2));
-        int seconds     = int.parse (date.substring (17, 2));
-        string timezone = date.substring (19);
-
-        return new DateTime (new TimeZone (timezone), year, month, day, hour, minute, seconds);
-    }
 }
 
 private class HistoryEntry : Object // TODO make struct? needs using HistoryEntry? for the List...
@@ -111,4 +203,33 @@ private class HistoryEntry : Object // TODO make struct? needs using HistoryEntr
     {
         Object (date: date, size: size, duration: duration);
     }
+
+    /*\
+    * * utilities
+    \*/
+
+    internal static string get_duration_string (uint duration)
+    {
+        if (duration >= 3600)
+            /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has taken 
one hour or more; the %u are replaced by the hours (h), minutes (m) and seconds (s); as an example, you might 
want to use "%u:%.2u:%.2u", that is quite international (the ".2" meaning "two digits, padding with 0") */
+            return _("%uh %um %us").printf (duration / 3600, (duration / 60) % 60, duration % 60);
+
+        if (duration >= 60)
+            /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has taken 
between one minute and one hour; the %u are replaced by the minutes (m) and seconds (s); as an example, you 
might want to use "%.2u:%.2u", that is quite international (the ".2" meaning "two digits, padding with 0") */
+            return _("%um %us").printf (duration / 60, duration % 60);
+
+        else
+            /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has taken 
less than one minute; the %u is replaced by the number of seconds (s) it has taken; as an example, you might 
want to use "00:%.2u", that is quite international (the ".2" meaning "two digits, padding with 0") */
+            return _("%us").printf (duration);
+    }
+
+    internal static int compare_entries (HistoryEntry a, HistoryEntry b)
+    {
+        if (a.size != b.size)
+            return (int) a.size - (int) b.size;
+        if (a.duration != b.duration)
+            return (int) a.duration - (int) b.duration;
+        else
+            return a.date.compare (b.date);
+    }
 }
diff --git a/src/meson.build b/src/meson.build
index 5aaf72b..e9363bf 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -2,14 +2,19 @@ resources = gnome.compile_resources ('resources', 'gnome-tetravex.gresource.xml'
                                      source_dir: '.',
                                      c_name: 'resources')
 
+sources = files (
+    'config.vapi',
+    'gnome-tetravex.vala',
+    'history.vala',
+    'puzzle.vala',
+    'puzzle-view.vala',
+    'score-dialog.vala',
+    'score-overlay.vala',
+    'theme.vala'
+)
+
 gnome_tetravex = executable ('gnome-tetravex',
-                             [ 'config.vapi',
-                               'gnome-tetravex.vala',
-                               'history.vala',
-                               'puzzle.vala',
-                               'puzzle-view.vala',
-                               'score-dialog.vala',
-                               'theme.vala'] + resources,
+                             sources + resources,
                              dependencies: [ glib_dep,
                                              gtk_dep,
                                              libm_dep ],
diff --git a/src/puzzle-view.vala b/src/puzzle-view.vala
index e57e42b..017d26a 100644
--- a/src/puzzle-view.vala
+++ b/src/puzzle-view.vala
@@ -62,6 +62,7 @@ private class PuzzleView : Gtk.DrawingArea
             if (puzzle_init_done)
                 SignalHandler.disconnect_by_func ((!) _puzzle, null, this);
 
+            show_right_sockets ();
             _puzzle = value;
             last_selected_tile = null;
             tiles.remove_all ();
@@ -110,8 +111,12 @@ private class PuzzleView : Gtk.DrawingArea
     private uint animation_timeout = 0;
 
     /* Set in configure event */
+    [CCode (notify = true)] internal uint board_height   { internal get; private set; default = 0; }
+    [CCode (notify = true)] internal uint x_offset_right { internal get; private set; default = 0; }
+    [CCode (notify = true)] internal uint y_offset       { internal get; private set; default = 0; }
+    [CCode (notify = true)] internal uint right_margin   { internal get; private set; default = 0; }
     private uint x_offset = 0;
-    private uint y_offset = 0;
+    private uint gap_offset = 0;
     private uint tilesize = 0;
     private uint gap = 0;
     private double arrow_x = 0.0;
@@ -264,17 +269,21 @@ private class PuzzleView : Gtk.DrawingArea
             uint width  = (uint) (allocated_width  / (2 * puzzle.size + 1.5));
             uint height = (uint) (allocated_height / (puzzle.size + 1));
             tilesize = uint.min (width, height);
+            board_height = puzzle.size * tilesize;
             gap = tilesize / 2;
-            x_offset = (allocated_width  - 2 * puzzle.size * tilesize - gap) / 2;
-            y_offset = (allocated_height -     puzzle.size * tilesize      ) / 2;
+            x_offset = (allocated_width  - 2 * board_height - gap) / 2;
+            y_offset = (allocated_height -     board_height      ) / 2;
+            gap_offset = x_offset + board_height;
+            x_offset_right = gap_offset + gap;
+            right_margin = allocated_width - x_offset_right - board_height;
 
             board_x_maxi = allocated_width  - (int) tilesize;
             board_y_maxi = allocated_height - (int) tilesize;
 
-            snap_distance = (tilesize * puzzle.size) / 40.0;
+            snap_distance = board_height / 40.0;
 
-            arrow_x = x_offset + puzzle.size * tilesize + gap * 0.25;
-            arrow_y = y_offset + puzzle.size * tilesize * 0.5;
+            arrow_x = gap_offset + gap * 0.25;
+            arrow_y = y_offset + board_height * 0.5;
 
             /* Precalculate sockets positions */
             for (uint y = 0; y < puzzle.size; y++)
@@ -316,12 +325,12 @@ private class PuzzleView : Gtk.DrawingArea
         /* Draw arrow */
         context.save ();
         context.translate (arrow_x, arrow_y);
-        theme.draw_arrow (context, tilesize, gap);
+        theme.draw_arrow (context, tilesize, gap, socket_animation_level);
         context.restore ();
 
         /* Draw sockets */
         for (uint y = 0; y < puzzle.size; y++)
-            for (uint x = 0; x < puzzle.size * 2; x++)
+            for (uint x = 0; x < puzzle.size; x++)
             {
                 context.save ();
                 context.translate (sockets_xs [x, y], sockets_ys [x, y]);
@@ -329,6 +338,15 @@ private class PuzzleView : Gtk.DrawingArea
                 context.restore ();
             }
 
+        for (uint y = 0; y < puzzle.size; y++)
+            for (uint x = puzzle.size; x < puzzle.size * 2; x++)
+            {
+                context.save ();
+                context.translate (sockets_xs [x, y], sockets_ys [x, y]);
+                theme.draw_socket (context, tilesize, socket_animation_level);
+                context.restore ();
+            }
+
         /* Draw tiles */
         SList<TileImage> moving_tiles = new SList<TileImage> ();
         HashTableIter<Tile, TileImage> iter = HashTableIter<Tile, TileImage> (tiles);
@@ -430,7 +448,7 @@ private class PuzzleView : Gtk.DrawingArea
 
     private bool on_right_half (double x)
     {
-        return x > x_offset + tilesize * puzzle.size + gap * 0.5;
+        return x > x_offset_right - gap * 0.5;
     }
 
     private void drop_tile (double x, double y)
@@ -449,7 +467,7 @@ private class PuzzleView : Gtk.DrawingArea
         int16 tile_x;
         if (on_right_half (x))
         {
-            tile_x = (int16) puzzle.size + (int16) Math.floor ((x - (x_offset + puzzle.size * tilesize + 
gap)) / tilesize);
+            tile_x = (int16) puzzle.size + (int16) Math.floor ((x - x_offset_right) / tilesize);
             tile_x = tile_x.clamp ((int16) puzzle.size, 2 * (int16) puzzle.size - 1);
         }
         else
@@ -644,6 +662,10 @@ private class PuzzleView : Gtk.DrawingArea
         tile_selected = false;
     }
 
+    /*\
+    * * history proxies
+    \*/
+
     internal void undo ()
     {
         last_selected_tile = null;
@@ -655,4 +677,31 @@ private class PuzzleView : Gtk.DrawingArea
         last_selected_tile = null;
         puzzle.redo ();
     }
+
+    /*\
+    * * final animation
+    \*/
+
+    private uint8 socket_animation_level = 0;
+
+    internal void hide_right_sockets ()
+    {
+        Timeout.add (75, () => {
+                socket_animation_level++;
+                queue_draw_area ((int) gap_offset,
+                                 (int) y_offset,
+                                 (int) (board_height + gap),
+                                 (int) board_height);
+
+                if (socket_animation_level < 17)
+                    return Source.CONTINUE;
+                else
+                    return Source.REMOVE;
+            });
+    }
+
+    private inline void show_right_sockets ()
+    {
+        socket_animation_level = 0;
+    }
 }
diff --git a/src/score-dialog.vala b/src/score-dialog.vala
index 45350a4..49560ae 100644
--- a/src/score-dialog.vala
+++ b/src/score-dialog.vala
@@ -20,26 +20,14 @@ private class ScoreDialog : Dialog
     private ComboBox size_combo;
     private TreeView scores;
 
-    internal ScoreDialog (History history, uint8 size, HistoryEntry? selected_entry = null, bool show_quit = 
false)
+    internal ScoreDialog (History history, uint8 size, HistoryEntry? selected_entry = null)
     {
+        Object (use_header_bar: /* true */ 1);
+
         this.history = history;
         history.entry_added.connect (entry_added_cb);
         this.selected_entry = selected_entry;
 
-        if (show_quit)
-        {
-            /* Translators: label of a button of the Scores dialog, as it is displayed at the end of a game; 
quits the application */
-            add_button (_("Quit"), ResponseType.CLOSE);
-
-
-            /* Translators: label of a button of the Scores dialog, as it is displayed at the end of a game; 
starts a new game */
-            add_button (_("New Game"), ResponseType.OK);
-        }
-        else
-        {
-            /* Translators: label of a button of the Scores dialog, as it is displayed when called from the 
hamburger menu; closes the dialog */
-            add_button (_("OK"), ResponseType.DELETE_EVENT);
-        }
         set_size_request (200, 300);
 
         Box vbox = new Box (Orientation.VERTICAL, 5);
@@ -88,7 +76,7 @@ private class ScoreDialog : Dialog
         scroll.add (scores);
 
         List<unowned HistoryEntry> entries = history.entries.copy ();
-        entries.sort (compare_entries);
+        entries.sort (HistoryEntry.compare_entries);
         foreach (HistoryEntry entry in entries)
             entry_added_cb (entry);
 
@@ -102,7 +90,7 @@ private class ScoreDialog : Dialog
         score_model.clear ();
 
         List<unowned HistoryEntry> entries = history.entries.copy ();
-        entries.sort (compare_entries);
+        entries.sort (HistoryEntry.compare_entries);
 
         foreach (HistoryEntry entry in entries)
         {
@@ -112,18 +100,7 @@ private class ScoreDialog : Dialog
             /* "the preferred date representation for the current locale without the time" */
             string date_label = entry.date.format ("%x");
 
-            string time_label;
-            if (entry.duration >= 3600)
-                /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has 
taken one hour or more; the %u are replaced by the hours (h), minutes (m) and seconds (s); as an example, you 
might want to use "%u:%.2u:%.2u", that is quite international (the ".2" meaning "two digits, padding with 0") 
*/
-                time_label = _("%uh %um %us").printf (entry.duration / 3600, (entry.duration / 60) % 60, 
entry.duration % 60);
-
-            else if (entry.duration >= 60)
-                /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has 
taken between one minute and one hour; the %u are replaced by the minutes (m) and seconds (s); as an example, 
you might want to use "%.2u:%.2u", that is quite international (the ".2" meaning "two digits, padding with 
0") */
-                time_label = _("%um %us").printf (entry.duration / 60, entry.duration % 60);
-
-            else
-                /* Translators: that is the duration of a game, as seen in the Scores dialog, if game has 
taken less than one minute; the %u is replaced by the number of seconds (s) it has taken; as an example, you 
might want to use "00:%.2u", that is quite international (the ".2" meaning "two digits, padding with 0") */
-                time_label = _("%us").printf (entry.duration);
+            string time_label = HistoryEntry.get_duration_string (entry.duration);
 
             int weight = Pango.Weight.NORMAL;
             if (entry == selected_entry)
@@ -149,15 +126,6 @@ private class ScoreDialog : Dialog
         }
     }
 
-    private static int compare_entries (HistoryEntry a, HistoryEntry b)
-    {
-        if (a.size != b.size)
-            return (int) a.size - (int) b.size;
-        if (a.duration != b.duration)
-            return (int) a.duration - (int) b.duration;
-        return a.date.compare (b.date);
-    }
-
     private void size_changed_cb (ComboBox combo)
     {
         TreeIter iter;
diff --git a/src/score-overlay-entry.ui b/src/score-overlay-entry.ui
new file mode 100644
index 0000000..780e5ee
--- /dev/null
+++ b/src/score-overlay-entry.ui
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="ScoreOverlayEntry" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="width-request">160</property> <!-- two tiles of 80 max -->
+    <property name="halign">center</property>
+    <child>
+      <object class="GtkLabel" id="place_label">
+        <property name="visible">True</property>
+        <property name="hexpand">True</property>
+        <property name="halign">start</property>
+        <style>
+          <class name="bold-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="top-attach">2</property>
+        <property name="left-attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="value_label">
+        <property name="visible">True</property>
+        <property name="hexpand">True</property>
+        <property name="halign">end</property>
+      </object>
+      <packing>
+        <property name="top-attach">2</property>
+        <property name="left-attach">2</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/score-overlay.ui b/src/score-overlay.ui
new file mode 100644
index 0000000..72bd2fd
--- /dev/null
+++ b/src/score-overlay.ui
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="ScoreOverlay" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="valign">center</property>
+    <property name="orientation">vertical</property>
+    <property name="row-homogeneous">True</property>
+    <property name="column-homogeneous">True</property>
+    <property name="column-spacing">4</property>
+    <style>
+      <class name="score-overlay"/>
+    </style>
+    <child>
+      <object class="GtkLabel">
+        <property name="visible">True</property>
+        <property name="hexpand">True</property>
+        <property name="label" translatable="yes">Congratulations!</property>
+        <style>
+          <class name="score-title"/>
+        </style>
+      </object>
+      <packing>
+        <property name="top-attach">0</property>
+        <property name="left-attach">0</property>
+        <property name="height">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="ScoreOverlayEntry" id="score_0"/>
+      <packing>
+        <property name="top-attach">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="ScoreOverlayEntry" id="score_1"/>
+      <packing>
+        <property name="top-attach">3</property>
+      </packing>
+    </child>
+    <child>
+      <object class="ScoreOverlayEntry" id="score_2"/>
+      <packing>
+        <property name="top-attach">4</property>
+      </packing>
+    </child>
+    <child>
+      <object class="ScoreOverlayEntry" id="score_3"/>
+      <packing>
+        <property name="top-attach">5</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton">
+        <property name="visible">True</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <property name="label" translatable="yes">Show scores</property>
+        <property name="action-name">app.scores</property>
+      </object>
+      <packing>
+        <property name="top-attach">6</property>
+        <property name="left-attach">0</property>
+        <property name="height">2</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/score-overlay.vala b/src/score-overlay.vala
new file mode 100644
index 0000000..7ab9b6a
--- /dev/null
+++ b/src/score-overlay.vala
@@ -0,0 +1,164 @@
+/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * Copyright (C) 2019 Arnaud Bonatti
+ *
+ * This program 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 2 of the License, or (at your option) any later
+ * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Tetravex/score-overlay.ui")]
+private class ScoreOverlay : Grid
+{
+    [CCode (notify = true)] internal uint board_height
+    {
+        internal set
+        {
+            if (value < 250 && value != 0)
+                get_style_context ().add_class ("small-window");
+            else
+                get_style_context ().remove_class ("small-window");
+        }
+    }
+
+    /*\
+    * * updating labels
+    \*/
+
+    [GtkChild] private ScoreOverlayEntry score_0;
+    [GtkChild] private ScoreOverlayEntry score_1;
+    [GtkChild] private ScoreOverlayEntry score_2;
+    [GtkChild] private ScoreOverlayEntry score_3;
+
+    internal void set_score (uint8          puzzle_size,
+                             uint /* [1[ */ position,
+                             HistoryEntry   entry,
+                             HistoryEntry?  other_entry_0,
+                             HistoryEntry?  other_entry_1,
+                             HistoryEntry?  other_entry_2)
+    {
+        switch (position)
+        {
+            case 1:
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has been the fastest for a puzzle of this size; introduces the game time */
+                score_0.set_place_label (_("New best time!"));
+                score_0.set_value_label (HistoryEntry.get_duration_string (entry.duration), true);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has been the fastest for a puzzle of this size; introduces the old best time */
+                score_1.set_place_label (_("Second:"));
+                if (other_entry_0 != null)
+                    score_1.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_0).duration));
+                else
+                    score_1.set_value_label (null);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has been the fastest for a puzzle of this size; introduces the old second best time */
+                score_2.set_place_label (_("Third:"));
+                if (other_entry_1 != null)
+                    score_2.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_1).duration));
+                else
+                    score_2.set_value_label (null);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has been the fastest for a puzzle of this size; introduces the old third best time */
+                score_3.set_place_label (_("Out of podium:"));
+                if (other_entry_2 != null)
+                    score_3.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_2).duration));
+                else
+                    score_3.set_value_label (null);
+                break;
+
+            case 2:
+                if (other_entry_0 == null)
+                    assert_not_reached ();
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has made the second best time for a puzzle of this size; introduces the best time ever */
+                score_0.set_place_label (_("Best time:"));
+                score_0.set_value_label (HistoryEntry.get_duration_string (((!) other_entry_0).duration));
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has made the second best time for a puzzle of this size; introduces the game time */
+                score_1.set_place_label (_("Your time:"));
+                score_1.set_value_label (HistoryEntry.get_duration_string (entry.duration), true);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has made the second best time for a puzzle of this size; introduces the old second best time */
+                score_2.set_place_label (_("Third:"));
+                if (other_entry_1 != null)
+                    score_2.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_1).duration));
+                else
+                    score_2.set_value_label (null);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has made the second best time for a puzzle of this size; introduces the old third best time */
+                score_3.set_place_label (_("Out of podium:"));
+                if (other_entry_2 != null)
+                    score_3.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_2).duration));
+                else
+                    score_3.set_value_label (null);
+                break;
+
+            default:
+                if (other_entry_0 == null || other_entry_1 == null)
+                    assert_not_reached ();
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; introduces the best time 
ever */
+                score_0.set_place_label (_("Best time:"));
+                score_0.set_value_label (HistoryEntry.get_duration_string (((!) other_entry_0).duration));
+
+                if (position == 3)
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; introduces the second best 
time */
+                    score_1.set_place_label (_("Second:"));
+
+                else if (position == 4)
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; introduces the third best 
time */
+                    score_1.set_place_label (_("Third:"));
+
+                else
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; the %u is replaced by the 
rank before the one of the game played */
+                    score_1.set_place_label (_("Place %u:").printf (position - 1));
+                score_1.set_value_label (HistoryEntry.get_duration_string (((!) other_entry_1).duration));
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; introduces the game time */
+                score_2.set_place_label (_("Your time:"));
+                score_2.set_value_label (HistoryEntry.get_duration_string (entry.duration), true);
+
+                /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if 
the player has not made the first or second best time for a puzzle of this size; the %u is replaced by the 
rank after the one of the game played */
+                score_3.set_place_label (_("Place %u:").printf (position + 1));
+                if (other_entry_2 != null)
+                    score_3.set_value_label (HistoryEntry.get_duration_string (((!) 
other_entry_2).duration));
+                else
+                    score_3.set_value_label (null);
+                break;
+        }
+    }
+}
+
+[GtkTemplate (ui = "/org/gnome/Tetravex/score-overlay-entry.ui")]
+private class ScoreOverlayEntry : Grid
+{
+    [GtkChild] private Label place_label;
+    [GtkChild] private Label value_label;
+
+    internal void set_place_label (string label)
+    {
+        place_label.set_label (label);
+    }
+
+    internal void set_value_label (string? label, bool bold_label = false)
+    {
+        if (label != null)
+        {
+            value_label.set_label ((!) label);
+            value_label.get_style_context ().remove_class ("italic-label");
+        }
+        else
+        {
+            /* Translators: text of the score overlay, displayed after a puzzle is complete; appears if the 
player has made one of the worst scores for a game of this size; says that the rank after the one of the game 
is "free", inviting to do worse */
+            value_label.set_label (_("Free!"));
+            value_label.get_style_context ().add_class ("italic-label");
+        }
+
+        if (bold_label)
+            value_label.get_style_context ().add_class ("bold-label");
+        else
+            value_label.get_style_context ().remove_class ("bold-label");
+    }
+}
diff --git a/src/tetravex.css b/src/tetravex.css
new file mode 100644
index 0000000..835d29c
--- /dev/null
+++ b/src/tetravex.css
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 Arnaud Bonatti
+ *
+ * This program 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 2 of the License, or (at your option) any later
+ * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+              .score-title {
+  font-weight:bolder;
+
+  font-size:200%;
+  margin-bottom:1.5rem;
+  margin-top:0.5rem;
+
+  transition:font-size      0.5s,
+             margin-bottom  0.5s,
+             margin-top     0.5s;
+}
+.small-window .score-title {
+  font-size:150%;
+  margin-bottom:1rem;
+  margin-top:0.2rem;
+}
+
+grid.score-overlay button {
+  margin-top:0.5rem;
+}
+
+.bold-label {
+  font-weight:bolder;
+}
+
+.italic-label {
+  font-style:italic;
+}
diff --git a/src/theme.vala b/src/theme.vala
index 769f11a..42b6ba3 100644
--- a/src/theme.vala
+++ b/src/theme.vala
@@ -71,11 +71,13 @@ private class Theme : Object
     * * drawing fixed things
     \*/
 
-    internal void draw_arrow (Cairo.Context context, uint size, uint gap)
+    internal void draw_arrow (Cairo.Context context, uint size, uint gap, uint animation_level = /* 0-16 */ 
0)
     {
         double w = gap * 0.5;
         double h = size * 1.5;
-        uint depth = uint.min ((uint) (size * 0.025), 2);
+        double depth = double.min (size * 0.025, 2.0) - (double) animation_level / 6.0;
+        if (depth <= 0.0)
+            depth = 0.0;
         double dx = 1.4142 * depth;
         double dy = 6.1623 * depth;
 
@@ -84,7 +86,10 @@ private class Theme : Object
         context.line_to (w, h * 0.5);
         context.line_to (w, -h * 0.5);
         context.close_path ();
-        context.set_source_rgba (0, 0, 0, 0.125);
+        if (animation_level == 0)
+            context.set_source_rgba (0, 0, 0, 0.125);
+        else
+            context.set_source_rgba (0, 0, 0, 0.125 * (16.0 - (double) animation_level) / 16.0);
         context.fill ();
 
         /* Arrow highlight */
@@ -108,13 +113,18 @@ private class Theme : Object
         context.fill ();
     }
 
-    internal void draw_socket (Cairo.Context context, uint size)
+    internal void draw_socket (Cairo.Context context, uint size, uint animation_level = /* 0-16 */ 0)
     {
-        uint depth = uint.min ((uint) (size * 0.05), 4);
+        double depth = double.min (size * 0.05, 4.0) - (double) animation_level / 4.0;
+        if (depth <= 0.0)
+            depth = 0.0;
 
         /* Background */
         context.rectangle (depth, depth, size - depth * 2, size - depth * 2);
-        context.set_source_rgba (0, 0, 0, 0.125);
+        if (animation_level == 0)
+            context.set_source_rgba (0, 0, 0, 0.125);
+        else
+            context.set_source_rgba (0, 0, 0, 0.125 * (16.0 - (double) animation_level) / 16.0);
         context.fill ();
 
         /* Shadow */



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