1/*
2 * Copyright (C) 2010-2013 Robert Ancell
3 *
4 * This program is free software: you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License as published by the Free Software
6 * Foundation, either version 2 of the License, or (at your option) any later
7 * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
8 * license.
9 */
10
11public class Quadrapassel : Gtk.Application
12{
13    /* Application settings */
14    private Settings settings;
15
16    /* Main window */
17    private Gtk.Window window;
18    private Gtk.MenuButton menu_button;
19    private int window_width;
20    private int window_height;
21    private bool is_maximized;
22    private bool is_tiled;
23
24    /* Game being played */
25    private Game? game = null;
26
27    /* Rendering of game */
28    private GameView view;
29
30    /* Preview of the next shape */
31    private Preview preview;
32
33    /* Label showing current score */
34    private Gtk.Label score_label;
35
36    /* Label showing the number of lines destroyed */
37    private Gtk.Label n_destroyed_label;
38
39    /* Label showing the current level */
40    private Gtk.Label level_label;
41
42    private History history;
43
44    private SimpleAction pause_action;
45
46    private Gtk.Button pause_play_button;
47    private Gtk.Image pause_play_button_image;
48
49    private Gtk.Dialog preferences_dialog;
50    private Gtk.SpinButton starting_level_spin;
51    private Preview theme_preview;
52    private Gtk.SpinButton fill_height_spinner;
53    private Gtk.SpinButton fill_prob_spinner;
54    private Gtk.CheckButton do_preview_toggle;
55    private Gtk.CheckButton difficult_blocks_toggle;
56    private Gtk.CheckButton rotate_counter_clock_wise_toggle;
57    private Gtk.CheckButton show_shadow_toggle;
58    private Gtk.CheckButton sound_toggle;
59    private Gtk.ListStore controls_model;
60
61    private const GLib.ActionEntry[] action_entries =
62    {
63        { "new-game",      new_game_cb    },
64        { "pause",         pause_cb       },
65        { "scores",        scores_cb      },
66        { "menu",          menu_cb },
67        { "preferences",   preferences_cb },
68        { "help",          help_cb        },
69        { "about",         about_cb       },
70        { "quit",          quit_cb        }
71    };
72
73    public Quadrapassel ()
74    {
75        Object (application_id: "org.gnome.Quadrapassel", flags: ApplicationFlags.FLAGS_NONE);
76    }
77
78    protected override void startup ()
79    {
80        base.startup ();
81
82        Gtk.Settings.get_default ().set ("gtk-application-prefer-dark-theme", true);
83
84        add_action_entries (action_entries, this);
85        set_accels_for_action ("app.new-game", {"<Primary>n"});
86        set_accels_for_action ("app.pause", {"Pause"});
87        set_accels_for_action ("app.menu", {"F10"});
88        set_accels_for_action ("app.help", {"F1"});
89        set_accels_for_action ("app.quit", {"<Primary>q"});
90        pause_action = lookup_action ("pause") as SimpleAction;
91
92        settings = new Settings ("org.gnome.Quadrapassel");
93
94        window = new Gtk.ApplicationWindow (this);
95        window.icon_name = "org.gnome.Quadrapassel";
96        window.set_events (window.get_events () | Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK);
97        window.title = _("Quadrapassel");
98        window.size_allocate.connect (size_allocate_cb);
99        window.window_state_event.connect (window_state_event_cb);
100        window.key_press_event.connect (key_press_event_cb);
101        window.key_release_event.connect (key_release_event_cb);
102        window.set_default_size (settings.get_int ("window-width"), settings.get_int ("window-height"));
103        if (settings.get_boolean ("window-is-maximized"))
104            window.maximize ();
105
106        var headerbar = new Gtk.HeaderBar ();
107        headerbar.show_close_button = true;
108        headerbar.set_title (_("Quadrapassel"));
109        headerbar.show ();
110        window.set_titlebar (headerbar);
111
112        var menu = new Menu ();
113        var section = new Menu ();
114        menu.append_section (null, section);
115        section.append (_("_New Game"), "app.new-game");
116        section.append (_("_Scores"), "app.scores");
117        section = new Menu ();
118        menu.append_section (null, section);
119        section.append (_("_Preferences"), "app.preferences");
120        section.append (_("_Help"), "app.help");
121        section.append (_("_About Quadrapassel"), "app.about");
122        menu_button = new Gtk.MenuButton ();
123        menu_button.set_image (new Gtk.Image.from_icon_name ("open-menu-symbolic", Gtk.IconSize.BUTTON));
124        menu_button.show ();
125        menu_button.set_menu_model (menu);
126
127        headerbar.pack_end(menu_button);
128
129        var game_grid = new Gtk.Grid ();
130        game_grid.set_column_homogeneous (true);
131        window.add (game_grid);
132
133        view = new GameView ();
134        view.theme = settings.get_string ("theme");
135        view.mute = !settings.get_boolean ("sound");
136        view.show_shadow = settings.get_boolean ("show-shadow");
137        view.game = new Game (20, 10, 1, 20, 10); // Game board size, changed width to 10
138        view.show ();
139        var game_aspect = new Gtk.AspectFrame (null, 0.5f, 0.5f, 10.0f/20.0f, false); // change to 10 from 14
140        game_aspect.show ();
141        game_aspect.add (view);
142        game_aspect.border_width = 1;
143        game_grid.attach (game_aspect, 0, 1, 2, 17);
144
145        pause_play_button = new Gtk.Button ();
146        pause_play_button_image = new Gtk.Image.from_icon_name ("media-playback-start-symbolic", Gtk.IconSize.DIALOG);
147        pause_play_button.add (pause_play_button_image);
148        pause_play_button.action_name = "app.new-game";
149        pause_play_button.tooltip_text = _("Start a new game");
150        pause_play_button.margin = 30;
151        pause_play_button_image.show ();
152        pause_play_button.show ();
153
154        var preview_frame = new Gtk.AspectFrame (_("Next"), 0.5f, 0.5f, 1.0f, false);
155        preview_frame.set_label_align (0.5f, 1.0f);
156        preview = new Preview (preview_frame);
157        preview.theme = settings.get_string ("theme");
158        preview.enabled = settings.get_boolean ("do-preview");
159        preview_frame.add (preview);
160        preview_frame.show ();
161        preview.show ();
162
163        game_grid.attach (preview_frame, 2, 1, 1, 3);
164        game_grid.show ();
165
166        var label = new Gtk.Label (null);
167        label.set_markup ("<span color='gray'>%s</span>".printf (_("Score")));
168        label.set_alignment (0.5f, 0.5f);
169        label.show ();
170        game_grid.attach (label, 2, 5, 1, 1);
171        score_label = new Gtk.Label ("<big>-</big>");
172        score_label.set_use_markup (true);
173        score_label.set_alignment (0.5f, 0.0f);
174        score_label.show ();
175        game_grid.attach (score_label, 2, 6, 1, 2);
176
177        label = new Gtk.Label (null);
178        label.set_markup ("<span color='gray'>%s</span>".printf (_("Lines")));
179        label.set_alignment (0.5f, 0.5f);
180        label.show ();
181        game_grid.attach (label, 2, 9, 1, 1);
182        n_destroyed_label = new Gtk.Label ("<big>-</big>");
183        n_destroyed_label.set_use_markup (true);
184        n_destroyed_label.set_alignment (0.5f, 0.0f);
185        n_destroyed_label.show ();
186        game_grid.attach (n_destroyed_label, 2, 10, 1, 2);
187
188        label = new Gtk.Label (null);
189        label.set_markup ("<span color='gray'>%s</span>".printf (_("Level")));
190        label.set_alignment (0.5f, 0.5f);
191        label.show ();
192        game_grid.attach (label, 2, 13, 1, 1);
193        level_label = new Gtk.Label ("<big>-</big>");
194        level_label.set_use_markup (true);
195        level_label.set_alignment (0.5f, 0.0f);
196        level_label.show ();
197        game_grid.attach (level_label, 2, 14, 1, 2);
198
199        game_grid.attach (pause_play_button, 2, 16, 1, 2);
200
201        history = new History (Path.build_filename (Environment.get_user_data_dir (), "quadrapassel", "history"));
202        history.load ();
203
204        pause_action.set_enabled (false);
205    }
206
207    private void size_allocate_cb (Gtk.Allocation allocation)
208    {
209        if (is_maximized || is_tiled)
210            return;
211        window.get_size (out window_width, out window_height);
212    }
213
214    private bool window_state_event_cb (Gdk.EventWindowState event)
215    {
216        if ((event.changed_mask & Gdk.WindowState.MAXIMIZED) != 0)
217            is_maximized = (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0;
218        /* We don’t save this state, but track it for saving size allocation */
219        if ((event.changed_mask & Gdk.WindowState.TILED) != 0)
220            is_tiled = (event.new_window_state & Gdk.WindowState.TILED) != 0;
221        return false;
222    }
223
224    protected override void shutdown ()
225    {
226        base.shutdown ();
227
228        /* Save window state */
229        settings.set_int ("window-width", window_width);
230        settings.set_int ("window-height", window_height);
231        settings.set_boolean ("window-is-maximized", is_maximized);
232
233        /* Record the score if the game isn't over. */
234        if (game != null && !game.game_over && game.score > 0)
235        {
236            var date = new DateTime.now_local ();
237            var entry = new HistoryEntry (date, game.score);
238            history.add (entry);
239            history.save ();
240        }
241    }
242
243    protected override void activate ()
244    {
245        window.present ();
246    }
247
248    private void preferences_dialog_close_cb ()
249    {
250        preferences_dialog.destroy ();
251        preferences_dialog = null;
252    }
253
254    private void preferences_dialog_response_cb (int response_id)
255    {
256        preferences_dialog_close_cb ();
257    }
258
259    private void preferences_cb ()
260    {
261        if (preferences_dialog != null)
262        {
263            preferences_dialog.present ();
264            return;
265        }
266
267        preferences_dialog = new Gtk.Dialog.with_buttons (_("Preferences"),
268                                                          window,
269                                                          Gtk.DialogFlags.USE_HEADER_BAR,
270                                                          null);
271        preferences_dialog.set_border_width (5);
272        var vbox = (Gtk.Box) preferences_dialog.get_content_area ();
273        vbox.set_spacing (2);
274        preferences_dialog.close.connect (preferences_dialog_close_cb);
275        preferences_dialog.response.connect (preferences_dialog_response_cb);
276
277        var notebook = new Gtk.Notebook ();
278        notebook.set_border_width (5);
279        vbox.pack_start (notebook, true, true, 0);
280
281        var grid = new Gtk.Grid ();
282        grid.set_row_spacing (6);
283        grid.set_column_spacing (12);
284        grid.border_width = 12;
285        var label = new Gtk.Label (_("Game"));
286        notebook.append_page (grid, label);
287
288        /* pre-filled rows */
289        label = new Gtk.Label.with_mnemonic (_("_Number of pre-filled rows:"));
290        label.set_alignment (0, 0.5f);
291        label.set_hexpand (true);
292        grid.attach (label, 0, 0, 1, 1);
293
294        var adj = new Gtk.Adjustment (settings.get_int ("line-fill-height"), 0, 15, 1, 5, 0);
295        // the maximum should be at least 4 less than the new game height but as long as the game height is a magic 20 and not a setting, we can keep it at 15
296        fill_height_spinner = new Gtk.SpinButton (adj, 10, 0);
297        fill_height_spinner.set_update_policy (Gtk.SpinButtonUpdatePolicy.ALWAYS);
298        fill_height_spinner.set_snap_to_ticks (true);
299        fill_height_spinner.value_changed.connect (fill_height_spinner_value_changed_cb);
300        grid.attach (fill_height_spinner, 1, 0, 1, 1);
301        label.set_mnemonic_widget (fill_height_spinner);
302
303        /* pre-filled rows density */
304        label = new Gtk.Label.with_mnemonic (_("_Density of blocks in a pre-filled row:"));
305        label.set_alignment (0, 0.5f);
306        label.set_hexpand (true);
307        grid.attach (label, 0, 1, 1, 1);
308
309        adj = new Gtk.Adjustment (settings.get_int ("line-fill-probability"), 0, 10, 1, 5, 0);
310        fill_prob_spinner = new Gtk.SpinButton (adj, 10, 0);
311        fill_prob_spinner.set_update_policy (Gtk.SpinButtonUpdatePolicy.ALWAYS);
312        fill_prob_spinner.set_snap_to_ticks (true);
313        fill_prob_spinner.value_changed.connect (fill_prob_spinner_value_changed_cb);
314        grid.attach (fill_prob_spinner, 1, 1, 1, 1);
315        label.set_mnemonic_widget (fill_prob_spinner);
316
317        /* starting level */
318        label = new Gtk.Label.with_mnemonic (_("_Starting level:"));
319        label.set_alignment (0, 0.5f);
320        label.set_hexpand (true);
321        grid.attach (label, 0, 2, 1, 1);
322
323        adj = new Gtk.Adjustment (settings.get_int ("starting-level"), 1, 20, 1, 5, 0);
324        starting_level_spin = new Gtk.SpinButton (adj, 10.0, 0);
325        starting_level_spin.set_update_policy (Gtk.SpinButtonUpdatePolicy.ALWAYS);
326        starting_level_spin.set_snap_to_ticks (true);
327        starting_level_spin.value_changed.connect (starting_level_value_changed_cb);
328        grid.attach (starting_level_spin, 1, 2, 1, 1);
329        label.set_mnemonic_widget (starting_level_spin);
330
331        sound_toggle = new Gtk.CheckButton.with_mnemonic (_("_Enable sounds"));
332        sound_toggle.set_active (settings.get_boolean ("sound"));
333        sound_toggle.toggled.connect (sound_toggle_toggled_cb);
334        grid.attach (sound_toggle, 0, 3, 2, 1);
335
336        difficult_blocks_toggle = new Gtk.CheckButton.with_mnemonic (_("Choose difficult _blocks"));
337        difficult_blocks_toggle.set_active (settings.get_boolean ("pick-difficult-blocks"));
338        difficult_blocks_toggle.toggled.connect (difficult_blocks_toggled_cb);
339        grid.attach (difficult_blocks_toggle, 0, 4, 2, 1);
340
341        do_preview_toggle = new Gtk.CheckButton.with_mnemonic (_("_Preview next block"));
342        do_preview_toggle.set_active (settings.get_boolean ("do-preview"));
343        do_preview_toggle.toggled.connect (do_preview_toggle_toggled_cb);
344        grid.attach (do_preview_toggle, 0, 5, 2, 1);
345
346        /* rotate counter clock wise */
347        rotate_counter_clock_wise_toggle = new Gtk.CheckButton.with_mnemonic (_("_Rotate blocks counterclockwise"));
348        rotate_counter_clock_wise_toggle.set_active (settings.get_boolean ("rotate-counter-clock-wise"));
349        rotate_counter_clock_wise_toggle.toggled.connect (set_rotate_counter_clock_wise);
350        grid.attach (rotate_counter_clock_wise_toggle, 0, 6, 2, 1);
351
352        show_shadow_toggle = new Gtk.CheckButton.with_mnemonic (_("Show _where the block will land"));
353        show_shadow_toggle.set_active (settings.get_boolean ("show-shadow"));
354        show_shadow_toggle.toggled.connect (user_target_toggled_cb);
355        grid.attach (show_shadow_toggle, 0, 7, 2, 1);
356
357        /* controls page */
358        controls_model = new Gtk.ListStore (4, typeof (string), typeof (string), typeof (uint), typeof (uint));
359        Gtk.TreeIter iter;
360        controls_model.append (out iter);
361        var keyval = settings.get_int ("key-left");
362        controls_model.set (iter, 0, "key-left", 1, _("Move left"), 2, keyval);
363        controls_model.append (out iter);
364        keyval = settings.get_int ("key-right");
365        controls_model.set (iter, 0, "key-right", 1, _("Move right"), 2, keyval);
366        controls_model.append (out iter);
367        keyval = settings.get_int ("key-down");
368        controls_model.set (iter, 0, "key-down", 1, _("Move down"), 2, keyval);
369        controls_model.append (out iter);
370        keyval = settings.get_int ("key-drop");
371        controls_model.set (iter, 0, "key-drop", 1, _("Drop"), 2, keyval);
372        controls_model.append (out iter);
373        keyval = settings.get_int ("key-rotate");
374        controls_model.set (iter, 0, "key-rotate", 1, _("Rotate"), 2, keyval);
375        controls_model.append (out iter);
376        keyval = settings.get_int ("key-pause");
377        controls_model.set (iter, 0, "key-pause", 1, _("Pause"), 2, keyval);
378        var controls_view = new Gtk.TreeView.with_model (controls_model);
379        controls_view.headers_visible = false;
380        controls_view.enable_search = false;
381        var label_renderer = new Gtk.CellRendererText ();
382        controls_view.insert_column_with_attributes (-1, "Control", label_renderer, "text", 1);
383        var key_renderer = new Gtk.CellRendererAccel ();
384        key_renderer.editable = true;
385        key_renderer.accel_mode = Gtk.CellRendererAccelMode.OTHER;
386        key_renderer.accel_edited.connect (accel_edited_cb);
387        key_renderer.accel_cleared.connect (accel_cleared_cb);
388        controls_view.insert_column_with_attributes (-1, "Key", key_renderer, "accel-key", 2, "accel-mods", 3);
389
390        var controls_list = new Gtk.ScrolledWindow (null, null);
391        controls_list.border_width = 12;
392        controls_list.hscrollbar_policy = Gtk.PolicyType.NEVER;
393        controls_list.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
394        controls_list.shadow_type = Gtk.ShadowType.IN;
395        controls_list.add (controls_view);
396        label = new Gtk.Label (_("Controls"));
397        notebook.append_page (controls_list, label);
398
399        /* theme page */
400        vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
401        vbox.set_border_width (12);
402        label = new Gtk.Label (_("Theme"));
403        notebook.append_page (vbox, label);
404
405        var theme_combo = new Gtk.ComboBox ();
406        vbox.pack_start (theme_combo, false, true, 0);
407        var theme_store = new Gtk.ListStore (2, typeof (string), typeof (string));
408        theme_combo.model = theme_store;
409        var renderer = new Gtk.CellRendererText ();
410        theme_combo.pack_start (renderer, true);
411        theme_combo.add_attribute (renderer, "text", 0);
412
413        theme_store.append (out iter);
414        theme_store.set (iter, 0, _("Plain"), 1, "plain", -1);
415        if (settings.get_string ("theme") == "plain")
416            theme_combo.set_active_iter (iter);
417
418        theme_store.append (out iter);
419        theme_store.set (iter, 0, _("Tango Flat"), 1, "tangoflat", -1);
420        if (settings.get_string ("theme") == "tangoflat")
421            theme_combo.set_active_iter (iter);
422
423        theme_store.append (out iter);
424        theme_store.set (iter, 0, _("Tango Shaded"), 1, "tangoshaded", -1);
425        if (settings.get_string ("theme") == "tangoshaded")
426            theme_combo.set_active_iter (iter);
427
428        theme_store.append (out iter);
429        theme_store.set (iter, 0, _("Clean"), 1, "clean", -1);
430        if (settings.get_string ("theme") == "clean")
431            theme_combo.set_active_iter (iter);
432
433        theme_combo.changed.connect (theme_combo_changed_cb);
434
435        theme_preview = new Preview (null);
436        theme_preview.game = new Game ();
437        theme_preview.theme = settings.get_string ("theme");
438        vbox.pack_start (theme_preview, true, true, 0);
439
440        preferences_dialog.show_all ();
441    }
442
443    private void sound_toggle_toggled_cb ()
444    {
445        var play_sound = sound_toggle.get_active ();
446        settings.set_boolean ("sound", play_sound);
447        view.mute = !play_sound;
448    }
449
450    private void do_preview_toggle_toggled_cb ()
451    {
452        var preview_enabled = do_preview_toggle.get_active ();
453        settings.set_boolean ("do-preview", preview_enabled);
454        preview.enabled = preview_enabled;
455    }
456
457    private void difficult_blocks_toggled_cb ()
458    {
459        settings.set_boolean ("pick-difficult-blocks", difficult_blocks_toggle.get_active ());
460    }
461
462    private void set_rotate_counter_clock_wise ()
463    {
464        settings.set_boolean ("rotate-counter-clock-wise", rotate_counter_clock_wise_toggle.get_active ());
465    }
466
467    private void user_target_toggled_cb ()
468    {
469        var show_shadow = show_shadow_toggle.get_active ();
470        settings.set_boolean ("show-shadow", show_shadow);
471        view.show_shadow = show_shadow;
472    }
473
474    private void accel_edited_cb (Gtk.CellRendererAccel cell, string path_string, uint keyval, Gdk.ModifierType mask, uint hardware_keycode)
475    {
476        var path = new Gtk.TreePath.from_string (path_string);
477        if (path == null)
478            return;
479
480        Gtk.TreeIter iter;
481        if (!controls_model.get_iter (out iter, path))
482            return;
483
484
485        if (keyval == settings.get_int ("key-left")||
486            keyval == settings.get_int ("key-right") ||
487            keyval == settings.get_int ("key-down") ||
488            keyval == settings.get_int ("key-drop") ||
489            keyval == settings.get_int ("key-rotate") ||
490            keyval == settings.get_int ("key-pause"))
491        {
492            // Throw up a dialog
493            var dialog = new Gtk.MessageDialog (null, Gtk.DialogFlags.MODAL, Gtk.MessageType.WARNING, Gtk.ButtonsType.OK, _("Unable to change key, as this key already exists"));
494            dialog.set_title (Environment.get_application_name ());
495            dialog.run ();
496            dialog.destroy ();
497            return;
498        }
499        else
500        {
501            string? key = null;
502            controls_model.get (iter, 0, out key);
503            if (key == null)
504                return;
505
506            controls_model.set (iter, 2, keyval);
507            settings.set_int (key, (int) keyval);
508        }
509    }
510
511    private void accel_cleared_cb (Gtk.CellRendererAccel cell, string path_string)
512    {
513        var path = new Gtk.TreePath.from_string (path_string);
514        if (path == null)
515            return;
516
517        Gtk.TreeIter iter;
518        if (!controls_model.get_iter (out iter, path))
519            return;
520
521        string? key = null;
522        controls_model.get (iter, 0, out key);
523        if (key == null)
524            return;
525
526        controls_model.set (iter, 2, 0);
527        settings.set_int (key, 0);
528    }
529
530    private void theme_combo_changed_cb (Gtk.ComboBox widget)
531    {
532        Gtk.TreeIter iter;
533        widget.get_active_iter (out iter);
534        string theme;
535        widget.model.get (iter, 1, out theme);
536        view.theme = theme;
537        preview.theme = theme;
538        if (theme_preview != null)
539            theme_preview.theme = theme;
540        settings.set_string ("theme", theme);
541    }
542
543    private void fill_height_spinner_value_changed_cb (Gtk.SpinButton spin)
544    {
545        int value = spin.get_value_as_int ();
546        settings.set_int ("line-fill-height", value);
547    }
548
549    private void fill_prob_spinner_value_changed_cb (Gtk.SpinButton spin)
550    {
551        int value = spin.get_value_as_int ();
552        settings.set_int ("line-fill-probability", value);
553    }
554
555    private void starting_level_value_changed_cb (Gtk.SpinButton spin)
556    {
557        int value = spin.get_value_as_int ();
558        settings.set_int ("starting-level", value);
559    }
560
561    private void pause_cb ()
562    {
563        if (game != null)
564            game.paused = !game.paused;
565    }
566
567    private void quit_cb ()
568    {
569        window.destroy ();
570    }
571
572    private bool key_press_event_cb (Gtk.Widget widget, Gdk.EventKey event)
573    {
574        var keyval = upper_key (event.keyval);
575
576        if (game.game_over && keyval == upper_key (settings.get_int ("key-start")))
577        {
578            new_game();
579        }
580
581        if (game == null) {
582            // Pressing pause with no game will start a new game.
583            if (keyval == upper_key (settings.get_int ("key-pause")))
584            {
585                new_game ();
586                return true;
587            }
588
589            return false;
590        }
591
592        if (keyval == upper_key (settings.get_int ("key-pause")))
593        {
594            if (!game.game_over)
595                game.paused = !game.paused;
596            return true;
597        }
598
599        if (game.paused)
600            return false;
601
602        if (keyval == upper_key (settings.get_int ("key-left")))
603        {
604            game.move_left ();
605            return true;
606        }
607        else if (keyval == upper_key (settings.get_int ("key-right")))
608        {
609            game.move_right ();
610            return true;
611        }
612        else if (keyval == upper_key (settings.get_int ("key-rotate")))
613        {
614            if (settings.get_boolean ("rotate-counter-clock-wise"))
615                game.rotate_left ();
616            else
617                game.rotate_right ();
618            return true;
619        }
620        else if (keyval == upper_key (settings.get_int ("key-down")))
621        {
622            game.set_fast_forward (true);
623            return true;
624        }
625        else if (keyval == upper_key (settings.get_int ("key-drop")))
626        {
627            game.drop ();
628            return true;
629        }
630
631        return false;
632    }
633
634    private bool key_release_event_cb (Gtk.Widget widget, Gdk.EventKey event)
635    {
636        var keyval = upper_key (event.keyval);
637
638        if (game == null)
639            return false;
640
641        if (keyval == upper_key (settings.get_int ("key-left")) ||
642            keyval == upper_key (settings.get_int ("key-right")))
643        {
644            game.stop_moving ();
645            return true;
646        }
647        else if (keyval == upper_key (settings.get_int ("key-down")))
648        {
649            game.set_fast_forward (false);
650            return true;
651        }
652
653        return false;
654    }
655
656    private uint upper_key (uint keyval)
657    {
658        if (keyval > 255)
659            return keyval;
660        return ((char) keyval).toupper ();
661    }
662
663    private void new_game_cb ()
664    {
665        new_game ();
666    }
667
668    private void new_game ()
669    {
670        if (game != null)
671        {
672            game.stop ();
673            SignalHandler.disconnect_matched (game, SignalMatchType.DATA, 0, 0, null, null, this);
674        }
675
676        // Set game dimension, change to 10
677        game = new Game (20, 10, settings.get_int ("starting-level"), settings.get_int ("line-fill-height"), settings.get_int ("line-fill-probability"), settings.get_boolean ("pick-difficult-blocks"));
678        game.pause_changed.connect (pause_changed_cb);
679        game.shape_landed.connect (shape_landed_cb);
680        game.complete.connect (complete_cb);
681        preview.game = game;
682        view.game = game;
683
684        game.start ();
685
686        update_score ();
687        pause_action.set_enabled (true);
688        pause_play_button.action_name = "app.pause";
689    }
690
691    private void pause_changed_cb ()
692    {
693        if (game.paused)
694        {
695            pause_play_button_image.set_from_icon_name ("media-playback-start-symbolic", Gtk.IconSize.DIALOG);
696            pause_play_button.tooltip_text = _("Unpause the game");
697        }
698        else
699        {
700            pause_play_button_image.set_from_icon_name ("media-playback-pause-symbolic", Gtk.IconSize.DIALOG);
701            pause_play_button.tooltip_text = _("Pause the game");
702        }
703    }
704
705    private void shape_landed_cb (int[] lines, List<Block> line_blocks)
706    {
707        update_score ();
708    }
709
710    private void complete_cb ()
711    {
712        pause_action.set_enabled (false);
713        pause_play_button_image.set_from_icon_name ("view-refresh-symbolic" , Gtk.IconSize.DIALOG);
714        pause_play_button.action_name = "app.new-game";
715        pause_play_button.tooltip_text = _("Start a new game");
716
717        if (game.score > 0)
718        {
719            var date = new DateTime.now_local ();
720            var entry = new HistoryEntry (date, game.score);
721            history.add (entry);
722            history.save ();
723
724            if (show_scores (entry, true) == Gtk.ResponseType.OK)
725                new_game ();
726        }
727    }
728
729    private int show_scores (HistoryEntry? selected_entry = null, bool show_close = false)
730    {
731        var dialog = new ScoreDialog (history, selected_entry, show_close);
732        dialog.modal = true;
733        dialog.transient_for = window;
734
735        var result = dialog.run ();
736        dialog.destroy ();
737
738        return result;
739    }
740
741    private void update_score ()
742    {
743        var score = 0;
744        var level = 0;
745        var n_lines_destroyed = 0;
746
747        if (game != null)
748        {
749            score = game.score;
750            level = game.level;
751            n_lines_destroyed = game.n_lines_destroyed;
752        }
753
754        score_label.set_markup ("<big>%d</big>".printf (score));
755        level_label.set_markup ("<big>%d</big>".printf (level));
756        n_destroyed_label.set_markup ("<big>%d</big>".printf (n_lines_destroyed));
757    }
758
759    private void help_cb ()
760    {
761        try
762        {
763            Gtk.show_uri (window.get_screen (), "help:quadrapassel", Gtk.get_current_event_time ());
764        }
765        catch (Error e)
766        {
767            warning ("Failed to show help: %s", e.message);
768        }
769    }
770
771    private void about_cb ()
772    {
773        string[] authors = { "GNOME Games Team", "Maintainer: John Ward<john@johnward.net>", null };
774        string[] documenters = { "Angela Boyle", null };
775
776        Gtk.show_about_dialog (window,
777                               "program-name", _("Quadrapassel"),
778                               "version", VERSION,
779                               "comments", _("A classic game where you rotate blocks to make complete rows, but don't pile your blocks too high or it's game over!"),
780                               "copyright", "Copyright © 1999 J. Marcin Gorycki, 2000–2015 Others",
781                               "license-type", Gtk.License.GPL_2_0,
782                               "authors", authors,
783                               "documenters", documenters,
784                               "translator-credits", _("translator-credits"),
785                               "logo-icon-name", "org.gnome.Quadrapassel",
786                               "website", "https://wiki.gnome.org/Apps/Quadrapassel",
787                               null);
788    }
789
790    private void menu_cb ()
791    {
792        menu_button.activate ();
793    }
794
795    private void scores_cb ()
796    {
797        show_scores ();
798    }
799
800    public static int main (string[] args)
801    {
802        Intl.setlocale (LocaleCategory.ALL, "");
803        Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
804        Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
805        Intl.textdomain (GETTEXT_PACKAGE);
806
807        var context = new OptionContext ("");
808
809        context.add_group (Gtk.get_option_group (true));
810        context.add_group (Clutter.get_option_group_without_init ());
811
812        try
813        {
814            context.parse (ref args);
815        }
816        catch (Error e)
817        {
818            stderr.printf ("%s\n", e.message);
819            return Posix.EXIT_FAILURE;
820        }
821
822        Environment.set_application_name (_("Quadrapassel"));
823
824        Gtk.Window.set_default_icon_name ("quadrapassel");
825
826        try
827        {
828            GtkClutter.init_with_args (ref args, "", new OptionEntry[0], null);
829        }
830        catch (Error e)
831        {
832            var dialog = new Gtk.MessageDialog (null, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.NONE, "Unable to initialize Clutter:\n%s", e.message);
833            dialog.set_title (Environment.get_application_name ());
834            dialog.run ();
835            dialog.destroy ();
836            return Posix.EXIT_FAILURE;
837        }
838
839        var app = new Quadrapassel ();
840        return app.run (args);
841    }
842}
843