1/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2/*
3   This file is part of GNOME Four-in-a-row.
4
5   Copyright © 2019 Arnaud Bonatti
6
7   GNOME Four-in-a-row is free software: you can redistribute it and/or
8   modify it under the terms of the GNU General Public License as published
9   by the Free Software Foundation, either version 3 of the License, or
10   (at your option) any later version.
11
12   GNOME Four-in-a-row is distributed in the hope that it will be useful,
13   but WITHOUT ANY WARRANTY; without even the implied warranty of
14   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   GNU General Public License for more details.
16
17   You should have received a copy of the GNU General Public License along
18   with GNOME Four-in-a-row.  If not, see <https://www.gnu.org/licenses/>.
19*/
20
21using Gtk;
22
23private interface AdaptativeWidget : Object
24{ /*
25       ╎ extra ╎
26       ╎ thin  ╎
27  ╶╶╶╶ ┏━━━━━━━┳━━━━━━━┳━━━━━──╴
28 extra ┃ PHONE ┃ PHONE ┃ EXTRA
29 flat  ┃ _BOTH ┃ _HZTL ┃ _FLAT
30  ╶╶╶╶ ┣━━━━━━━╋━━━━━━━╋━━━━╾──╴
31       ┃ PHONE ┃       ┃
32       ┃ _VERT ┃       ┃
33       ┣━━━━━━━┫       ┃
34       ┃ EXTRA ┃ QUITE ╿ USUAL
35       ╿ _THIN │ _THIN │ _SIZE
36       ╵       ╵       ╵
37       ╎   quite thin  ╎
38                              */
39
40    internal enum WindowSize {
41        START_SIZE,
42        USUAL_SIZE,
43        QUITE_THIN,
44        PHONE_VERT,
45        PHONE_HZTL,
46        PHONE_BOTH,
47        EXTRA_THIN,
48        EXTRA_FLAT;
49
50        internal static inline bool is_phone_size (WindowSize window_size)
51        {
52            return (window_size == PHONE_BOTH) || (window_size == PHONE_VERT) || (window_size == PHONE_HZTL);
53        }
54
55        internal static inline bool is_extra_thin (WindowSize window_size)
56        {
57            return (window_size == PHONE_BOTH) || (window_size == PHONE_VERT) || (window_size == EXTRA_THIN);
58        }
59
60        internal static inline bool is_extra_flat (WindowSize window_size)
61        {
62            return (window_size == PHONE_BOTH) || (window_size == PHONE_HZTL) || (window_size == EXTRA_FLAT);
63        }
64
65        internal static inline bool is_quite_thin (WindowSize window_size)
66        {
67            return is_extra_thin (window_size) || (window_size == PHONE_HZTL) || (window_size == QUITE_THIN);
68        }
69    }
70
71    internal abstract void set_window_size (WindowSize new_size);
72}
73
74private const int LARGE_WINDOW_SIZE = 1042;
75
76[GtkTemplate (ui = "/org/gnome/Four-in-a-row/ui/adaptative-window.ui")]
77private abstract class AdaptativeWindow : ApplicationWindow
78{
79    [CCode (notify = false)] public string window_title
80    {
81        protected construct
82        {
83            string? _value = value;
84            if (_value == null)
85                assert_not_reached ();
86
87            title = value;
88        }
89    }
90
91    private StyleContext window_style_context;
92    [CCode (notify = false)] public string specific_css_class_or_empty
93    {
94        protected construct
95        {
96            string? _value = value;
97            if (_value == null)
98                assert_not_reached ();
99
100            window_style_context = get_style_context ();
101            if (value != "")
102                window_style_context.add_class (value);
103        }
104    }
105
106    construct
107    {
108        // window_style_context is created by specific_css_class_or_empty
109        window_style_context.add_class ("startup");
110
111        manage_high_contrast ();
112
113        load_window_state ();
114
115        Timeout.add (300, () => { window_style_context.remove_class ("startup"); return Source.REMOVE; });
116    }
117
118    /*\
119    * * callbacks
120    \*/
121
122    [GtkCallback]
123    private bool on_window_state_event (Widget widget, Gdk.EventWindowState event)
124    {
125        if ((event.changed_mask & Gdk.WindowState.MAXIMIZED) != 0)
126            window_is_maximized = (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0;
127
128        /* fullscreen: saved as maximized */
129        bool window_was_fullscreen = window_is_fullscreen;
130        if ((event.changed_mask & Gdk.WindowState.FULLSCREEN) != 0)
131            window_is_fullscreen = (event.new_window_state & Gdk.WindowState.FULLSCREEN) != 0;
132        if (window_was_fullscreen && !window_is_fullscreen)
133            on_unfullscreen ();
134        else if (!window_was_fullscreen && window_is_fullscreen)
135            on_fullscreen ();
136
137        /* tiled: not saved, but should not change saved window size */
138        Gdk.WindowState tiled_state = Gdk.WindowState.TILED
139                                    | Gdk.WindowState.TOP_TILED
140                                    | Gdk.WindowState.BOTTOM_TILED
141                                    | Gdk.WindowState.LEFT_TILED
142                                    | Gdk.WindowState.RIGHT_TILED;
143        if ((event.changed_mask & tiled_state) != 0)
144            window_is_tiled = (event.new_window_state & tiled_state) != 0;
145
146        return false;
147    }
148    protected abstract void on_fullscreen ();
149    protected abstract void on_unfullscreen ();
150
151    [GtkCallback]
152    private void on_size_allocate (Allocation allocation)
153    {
154        int height = allocation.height;
155        int width = allocation.width;
156
157        update_adaptative_children (ref width, ref height);
158        update_window_state ();
159    }
160
161    [GtkCallback]
162    private void on_destroy ()
163    {
164        before_destroy ();
165        save_window_state ();
166        base.destroy ();
167    }
168
169    protected virtual void before_destroy () {}
170
171    /*\
172    * * adaptative stuff
173    \*/
174
175    private AdaptativeWidget.WindowSize window_size = AdaptativeWidget.WindowSize.START_SIZE;
176
177    private List<AdaptativeWidget> adaptative_children = new List<AdaptativeWidget> ();
178    protected void add_adaptative_child (AdaptativeWidget child)
179    {
180        adaptative_children.append (child);
181    }
182
183    private void update_adaptative_children (ref int width, ref int height)
184    {
185        bool extra_flat = height < 400;
186        bool flat       = height < 500;
187
188        if (width < 590)
189        {
190            if (extra_flat)         change_window_size (AdaptativeWidget.WindowSize.PHONE_BOTH);
191            else if (height < 787)  change_window_size (AdaptativeWidget.WindowSize.PHONE_VERT);
192            else                    change_window_size (AdaptativeWidget.WindowSize.EXTRA_THIN);
193
194            set_style_classes (/* extra thin */ true, /* thin */ true, /* large */ false,
195                               /* extra flat */ extra_flat, /* flat */ flat);
196        }
197        else if (width < 787)
198        {
199            if (extra_flat)         change_window_size (AdaptativeWidget.WindowSize.PHONE_HZTL);
200            else                    change_window_size (AdaptativeWidget.WindowSize.QUITE_THIN);
201
202            set_style_classes (/* extra thin */ false, /* thin */ true, /* large */ false,
203                               /* extra flat */ extra_flat, /* flat */ flat);
204        }
205        else
206        {
207            if (extra_flat)         change_window_size (AdaptativeWidget.WindowSize.EXTRA_FLAT);
208            else                    change_window_size (AdaptativeWidget.WindowSize.USUAL_SIZE);
209
210            set_style_classes (/* extra thin */ false, /* thin */ false, /* large */ (width > LARGE_WINDOW_SIZE),
211                               /* extra flat */ extra_flat, /* flat */ flat);
212        }
213    }
214
215    private void change_window_size (AdaptativeWidget.WindowSize new_window_size)
216    {
217        if (window_size == new_window_size)
218            return;
219        window_size = new_window_size;
220        adaptative_children.@foreach ((adaptative_child) => adaptative_child.set_window_size (new_window_size));
221    }
222
223    /*\
224    * * manage style classes
225    \*/
226
227    private bool has_extra_thin_window_class = false;
228    private bool has_thin_window_class = false;
229    private bool has_large_window_class = false;
230    private bool has_extra_flat_window_class = false;
231    private bool has_flat_window_class = false;
232
233    private void set_style_classes (bool extra_thin_window, bool thin_window, bool large_window,
234                                    bool extra_flat_window, bool flat_window)
235    {
236        // for width
237        if (has_extra_thin_window_class && !extra_thin_window)
238            set_style_class ("extra-thin-window", false, ref has_extra_thin_window_class);
239        if (has_thin_window_class && !thin_window)
240            set_style_class ("thin-window", false, ref has_thin_window_class);
241
242        if (large_window != has_large_window_class)
243            set_style_class ("large-window", large_window, ref has_large_window_class);
244        if (thin_window != has_thin_window_class)
245            set_style_class ("thin-window", thin_window, ref has_thin_window_class);
246        if (extra_thin_window != has_extra_thin_window_class)
247            set_style_class ("extra-thin-window", extra_thin_window, ref has_extra_thin_window_class);
248
249        // for height
250        if (has_extra_flat_window_class && !extra_flat_window)
251            set_style_class ("extra-flat-window", false, ref has_extra_flat_window_class);
252
253        if (flat_window != has_flat_window_class)
254            set_style_class ("flat-window", flat_window, ref has_flat_window_class);
255        if (extra_flat_window != has_extra_flat_window_class)
256            set_style_class ("extra-flat-window", extra_flat_window, ref has_extra_flat_window_class);
257    }
258
259    private inline void set_style_class (string class_name, bool new_state, ref bool old_state)
260    {
261        old_state = new_state;
262        if (new_state)
263            window_style_context.add_class (class_name);
264        else
265            window_style_context.remove_class (class_name);
266    }
267
268    /*\
269    * * manage window state
270    \*/
271
272    [CCode (notify = false)] public string schema_path
273    {
274        protected construct
275        {
276            string? _value = value;
277            if (_value == null)
278                assert_not_reached ();
279
280            settings = new GLib.Settings.with_path ("org.gnome.Four-in-a-row.Lib", value);
281        }
282    }
283    private GLib.Settings settings;
284
285    private int window_width = 0;
286    private int window_height = 0;
287    private bool window_is_maximized = false;
288    private bool window_is_fullscreen = false;
289    private bool window_is_tiled = false;
290
291    private void load_window_state ()   // called on construct
292    {
293        if (settings.get_boolean ("window-is-maximized"))
294            maximize ();
295        set_default_size (settings.get_int ("window-width"), settings.get_int ("window-height"));
296    }
297
298    private void update_window_state () // called on size-allocate
299    {
300        if (window_is_maximized || window_is_tiled || window_is_fullscreen)
301            return;
302        int? _window_width = null;
303        int? _window_height = null;
304        get_size (out _window_width, out _window_height);
305        if (_window_width == null || _window_height == null)
306            return;
307        window_width = (!) _window_width;
308        window_height = (!) _window_height;
309    }
310
311    private void save_window_state ()   // called on destroy
312    {
313        settings.delay ();
314        settings.set_int ("window-width", window_width);
315        settings.set_int ("window-height", window_height);
316        settings.set_boolean ("window-is-maximized", window_is_maximized || window_is_fullscreen);
317        settings.apply ();
318    }
319
320    /*\
321    * * manage high-constrast
322    \*/
323
324    internal signal void gtk_theme_changed ();
325
326    private void manage_high_contrast ()
327    {
328        Gtk.Settings? nullable_gtk_settings = Gtk.Settings.get_default ();
329        if (nullable_gtk_settings == null)
330            return;
331
332        Gtk.Settings gtk_settings = (!) nullable_gtk_settings;
333        gtk_settings.notify ["gtk-theme-name"].connect (update_highcontrast_state);
334        _update_highcontrast_state (gtk_settings.gtk_theme_name);
335    }
336
337    private void update_highcontrast_state (Object gtk_settings, ParamSpec unused)
338    {
339        _update_highcontrast_state (((Gtk.Settings) gtk_settings).gtk_theme_name);
340        gtk_theme_changed ();
341    }
342
343    private bool highcontrast_state = false;
344    private void _update_highcontrast_state (string theme_name)
345    {
346        bool highcontrast_new_state = "HighContrast" in theme_name;
347        if (highcontrast_new_state == highcontrast_state)
348            return;
349        highcontrast_state = highcontrast_new_state;
350
351        if (highcontrast_new_state)
352            window_style_context.add_class ("hc-theme");
353        else
354            window_style_context.remove_class ("hc-theme");
355    }
356}
357