1 /* 2 * Copyright 2007, Axel Dörfler, axeld@pinc-software.de. All rights reserved. 3 * Distributed under the terms of the MIT License. 4 */ 5 6 7 #include "SudokuView.h" 8 9 #include "SudokuField.h" 10 #include "SudokuSolver.h" 11 12 #include <ctype.h> 13 #include <errno.h> 14 #include <stdio.h> 15 #include <stdlib.h> 16 17 #include <Application.h> 18 #include <Beep.h> 19 #include <File.h> 20 #include <Path.h> 21 22 23 const uint32 kMsgCheckSolved = 'chks'; 24 25 const uint32 kStrongLineSize = 2; 26 27 28 SudokuView::SudokuView(BRect frame, const char* name, 29 const BMessage& settings, uint32 resizingMode) 30 : BView(frame, name, resizingMode, 31 B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS), 32 fField(NULL), 33 fShowHintX(~0UL), 34 fLastHintValue(~0UL), 35 fLastField(~0UL), 36 fShowKeyboardFocus(false), 37 fEditable(true) 38 { 39 BMessage field; 40 if (settings.FindMessage("field", &field) == B_OK) { 41 fField = new SudokuField(&field); 42 if (fField->InitCheck() != B_OK) { 43 delete fField; 44 fField = NULL; 45 } else if (fField->IsSolved()) 46 ClearAll(); 47 } 48 if (fField == NULL) 49 fField = new SudokuField(3); 50 51 fBlockSize = fField->BlockSize(); 52 53 if (settings.FindInt32("hint flags", (int32*)&fHintFlags) != B_OK) 54 fHintFlags = kMarkInvalid; 55 if (settings.FindBool("show cursor", &fShowCursor) != B_OK) 56 fShowCursor = false; 57 58 SetViewColor(B_TRANSPARENT_COLOR); 59 // to avoid flickering 60 rgb_color color = { 255, 255, 240 }; 61 fBackgroundColor = color; 62 SetLowColor(color); 63 FrameResized(0, 0); 64 } 65 66 67 SudokuView::~SudokuView() 68 { 69 delete fField; 70 } 71 72 73 status_t 74 SudokuView::SaveState(BMessage& state) 75 { 76 BMessage field; 77 status_t status = fField->Archive(&field, true); 78 if (status == B_OK) 79 status = state.AddMessage("field", &field); 80 if (status == B_OK) 81 status = state.AddInt32("hint flags", fHintFlags); 82 if (status == B_OK) 83 status = state.AddBool("show cursor", fShowCursor); 84 85 return status; 86 } 87 88 89 status_t 90 SudokuView::_FilterString(const char* data, size_t dataLength, char* buffer, 91 uint32& out, bool& ignore) 92 { 93 uint32 maxOut = fField->Size() * fField->Size(); 94 95 for (uint32 i = 0; data[i] && i < dataLength; i++) { 96 if (data[i] == '#') 97 ignore = true; 98 else if (data[i] == '\n') 99 ignore = false; 100 101 if (ignore || isspace(data[i])) 102 continue; 103 104 if (!_ValidCharacter(data[i])) { 105 return B_BAD_VALUE; 106 } 107 108 buffer[out++] = data[i]; 109 if (out == maxOut) 110 break; 111 } 112 113 buffer[out] = '\0'; 114 return B_OK; 115 } 116 117 118 status_t 119 SudokuView::SetTo(entry_ref& ref) 120 { 121 BPath path; 122 status_t status = path.SetTo(&ref); 123 if (status < B_OK) 124 return status; 125 126 FILE* file = fopen(path.Path(), "r"); 127 if (file == NULL) 128 return errno; 129 130 uint32 maxOut = fField->Size() * fField->Size(); 131 char buffer[1024]; 132 char line[1024]; 133 bool ignore = false; 134 uint32 out = 0; 135 136 while (fgets(line, sizeof(line), file) != NULL 137 && out < maxOut) { 138 status = _FilterString(line, sizeof(line), buffer, out, ignore); 139 if (status < B_OK) { 140 fclose(file); 141 return status; 142 } 143 } 144 145 status = fField->SetTo(_BaseCharacter(), buffer); 146 Invalidate(); 147 return status; 148 } 149 150 151 status_t 152 SudokuView::SetTo(const char* data) 153 { 154 if (data == NULL) 155 return B_BAD_VALUE; 156 157 char buffer[1024]; 158 bool ignore = false; 159 uint32 out = 0; 160 161 status_t status = _FilterString(data, 65536, buffer, out, ignore); 162 if (status < B_OK) 163 return B_BAD_VALUE; 164 165 status = fField->SetTo(_BaseCharacter(), buffer); 166 Invalidate(); 167 return status; 168 } 169 170 171 status_t 172 SudokuView::SetTo(SudokuField* field) 173 { 174 if (field == NULL || field == fField) 175 return B_BAD_VALUE; 176 177 delete fField; 178 fField = field; 179 180 fBlockSize = fField->BlockSize(); 181 FrameResized(0, 0); 182 Invalidate(); 183 return B_OK; 184 } 185 186 187 status_t 188 SudokuView::SaveTo(entry_ref& ref, bool asText) 189 { 190 BFile file; 191 status_t status = file.SetTo(&ref, B_WRITE_ONLY | B_CREATE_FILE 192 | B_ERASE_FILE); 193 if (status < B_OK) 194 return status; 195 196 if (asText) { 197 char line[1024]; 198 strcpy(line, "# Written by Sudoku\n\n"); 199 file.Write(line, strlen(line)); 200 201 uint32 i = 0; 202 203 for (uint32 y = 0; y < fField->Size(); y++) { 204 for (uint32 x = 0; x < fField->Size(); x++) { 205 if (x != 0 && x % fBlockSize == 0) 206 line[i++] = ' '; 207 _SetText(&line[i++], fField->ValueAt(x, y)); 208 } 209 line[i++] = '\n'; 210 } 211 212 file.Write(line, i); 213 } else { 214 } 215 216 return status; 217 } 218 219 220 void 221 SudokuView::ClearChanged() 222 { 223 for (uint32 y = 0; y < fField->Size(); y++) { 224 for (uint32 x = 0; x < fField->Size(); x++) { 225 if ((fField->FlagsAt(x, y) & kInitialValue) == 0) 226 fField->SetValueAt(x, y, 0); 227 } 228 } 229 230 Invalidate(); 231 } 232 233 234 void 235 SudokuView::ClearAll() 236 { 237 fField->Reset(); 238 Invalidate(); 239 } 240 241 242 void 243 SudokuView::SetHintFlags(uint32 flags) 244 { 245 if (flags == fHintFlags) 246 return; 247 248 if ((flags & kMarkInvalid) ^ (fHintFlags & kMarkInvalid)) 249 Invalidate(); 250 251 fHintFlags = flags; 252 } 253 254 255 void 256 SudokuView::SetEditable(bool editable) 257 { 258 fEditable = editable; 259 } 260 261 262 void 263 SudokuView::AttachedToWindow() 264 { 265 MakeFocus(true); 266 } 267 268 269 void 270 SudokuView::_FitFont(BFont& font, float fieldWidth, float fieldHeight) 271 { 272 font.SetSize(100); 273 274 font_height fontHeight; 275 font.GetHeight(&fontHeight); 276 277 float width = font.StringWidth("W"); 278 float height = ceilf(fontHeight.ascent) + ceilf(fontHeight.descent); 279 280 float factor = fieldWidth != fHintWidth ? 4.f / 5.f : 1.f; 281 float widthFactor = fieldWidth / (width / factor); 282 float heightFactor = fieldHeight / (height / factor); 283 font.SetSize(100 * min_c(widthFactor, heightFactor)); 284 } 285 286 287 void 288 SudokuView::FrameResized(float /*width*/, float /*height*/) 289 { 290 // font for numbers 291 292 uint32 size = fField->Size(); 293 fWidth = (Bounds().Width() - kStrongLineSize * (fBlockSize - 1)) / size; 294 fHeight = (Bounds().Height() - kStrongLineSize * (fBlockSize - 1)) / size; 295 _FitFont(fFieldFont, fWidth - 2, fHeight - 2); 296 297 font_height fontHeight; 298 fFieldFont.GetHeight(&fontHeight); 299 fBaseline = ceilf(fontHeight.ascent) / 2 300 + (fHeight - ceilf(fontHeight.descent)) / 2; 301 302 // font for hint 303 304 fHintWidth = (fWidth - 2) / fBlockSize; 305 fHintHeight = (fHeight - 2) / fBlockSize; 306 _FitFont(fHintFont, fHintWidth, fHintHeight); 307 308 fHintFont.GetHeight(&fontHeight); 309 fHintBaseline = ceilf(fontHeight.ascent) / 2 310 + (fHintHeight - ceilf(fontHeight.descent)) / 2; 311 } 312 313 314 BPoint 315 SudokuView::_LeftTop(uint32 x, uint32 y) 316 { 317 return BPoint(x * fWidth + x / fBlockSize * kStrongLineSize + 1, 318 y * fHeight + y / fBlockSize * kStrongLineSize + 1); 319 } 320 321 322 BRect 323 SudokuView::_Frame(uint32 x, uint32 y) 324 { 325 BPoint leftTop = _LeftTop(x, y); 326 BPoint rightBottom = leftTop + BPoint(fWidth - 2, fHeight - 2); 327 328 return BRect(leftTop, rightBottom); 329 } 330 331 332 void 333 SudokuView::_InvalidateHintField(uint32 x, uint32 y, uint32 hintX, 334 uint32 hintY) 335 { 336 BPoint leftTop = _LeftTop(x, y); 337 leftTop.x += hintX * fHintWidth; 338 leftTop.y += hintY * fHintHeight; 339 BPoint rightBottom = leftTop; 340 rightBottom.x += fHintWidth; 341 rightBottom.y += fHintHeight; 342 343 Invalidate(BRect(leftTop, rightBottom)); 344 } 345 346 347 void 348 SudokuView::_InvalidateField(uint32 x, uint32 y) 349 { 350 Invalidate(_Frame(x, y)); 351 } 352 353 354 void 355 SudokuView::_InvalidateKeyboardFocus(uint32 x, uint32 y) 356 { 357 BRect frame = _Frame(x, y); 358 frame.InsetBy(-1, -1); 359 Invalidate(frame); 360 } 361 362 363 bool 364 SudokuView::_GetHintFieldFor(BPoint where, uint32 x, uint32 y, 365 uint32& hintX, uint32& hintY) 366 { 367 BPoint leftTop = _LeftTop(x, y); 368 hintX = (uint32)floor((where.x - leftTop.x) / fHintWidth); 369 hintY = (uint32)floor((where.y - leftTop.y) / fHintHeight); 370 371 if (hintX >= fBlockSize || hintY >= fBlockSize) 372 return false; 373 374 return true; 375 } 376 377 378 bool 379 SudokuView::_GetFieldFor(BPoint where, uint32& x, uint32& y) 380 { 381 float block = fWidth * fBlockSize + kStrongLineSize; 382 x = (uint32)floor(where.x / block); 383 uint32 offsetX = (uint32)floor((where.x - x * block) / fWidth); 384 x = x * fBlockSize + offsetX; 385 386 block = fHeight * fBlockSize + kStrongLineSize; 387 y = (uint32)floor(where.y / block); 388 uint32 offsetY = (uint32)floor((where.y - y * block) / fHeight); 389 y = y * fBlockSize + offsetY; 390 391 if (offsetX >= fBlockSize || offsetY >= fBlockSize 392 || x >= fField->Size() || y >= fField->Size()) 393 return false; 394 395 return true; 396 } 397 398 399 void 400 SudokuView::_RemoveHint() 401 { 402 if (fShowHintX == ~0UL) 403 return; 404 405 uint32 x = fShowHintX; 406 uint32 y = fShowHintY; 407 fShowHintX = ~0; 408 fShowHintY = ~0; 409 410 _InvalidateField(x, y); 411 } 412 413 414 void 415 SudokuView::MouseDown(BPoint where) 416 { 417 uint32 x, y; 418 if (!fEditable || !_GetFieldFor(where, x, y)) 419 return; 420 421 int32 buttons = B_PRIMARY_MOUSE_BUTTON; 422 int32 clicks = 1; 423 if (Looper() != NULL && Looper()->CurrentMessage() != NULL) { 424 Looper()->CurrentMessage()->FindInt32("buttons", &buttons); 425 Looper()->CurrentMessage()->FindInt32("clicks", &clicks); 426 } 427 428 uint32 hintX, hintY; 429 if (!_GetHintFieldFor(where, x, y, hintX, hintY)) 430 return; 431 432 uint32 value = hintX + hintY * fBlockSize; 433 uint32 field = x + y * fField->Size(); 434 435 if (clicks == 2 && fLastHintValue == value && fLastField == field 436 || (buttons & (B_SECONDARY_MOUSE_BUTTON 437 | B_TERTIARY_MOUSE_BUTTON)) != 0) { 438 // double click 439 if ((fField->FlagsAt(x, y) & kInitialValue) == 0) { 440 if (fField->ValueAt(x, y) > 0) { 441 fField->SetValueAt(x, y, 0); 442 fShowHintX = x; 443 fShowHintY = y; 444 } else { 445 fField->SetValueAt(x, y, value + 1); 446 BMessenger(this).SendMessage(kMsgCheckSolved); 447 } 448 449 _InvalidateField(x, y); 450 } 451 return; 452 } 453 454 uint32 hintMask = fField->HintMaskAt(x, y); 455 uint32 valueMask = 1UL << value; 456 if (hintMask & valueMask) 457 hintMask &= ~valueMask; 458 else 459 hintMask |= valueMask; 460 461 fField->SetHintMaskAt(x, y, hintMask); 462 _InvalidateHintField(x, y, hintX, hintY); 463 464 fLastHintValue = value; 465 fLastField = field; 466 } 467 468 469 void 470 SudokuView::MouseMoved(BPoint where, uint32 transit, 471 const BMessage* dragMessage) 472 { 473 if (transit == B_EXITED_VIEW || dragMessage != NULL) { 474 _RemoveHint(); 475 return; 476 } 477 478 if (fShowKeyboardFocus) { 479 fShowKeyboardFocus = false; 480 _InvalidateKeyboardFocus(fKeyboardX, fKeyboardY); 481 } 482 483 uint32 x, y; 484 if (!_GetFieldFor(where, x, y) 485 || (fField->FlagsAt(x, y) & kInitialValue) != 0 486 || !fShowCursor && fField->ValueAt(x, y) != 0) { 487 _RemoveHint(); 488 return; 489 } 490 491 if (fShowHintX == x && fShowHintY == y) 492 return; 493 494 _RemoveHint(); 495 fShowHintX = x; 496 fShowHintY = y; 497 _InvalidateField(x, y); 498 } 499 500 501 void 502 SudokuView::_InsertKey(char rawKey, int32 modifiers) 503 { 504 if (!fEditable || !_ValidCharacter(rawKey) 505 || (fField->FlagsAt(fKeyboardX, fKeyboardY) & kInitialValue) != 0) 506 return; 507 508 uint32 value = rawKey - _BaseCharacter(); 509 510 if (modifiers & (B_SHIFT_KEY | B_OPTION_KEY)) { 511 // set or remove hint 512 if (value == 0) 513 return; 514 515 uint32 hintMask = fField->HintMaskAt(fKeyboardX, fKeyboardY); 516 uint32 valueMask = 1UL << (value - 1); 517 if (modifiers & B_OPTION_KEY) 518 hintMask &= ~valueMask; 519 else 520 hintMask |= valueMask; 521 522 fField->SetValueAt(fKeyboardX, fKeyboardY, 0); 523 fField->SetHintMaskAt(fKeyboardX, fKeyboardY, hintMask); 524 } else { 525 fField->SetValueAt(fKeyboardX, fKeyboardY, value); 526 if (value) 527 BMessenger(this).SendMessage(kMsgCheckSolved); 528 } 529 } 530 531 532 void 533 SudokuView::KeyDown(const char *bytes, int32 /*numBytes*/) 534 { 535 be_app->ObscureCursor(); 536 537 uint32 x = fKeyboardX, y = fKeyboardY; 538 539 switch (bytes[0]) { 540 case B_UP_ARROW: 541 if (fKeyboardY == 0) 542 fKeyboardY = fField->Size() - 1; 543 else 544 fKeyboardY--; 545 break; 546 case B_DOWN_ARROW: 547 if (fKeyboardY == fField->Size() - 1) 548 fKeyboardY = 0; 549 else 550 fKeyboardY++; 551 break; 552 553 case B_LEFT_ARROW: 554 if (fKeyboardX == 0) 555 fKeyboardX = fField->Size() - 1; 556 else 557 fKeyboardX--; 558 break; 559 case B_RIGHT_ARROW: 560 if (fKeyboardX == fField->Size() - 1) 561 fKeyboardX = 0; 562 else 563 fKeyboardX++; 564 break; 565 566 case B_BACKSPACE: 567 case B_DELETE: 568 case B_SPACE: 569 // clear value 570 _InsertKey(_BaseCharacter(), 0); 571 break; 572 573 default: 574 int32 rawKey = bytes[0]; 575 int32 modifiers = 0; 576 if (Looper() != NULL && Looper()->CurrentMessage() != NULL) { 577 Looper()->CurrentMessage()->FindInt32("raw_char", &rawKey); 578 Looper()->CurrentMessage()->FindInt32("modifiers", &modifiers); 579 } 580 581 _InsertKey(rawKey, modifiers); 582 break; 583 } 584 585 if (!fShowKeyboardFocus && fShowHintX != ~0UL) { 586 // always start at last mouse position, if any 587 fKeyboardX = fShowHintX; 588 fKeyboardY = fShowHintY; 589 } 590 591 _RemoveHint(); 592 593 // remove old focus, if any 594 if (fShowKeyboardFocus && (x != fKeyboardX || y != fKeyboardY)) 595 _InvalidateKeyboardFocus(x, y); 596 597 fShowKeyboardFocus = true; 598 _InvalidateKeyboardFocus(fKeyboardX, fKeyboardY); 599 } 600 601 602 void 603 SudokuView::MessageReceived(BMessage* message) 604 { 605 switch (message->what) { 606 case kMsgCheckSolved: 607 if (fField->IsSolved()) { 608 // notify window 609 Looper()->PostMessage(kMsgSudokuSolved); 610 } 611 break; 612 613 case kMsgSolveSudoku: 614 { 615 SudokuSolver solver; 616 solver.SetTo(fField); 617 bigtime_t start = system_time(); 618 solver.ComputeSolutions(); 619 printf("found %ld solutions in %g msecs\n", 620 solver.CountSolutions(), (system_time() - start) / 1000.0); 621 if (solver.CountSolutions() > 0) { 622 fField->SetTo(solver.SolutionAt(0)); 623 Invalidate(); 624 } else 625 beep(); 626 break; 627 } 628 629 case kMsgSolveSingle: 630 { 631 if (fField->IsSolved()) { 632 beep(); 633 break; 634 } 635 636 SudokuSolver solver; 637 solver.SetTo(fField); 638 bigtime_t start = system_time(); 639 solver.ComputeSolutions(); 640 printf("found %ld solutions in %g msecs\n", 641 solver.CountSolutions(), (system_time() - start) / 1000.0); 642 if (solver.CountSolutions() > 0) { 643 // find free spot 644 uint32 x, y; 645 do { 646 x = rand() % fField->Size(); 647 y = rand() % fField->Size(); 648 } while (fField->ValueAt(x, y)); 649 650 fField->SetValueAt(x, y, 651 solver.SolutionAt(0)->ValueAt(x, y)); 652 _InvalidateField(x, y); 653 } else 654 beep(); 655 break; 656 } 657 658 default: 659 BView::MessageReceived(message); 660 break; 661 } 662 } 663 664 665 char 666 SudokuView::_BaseCharacter() 667 { 668 return fField->Size() > 9 ? '@' : '0'; 669 } 670 671 672 bool 673 SudokuView::_ValidCharacter(char c) 674 { 675 char min = _BaseCharacter(); 676 char max = min + fField->Size(); 677 return c >= min && c <= max; 678 } 679 680 681 void 682 SudokuView::_SetText(char* text, uint32 value) 683 { 684 text[0] = value + _BaseCharacter(); 685 text[1] = '\0'; 686 } 687 688 689 void 690 SudokuView::_DrawKeyboardFocus() 691 { 692 BRect frame = _Frame(fKeyboardX, fKeyboardY); 693 SetHighColor(ui_color(B_KEYBOARD_NAVIGATION_COLOR)); 694 StrokeRect(frame); 695 frame.InsetBy(-1, -1); 696 StrokeRect(frame); 697 frame.InsetBy(2, 2); 698 StrokeRect(frame); 699 } 700 701 702 void 703 SudokuView::_DrawHints(uint32 x, uint32 y) 704 { 705 bool showAll = fShowHintX == x && fShowHintY == y; 706 uint32 hintMask = fField->HintMaskAt(x, y); 707 if (hintMask == 0 && !showAll) 708 return; 709 710 uint32 validMask = fField->ValidMaskAt(x, y); 711 BPoint leftTop = _LeftTop(x, y); 712 SetFont(&fHintFont); 713 714 for (uint32 j = 0; j < fBlockSize; j++) { 715 for (uint32 i = 0; i < fBlockSize; i++) { 716 uint32 value = j * fBlockSize + i; 717 if (hintMask & (1UL << value)) 718 SetHighColor(200, 0, 0); 719 else { 720 if (!showAll) 721 continue; 722 723 if ((fHintFlags & kMarkValidHints) == 0 724 || validMask & (1UL << value)) 725 SetHighColor(110, 110, 80); 726 else 727 SetHighColor(180, 180, 120); 728 } 729 730 char text[2]; 731 _SetText(text, value + 1); 732 DrawString(text, leftTop + BPoint((i + 0.5f) * fHintWidth 733 - StringWidth(text) / 2, floorf(j * fHintHeight) 734 + fHintBaseline)); 735 } 736 } 737 } 738 739 740 void 741 SudokuView::Draw(BRect /*updateRect*/) 742 { 743 // draw one pixel border otherwise not covered 744 // by lines and fields 745 SetLowColor(fBackgroundColor); 746 StrokeRect(Bounds(), B_SOLID_LOW); 747 748 // draw lines 749 750 uint32 size = fField->Size(); 751 752 SetHighColor(0, 0, 0); 753 754 float width = fWidth; 755 for (uint32 x = 1; x < size; x++) { 756 if (x % fBlockSize == 0) { 757 FillRect(BRect(width, 0, width + kStrongLineSize, 758 Bounds().Height())); 759 width += kStrongLineSize; 760 } else { 761 StrokeLine(BPoint(width, 0), BPoint(width, Bounds().Height())); 762 } 763 width += fWidth; 764 } 765 766 float height = fHeight; 767 for (uint32 y = 1; y < size; y++) { 768 if (y % fBlockSize == 0) { 769 FillRect(BRect(0, height, Bounds().Width(), 770 height + kStrongLineSize)); 771 height += kStrongLineSize; 772 } else { 773 StrokeLine(BPoint(0, height), BPoint(Bounds().Width(), height)); 774 } 775 height += fHeight; 776 } 777 778 // draw text 779 780 for (uint32 y = 0; y < size; y++) { 781 for (uint32 x = 0; x < size; x++) { 782 if ((fShowCursor && x == fShowHintX && y == fShowHintY 783 || fShowKeyboardFocus && x == fKeyboardX 784 && y == fKeyboardY) 785 && (fField->FlagsAt(x, y) & kInitialValue) == 0) { 786 //SetLowColor(tint_color(ViewColor(), B_DARKEN_1_TINT)); 787 SetLowColor(255, 255, 210); 788 FillRect(_Frame(x, y), B_SOLID_LOW); 789 } else { 790 SetLowColor(fBackgroundColor); 791 FillRect(_Frame(x, y), B_SOLID_LOW); 792 } 793 794 if (fShowKeyboardFocus && x == fKeyboardX && y == fKeyboardY) 795 _DrawKeyboardFocus(); 796 797 uint32 value = fField->ValueAt(x, y); 798 if (value == 0) { 799 _DrawHints(x, y); 800 continue; 801 } 802 803 SetFont(&fFieldFont); 804 if (fField->FlagsAt(x, y) & kInitialValue) 805 SetHighColor(0, 0, 0); 806 else { 807 if ((fHintFlags & kMarkInvalid) == 0 808 || fField->ValidMaskAt(x, y) & (1UL << (value - 1))) 809 SetHighColor(0, 0, 200); 810 else 811 SetHighColor(200, 0, 0); 812 } 813 814 char text[2]; 815 _SetText(text, value); 816 DrawString(text, _LeftTop(x, y) 817 + BPoint((fWidth - StringWidth(text)) / 2, fBaseline)); 818 } 819 } 820 } 821 822 823