1/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2/*
3 * Copyright © 2014 Parin Porecha
4 * Copyright © 2014 Michael Catanzaro
5 *
6 * This file is part of GNOME Sudoku.
7 *
8 * GNOME Sudoku is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * GNOME Sudoku is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with GNOME Sudoku. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22using Gtk;
23using Gdk;
24
25private class SudokuCellView : DrawingArea
26{
27    private double size_ratio = 2;
28
29    private Popover popover;
30    private Popover earmark_popover;
31
32    private SudokuGame game;
33
34    private int row;
35    private int col;
36
37    public int value
38    {
39        get { return game.board [row, col]; }
40        set
41        {
42            if (is_fixed)
43            {
44                if (game.mode == GameMode.PLAY)
45                    return;
46            }
47            if (value == 0)
48            {
49                if (game.board [row, col] != 0)
50                    game.remove (row, col);
51                if (game.mode == GameMode.PLAY)
52                    return;
53            }
54            if (value == game.board [row, col])
55                return;
56
57            game.insert (row, col, value);
58        }
59    }
60
61    public bool is_fixed
62    {
63        get { return game.board.is_fixed[row, col]; }
64    }
65
66    private bool _show_possibilities;
67    public bool show_possibilities
68    {
69        get { return _show_possibilities; }
70        set
71        {
72            _show_possibilities = value;
73            queue_draw ();
74        }
75    }
76
77    private bool _show_warnings = true;
78    public bool show_warnings
79    {
80        get { return _show_warnings; }
81        set
82        {
83            _show_warnings = value;
84            queue_draw ();
85        }
86    }
87
88    public bool selected { get; set; }
89    public bool highlighted_background { get; set; }
90    public bool highlighted_value { get; set; }
91
92    private NumberPicker number_picker;
93    private NumberPicker earmark_picker;
94
95    private EventControllerKey key_controller;      // for keeping in memory
96
97    public SudokuCellView (int row, int col, ref SudokuGame game)
98    {
99        this.game = game;
100        this.row = row;
101        this.col = col;
102
103        init_keyboard ();
104
105        value = game.board [row, col];
106
107        // background_color is set in the SudokuView, as it manages the color of the cells
108
109        can_focus = true;
110        events = EventMask.EXPOSURE_MASK | EventMask.BUTTON_PRESS_MASK | EventMask.BUTTON_RELEASE_MASK | EventMask.KEY_PRESS_MASK;
111
112        if (is_fixed && game.mode == GameMode.PLAY)
113            return;
114
115        focus_out_event.connect (focus_out_cb);
116        game.cell_changed.connect (cell_changed_cb);
117    }
118
119    public override bool button_press_event (EventButton event)
120    {
121        if (event.button != 1 && event.button != 3)
122            return false;
123
124        if (!is_focus)
125            grab_focus ();
126        if (game.mode == GameMode.PLAY && (is_fixed || game.paused))
127            return false;
128
129        if (popover != null || earmark_popover != null)
130        {
131            hide_both_popovers ();
132            return false;
133        }
134
135        if (event.button == 1)            // Left-Click
136        {
137            if (!_show_possibilities && (event.state & ModifierType.CONTROL_MASK) > 0 && game.mode == GameMode.PLAY)
138                show_earmark_picker ();
139            else
140                show_number_picker ();
141        }
142        else if (!_show_possibilities && event.button == 3 && game.mode == GameMode.PLAY)         // Right-Click
143            show_earmark_picker ();
144
145        return false;
146    }
147
148    private void create_earmark_picker ()
149    {
150        earmark_picker = new NumberPicker (ref game.board, true);
151        earmark_picker.earmark_state_changed.connect ((number, state) => {
152            if (state)
153                this.game.enable_earmark (row, col, number);
154            else
155                this.game.disable_earmark (row, col, number);
156            this.game.cell_changed (row, col, value, value);
157            queue_draw ();
158        });
159        earmark_picker.set_earmarks (row, col);
160    }
161
162    private void show_number_picker ()
163    {
164        if (earmark_popover != null)
165            earmark_popover.hide ();
166
167        number_picker = new NumberPicker (ref game.board);
168        number_picker.number_picked.connect ((o, number) => {
169            value = number;
170            if (number == 0)
171                notify_property ("value");
172            this.game.board.disable_all_earmarks (row, col);
173
174            popover.hide ();
175        });
176        number_picker.set_clear_button_visibility (value != 0);
177
178        popover = new Popover (this);
179        popover.add (number_picker);
180        popover.modal = false;
181        popover.position = PositionType.BOTTOM;
182        popover.notify["visible"].connect (()=> {
183            if (!popover.visible)
184                destroy_popover (ref popover, ref number_picker);
185        });
186        popover.focus_out_event.connect (() => {
187            popover.hide ();
188            return true;
189        });
190
191        popover.show ();
192    }
193
194    private void show_earmark_picker ()
195    {
196        if (popover != null)
197            popover.hide ();
198
199        create_earmark_picker ();
200
201        earmark_popover = new Popover (this);
202        earmark_popover.add (earmark_picker);
203        earmark_popover.modal = false;
204        earmark_popover.position = PositionType.BOTTOM;
205        earmark_popover.notify["visible"].connect (()=> {
206            if (!earmark_popover.visible)
207                destroy_popover (ref earmark_popover, ref earmark_picker);
208        });
209        earmark_popover.focus_out_event.connect (() => {
210            earmark_popover.hide ();
211            return true;
212        });
213
214        earmark_popover.show ();
215    }
216
217    private void destroy_popover (ref Popover popover, ref NumberPicker picker)
218    {
219        picker = null;
220        if (popover != null)
221        {
222            popover.destroy ();
223            popover = null;
224        }
225    }
226
227    public void hide_both_popovers ()
228    {
229        if (popover != null)
230            popover.hide ();
231        if (earmark_popover != null)
232            earmark_popover.hide ();
233    }
234
235    private bool focus_out_cb (Widget widget, EventFocus event)
236    {
237        hide_both_popovers ();
238        return false;
239    }
240
241    /* Key mapping function to help convert Gdk.keyval_name string to numbers */
242    private int key_map_keypad (string key_name)
243    {
244        /* Compared with "0" to make sure, actual "0" is not misinterpreted as parse error in int.parse() */
245        if (key_name == "KP_0" || key_name == "0")
246            return 0;
247        if (key_name == "KP_1")
248            return 1;
249        if (key_name == "KP_2")
250            return 2;
251        if (key_name == "KP_3")
252            return 3;
253        if (key_name == "KP_4")
254            return 4;
255        if (key_name == "KP_5")
256            return 5;
257        if (key_name == "KP_6")
258            return 6;
259        if (key_name == "KP_7")
260            return 7;
261        if (key_name == "KP_8")
262            return 8;
263        if (key_name == "KP_9")
264            return 9;
265        return -1;
266    }
267
268    private inline void init_keyboard ()  // called on construct
269    {
270        key_controller = new EventControllerKey (this);
271        key_controller.key_pressed.connect (on_key_pressed);
272    }
273
274    private inline bool on_key_pressed (EventControllerKey _key_controller, uint keyval, uint keycode, ModifierType state)
275    {
276        if (game.mode == GameMode.PLAY && (is_fixed || game.paused))
277            return false;
278        string k_name = keyval_name (keyval);
279        int k_no = int.parse (k_name);
280        /* If k_no is 0, there might be some error in parsing, crosscheck with keypad values. */
281        if (k_no == 0)
282            k_no = key_map_keypad (k_name);
283        if (k_no >= 1 && k_no <= 9)
284        {
285            bool want_earmark = (earmark_popover != null && earmark_popover.is_visible ())
286                || (state & ModifierType.CONTROL_MASK) > 0;
287            if (want_earmark && game.mode == GameMode.PLAY)
288            {
289                var new_state = !game.board.is_earmark_enabled (row, col, k_no);
290                if (new_state)
291                    game.enable_earmark (row, col, k_no);
292                else
293                    game.disable_earmark (row, col, k_no);
294
295                if (earmark_picker != null)
296                    earmark_picker.set_earmark (row, col, k_no-1, new_state);
297
298                queue_draw ();
299            }
300            else
301            {
302                value = k_no;
303                this.game.board.disable_all_earmarks (row, col);
304                hide_both_popovers ();
305            }
306            return true;
307        }
308        if (k_no == 0 || k_name == "BackSpace" || k_name == "Delete")
309        {
310            value = 0;
311            notify_property ("value");
312            return true;
313        }
314
315        if (k_name == "space" || k_name == "Return" || k_name == "KP_Enter")
316        {
317            if (popover != null)
318            {
319                popover.hide ();
320                return false;
321            }
322            show_number_picker ();
323            return true;
324        }
325
326        if (k_name == "Escape")
327        {
328            hide_both_popovers ();
329            return true;
330        }
331
332        return false;
333    }
334
335    public override bool draw (Cairo.Context c)
336    {
337        RGBA background_color;
338        if (_selected && is_focus)
339            background_color = selected_bg_color;
340        else if (is_fixed)
341            background_color = fixed_cell_color;
342        else if (_highlighted_background)
343            background_color = highlight_color;
344        else
345            background_color = free_cell_color;
346        c.set_source_rgba (background_color.red, background_color.green, background_color.blue, background_color.alpha);
347        c.rectangle (0, 0, get_allocated_width (), get_allocated_height ());
348        c.fill();
349
350        if (_show_warnings && game.board.broken_coords.contains (Coord (row, col)))
351            c.set_source_rgb (1.0, 0.0, 0.0);
352        else if (_highlighted_value)
353            c.set_source_rgb (0.2, 0.4, 0.9);
354        else if (_selected)
355            c.set_source_rgb (0.2, 0.2, 0.2);
356        else
357            c.set_source_rgb (0.0, 0.0, 0.0);
358
359        if (game.paused)
360            return false;
361
362        if (value != 0)
363        {
364            double height = (double) get_allocated_height ();
365            double width = (double) get_allocated_width ();
366            string text = "%d".printf (value);
367
368            c.set_font_size (height / size_ratio);
369            print_centered (c, text, width, height);
370            return false;
371        }
372
373        if (is_fixed && game.mode == GameMode.PLAY)
374            return false;
375
376        bool[] marks = null;
377        if (!_show_possibilities)
378        {
379            marks = game.board.get_earmarks (row, col);
380        }
381        else if (value == 0)
382        {
383            marks = game.board.get_possibilities_as_bool_array (row, col);
384        }
385
386        if (marks != null)
387        {
388            double possibility_size = get_allocated_height () / size_ratio / 2;
389            c.set_font_size (possibility_size);
390
391            double height = (double) get_allocated_height () / game.board.block_rows;
392            double width = (double) get_allocated_width () / game.board.block_cols;
393
394            int num = 0;
395            for (int row_tmp = 0; row_tmp < game.board.block_rows; row_tmp++)
396            {
397                for (int col_tmp = 0; col_tmp < game.board.block_cols; col_tmp++)
398                {
399                    num++;
400
401                    if (marks[num - 1])
402                    {
403                        if (_show_warnings && !game.board.is_possible (row, col, num))
404                            c.set_source_rgb (1.0, 0.0, 0.0);
405                        else
406                            c.set_source_rgb (0.0, 0.0, 0.0);
407
408                        var text = "%d".printf (num);
409
410                        c.save ();
411                        c.translate (col_tmp * width, (game.board.block_rows - row_tmp - 1) * height);
412                        print_centered (c, text, width, height);
413                        c.restore ();
414                    }
415                }
416            }
417        }
418
419        if (_show_warnings && (value == 0 && game.board.count_possibilities (row, col) == 0))
420        {
421            c.set_font_size (get_allocated_height () / size_ratio);
422            c.set_source_rgb (1.0, 0.0, 0.0);
423            print_centered (c, "X", get_allocated_width (), get_allocated_height ());
424        }
425
426        return false;
427    }
428
429    private void print_centered (Cairo.Context c, string text, double width, double height)
430    {
431        Cairo.FontExtents font_extents;
432        c.font_extents (out font_extents);
433
434        Cairo.TextExtents text_extents;
435        c.text_extents (text, out text_extents);
436
437        c.move_to (
438            (width - text_extents.width) / 2 - text_extents.x_bearing,
439            (height + font_extents.height) / 2 - font_extents.descent
440        );
441        c.show_text (text);
442    }
443
444    public void cell_changed_cb (int row, int col, int old_val, int new_val)
445    {
446        if (row == this.row && col == this.col)
447        {
448            this.value = new_val;
449            notify_property ("value");
450        }
451    }
452
453    public void clear ()
454    {
455        game.board.disable_all_earmarks (row, col);
456    }
457}
458
459public const RGBA fixed_cell_color = {0.8, 0.8, 0.8, 1.0};
460public const RGBA free_cell_color = {1.0, 1.0, 1.0, 1.0};
461public const RGBA highlight_color = {0.93, 0.93, 0.93, 1.0};
462public const RGBA selected_bg_color = {0.7, 0.8, 0.9, 1.0};
463
464public class SudokuView : AspectFrame
465{
466    public SudokuGame game;
467    private SudokuCellView[,] cells;
468
469    private bool previous_board_broken_state = false;
470
471    private Overlay overlay;
472    private DrawingArea drawing;
473    private Grid grid;
474
475    private int selected_row = -1;
476    private int selected_col = -1;
477    private void set_selected (int cell_row, int cell_col)
478    {
479        if (selected_row >= 0 && selected_col >= 0)
480        {
481            cells[selected_row, selected_col].selected = false;
482            cells[selected_row, selected_col].queue_draw ();
483        }
484        selected_row = cell_row;
485        selected_col = cell_col;
486        if (selected_row >= 0 && selected_col >= 0)
487        {
488            cells[selected_row, selected_col].selected = true;
489        }
490    }
491
492    public SudokuView (SudokuGame game)
493    {
494        shadow_type = ShadowType.NONE;
495        obey_child = false;
496        ratio = 1;
497
498        overlay = new Overlay ();
499        add (overlay);
500
501        drawing = new DrawingArea ();
502        drawing.draw.connect (draw_board);
503
504        if (grid != null)
505            overlay.remove (grid);
506
507        this.game = game;
508        this.game.paused_changed.connect(() => {
509            if (this.game.paused)
510                drawing.show ();
511            else
512                drawing.hide ();
513        });
514
515        var css_provider = new CssProvider ();
516        css_provider.load_from_resource ("/org/gnome/Sudoku/ui/gnome-sudoku.css");
517
518        grid = new Grid ();
519        grid.row_spacing = 2;
520        grid.column_spacing = 2;
521        grid.column_homogeneous = true;
522        grid.row_homogeneous = true;
523        grid.get_style_context ().add_class ("board");
524        grid.get_style_context ().add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
525
526        var blocks = new Grid[game.board.block_rows, game.board.block_cols];
527        for (var block_row = 0; block_row < game.board.block_rows; block_row++)
528        {
529            for (var block_col = 0; block_col < game.board.block_cols; block_col++)
530            {
531                var block_grid = new Grid ();
532                block_grid.row_spacing = 1;
533                block_grid.column_spacing = 1;
534                block_grid.column_homogeneous = true;
535                block_grid.row_homogeneous = true;
536                block_grid.get_style_context ().add_class ("block");
537                block_grid.get_style_context ().add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
538                grid.attach (block_grid, block_col, block_row, 1, 1);
539
540                blocks[block_row, block_col] = block_grid;
541            }
542        }
543
544        cells = new SudokuCellView[game.board.rows, game.board.cols];
545        for (var row = 0; row < game.board.rows; row++)
546        {
547            for (var col = 0; col < game.board.cols; col++)
548            {
549                var cell = new SudokuCellView (row, col, ref this.game);
550                var cell_row = row;
551                var cell_col = col;
552
553                cell.focus_in_event.connect (() => {
554                    if (game.paused)
555                        return false;
556
557                    this.set_selected (cell_row, cell_col);
558                    this.update_highlights ();
559                    queue_draw ();
560
561                    return false;
562                });
563
564                cell.focus_out_event.connect (() => {
565                    if (game.paused)
566                        return false;
567
568                    this.set_selected (-1, -1);
569                    this.update_highlights ();
570                    queue_draw ();
571
572                    return false;
573                });
574
575                cell.notify["value"].connect ((s, p)=> {
576                    if (_show_possibilities || _show_warnings || game.board.broken || previous_board_broken_state)
577                        previous_board_broken_state = game.board.broken;
578
579                    this.update_highlights ();
580                    // Redraw the board
581                    this.queue_draw ();
582                });
583
584                cells[row, col] = cell;
585
586                blocks[row / game.board.block_rows, col / game.board.block_cols].attach (cell, col % game.board.block_cols, row % game.board.block_rows);
587            }
588        }
589
590        overlay.add_overlay (drawing);
591        overlay.add (grid);
592        grid.show_all ();
593        overlay.show ();
594        drawing.hide ();
595    }
596
597    private void update_highlights ()
598    {
599        var has_selection = selected_row >= 0 && selected_col >= 0;
600        var cell_value = -1;
601        if (has_selection)
602            cell_value = cells[selected_row, selected_col].value;
603
604        for (var col_tmp = 0; col_tmp < game.board.cols; col_tmp++)
605        {
606            for (var row_tmp = 0; row_tmp < game.board.rows; row_tmp++)
607            {
608                cells[row_tmp, col_tmp].highlighted_background = has_selection && _highlighter && (
609                    col_tmp == selected_col ||
610                    row_tmp == selected_row ||
611                    (col_tmp / game.board.block_cols == selected_col / game.board.block_cols &&
612                     row_tmp / game.board.block_rows == selected_row / game.board.block_rows)
613                );
614                cells[row_tmp, col_tmp].highlighted_value = has_selection &&
615                    _highlighter &&
616                    cell_value == cells[row_tmp, col_tmp].value;
617            }
618        }
619    }
620
621    private bool draw_board (Cairo.Context c)
622    {
623        if (game.paused)
624        {
625            int board_length = grid.get_allocated_width ();
626
627            c.set_source_rgba (0, 0, 0, 0.75);
628            c.paint ();
629
630            c.select_font_face ("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
631            c.set_font_size (get_allocated_width () * 0.125);
632
633            /* Text on overlay when game is paused */
634            var text = _("Paused");
635            Cairo.TextExtents extents;
636            c.text_extents (text, out extents);
637            c.move_to (board_length/2.0 - extents.width/2.0, board_length/2.0 + extents.height/2.0);
638            c.set_source_rgb (1, 1, 1);
639            c.show_text (text);
640        }
641
642        return false;
643    }
644
645    public void clear ()
646    {
647        for (var i = 0; i < game.board.rows; i++)
648            for (var j = 0; j < game.board.cols; j++)
649                cells[i,j].clear ();
650    }
651
652    private bool _show_warnings = false;
653    public bool show_warnings
654    {
655        get { return _show_warnings; }
656        set {
657            _show_warnings = value;
658            for (var i = 0; i < game.board.rows; i++)
659                for (var j = 0; j < game.board.cols; j++)
660                    cells[i,j].show_warnings = _show_warnings;
661         }
662    }
663
664    private bool _show_possibilities = false;
665    public bool show_possibilities
666    {
667        get { return _show_possibilities; }
668        set {
669            _show_possibilities = value;
670            for (var i = 0; i < game.board.rows; i++)
671                for (var j = 0; j < game.board.cols; j++)
672                    cells[i,j].show_possibilities = value;
673        }
674    }
675
676    private bool _highlighter = false;
677    public bool highlighter
678    {
679        get { return _highlighter; }
680        set {
681            _highlighter = value;
682        }
683    }
684
685    public void hide_popovers ()
686    {
687        for (var i = 0; i < game.board.rows; i++)
688            for (var j = 0; j < game.board.cols; j++)
689                cells[i,j].hide_both_popovers ();
690    }
691}
692