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