1 /* 2 * Copyright 2007-2015, Axel Dörfler, axeld@pinc-software.de. 3 * Distributed under the terms of the MIT License. 4 */ 5 6 7 #include "SudokuView.h" 8 9 #include "Sudoku.h" 10 #include "SudokuField.h" 11 #include "SudokuSolver.h" 12 13 #include <ctype.h> 14 #include <errno.h> 15 #include <stdio.h> 16 #include <stdlib.h> 17 18 #include <Application.h> 19 #include <Beep.h> 20 #include <Bitmap.h> 21 #include <Clipboard.h> 22 #include <DataIO.h> 23 #include <Dragger.h> 24 #include <File.h> 25 #include <NodeInfo.h> 26 #include <Path.h> 27 #include <Picture.h> 28 #include <String.h> 29 30 31 static const uint32 kMsgCheckSolved = 'chks'; 32 33 static const uint32 kStrongLineSize = 2; 34 35 static const rgb_color kBackgroundColor = {255, 255, 240}; 36 static const rgb_color kHintColor = {255, 115, 0}; 37 static const rgb_color kValueColor = {0, 91, 162}; 38 static const rgb_color kValueCompletedColor = {55, 140, 35}; 39 static const rgb_color kInvalidValueColor = {200, 0, 0}; 40 static const rgb_color kValueHintBackgroundColor = {255, 215, 127}; 41 static const rgb_color kHintValueHintBackgroundColor = {255, 235, 185}; 42 43 extern const char* kSignature; 44 45 46 SudokuView::SudokuView(BRect frame, const char* name, 47 const BMessage& settings, uint32 resizingMode) 48 : 49 BView(frame, name, resizingMode, 50 B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS) 51 { 52 _InitObject(&settings); 53 54 #if 0 55 BRect rect(Bounds()); 56 rect.top = rect.bottom - 7; 57 rect.left = rect.right - 7; 58 BDragger* dragger = new BDragger(rect, this); 59 AddChild(dragger); 60 #endif 61 } 62 63 64 SudokuView::SudokuView(const char* name, const BMessage& settings) 65 : 66 BView(name, 67 B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS) 68 { 69 _InitObject(&settings); 70 } 71 72 73 SudokuView::SudokuView(BMessage* archive) 74 : 75 BView(archive) 76 { 77 _InitObject(archive); 78 } 79 80 81 SudokuView::~SudokuView() 82 { 83 delete fField; 84 } 85 86 87 status_t 88 SudokuView::Archive(BMessage* into, bool deep) const 89 { 90 status_t status; 91 92 status = BView::Archive(into, deep); 93 if (status < B_OK) 94 return status; 95 96 status = into->AddString("add_on", kSignature); 97 if (status < B_OK) 98 return status; 99 100 status = into->AddRect("bounds", Bounds()); 101 if (status < B_OK) 102 return status; 103 104 status = SaveState(*into); 105 if (status < B_OK) 106 return status; 107 return B_OK; 108 } 109 110 111 BArchivable* 112 SudokuView::Instantiate(BMessage* archive) 113 { 114 if (!validate_instantiation(archive, "SudokuView")) 115 return NULL; 116 return new SudokuView(archive); 117 } 118 119 120 status_t 121 SudokuView::SaveState(BMessage& state) const 122 { 123 BMessage field; 124 status_t status = fField->Archive(&field, true); 125 if (status == B_OK) 126 status = state.AddMessage("field", &field); 127 if (status == B_OK) 128 status = state.AddInt32("hint flags", fHintFlags); 129 if (status == B_OK) 130 status = state.AddBool("show cursor", fShowCursor); 131 132 return status; 133 } 134 135 136 status_t 137 SudokuView::SetTo(entry_ref& ref) 138 { 139 BPath path; 140 status_t status = path.SetTo(&ref); 141 if (status < B_OK) 142 return status; 143 144 FILE* file = fopen(path.Path(), "r"); 145 if (file == NULL) 146 return errno; 147 148 uint32 maxOut = fField->Size() * fField->Size(); 149 char buffer[1024]; 150 char line[1024]; 151 bool ignore = false; 152 uint32 out = 0; 153 154 while (fgets(line, sizeof(line), file) != NULL 155 && out < maxOut) { 156 status = _FilterString(line, sizeof(line), buffer, out, ignore); 157 if (status < B_OK) { 158 fclose(file); 159 return status; 160 } 161 } 162 163 _PushUndo(); 164 status = fField->SetTo(_BaseCharacter(), buffer); 165 fValueHintValue = UINT32_MAX; 166 Invalidate(); 167 fclose(file); 168 return status; 169 } 170 171 172 status_t 173 SudokuView::SetTo(const char* data) 174 { 175 if (data == NULL) 176 return B_BAD_VALUE; 177 178 char buffer[1024]; 179 bool ignore = false; 180 uint32 out = 0; 181 182 status_t status = _FilterString(data, 65536, buffer, out, ignore); 183 if (status < B_OK) 184 return B_BAD_VALUE; 185 186 _PushUndo(); 187 status = fField->SetTo(_BaseCharacter(), buffer); 188 fValueHintValue = UINT32_MAX; 189 Invalidate(); 190 return status; 191 } 192 193 194 status_t 195 SudokuView::SetTo(SudokuField* field) 196 { 197 if (field == NULL || field == fField) 198 return B_BAD_VALUE; 199 200 _PushUndo(); 201 delete fField; 202 fField = field; 203 204 fBlockSize = fField->BlockSize(); 205 fValueHintValue = UINT32_MAX; 206 FrameResized(0, 0); 207 Invalidate(); 208 return B_OK; 209 } 210 211 212 status_t 213 SudokuView::SaveTo(entry_ref& ref, uint32 exportAs) 214 { 215 BFile file; 216 status_t status = file.SetTo(&ref, B_WRITE_ONLY | B_CREATE_FILE 217 | B_ERASE_FILE); 218 if (status < B_OK) 219 return status; 220 221 return SaveTo(file, exportAs); 222 } 223 224 225 status_t 226 SudokuView::SaveTo(BDataIO& stream, uint32 exportAs) 227 { 228 BFile* file = dynamic_cast<BFile*>(&stream); 229 uint32 i = 0; 230 BNodeInfo nodeInfo; 231 232 if (file) 233 nodeInfo.SetTo(file); 234 235 switch (exportAs) { 236 case kExportAsText: 237 { 238 BString text = "# Written by Sudoku\n\n"; 239 stream.Write(text.String(), text.Length()); 240 241 char* line = text.LockBuffer(1024); 242 memset(line, 0, 1024); 243 for (uint32 y = 0; y < fField->Size(); y++) { 244 for (uint32 x = 0; x < fField->Size(); x++) { 245 if (x != 0 && x % fBlockSize == 0) 246 line[i++] = ' '; 247 _SetText(&line[i++], fField->ValueAt(x, y)); 248 } 249 line[i++] = '\n'; 250 } 251 text.UnlockBuffer(); 252 253 stream.Write(text.String(), text.Length()); 254 if (file) 255 nodeInfo.SetType("text/plain"); 256 return B_OK; 257 } 258 259 case kExportAsHTML: 260 { 261 bool netPositiveFriendly = false; 262 BString text = "<html>\n<head>\n<!-- Written by Sudoku -->\n" 263 "<style type=\"text/css\">\n" 264 "table.sudoku { background: #000000; border:0; border-collapse: " 265 "collapse; cellpadding: 10px; cellspacing: 10px; width: " 266 "300px; height: 300px; }\n" 267 "td.sudoku { background: #ffffff; border-color: black; " 268 "border-left: none ; border-top: none ; /*border: none;*/ " 269 "text-align: center; }\n" 270 "td.sudoku_initial { }\n" 271 "td.sudoku_filled { color: blue; }\n" 272 "td.sudoku_empty { }\n"; 273 274 // border styles: right bottom (none, small or large) 275 const char* kStyles[] = {"none", "small", "large"}; 276 const char* kCssStyles[] = {"none", "solid 1px black", 277 "solid 3px black"}; 278 enum style_type { kNone = 0, kSmall, kLarge }; 279 280 for (int32 right = 0; right < 3; right++) { 281 for (int32 bottom = 0; bottom < 3; bottom++) { 282 text << "td.sudoku_"; 283 if (right != bottom) 284 text << kStyles[right] << "_" << kStyles[bottom]; 285 else 286 text << kStyles[right]; 287 288 text << " { border-right: " << kCssStyles[right] 289 << "; border-bottom: " << kCssStyles[bottom] << "; }\n"; 290 } 291 } 292 293 text << "</style>\n" 294 "</head>\n<body>\n\n"; 295 stream.Write(text.String(), text.Length()); 296 297 text = "<table"; 298 if (netPositiveFriendly) 299 text << " border=\"1\""; 300 text << " class=\"sudoku\">"; 301 stream.Write(text.String(), text.Length()); 302 303 text = ""; 304 BString divider; 305 divider << (int)(100.0 / fField->Size()) << "%"; 306 for (uint32 y = 0; y < fField->Size(); y++) { 307 text << "<tr height=\"" << divider << "\">\n"; 308 for (uint32 x = 0; x < fField->Size(); x++) { 309 char buff[2]; 310 _SetText(buff, fField->ValueAt(x, y)); 311 312 BString style = "sudoku_"; 313 style_type right = kSmall; 314 style_type bottom = kSmall; 315 if ((x + 1) % fField->BlockSize() == 0) 316 right = kLarge; 317 if ((y + 1) % fField->BlockSize() == 0) 318 bottom = kLarge; 319 if (x == fField->Size() - 1) 320 right = kNone; 321 if (y == fField->Size() - 1) 322 bottom = kNone; 323 324 if (right != bottom) 325 style << kStyles[right] << "_" << kStyles[bottom]; 326 else 327 style << kStyles[right]; 328 329 if (fField->ValueAt(x, y) == 0) { 330 text << "<td width=\"" << divider << "\" "; 331 text << "class=\"sudoku sudoku_empty " << style; 332 text << "\">\n "; 333 } else if (fField->IsInitialValue(x, y)) { 334 text << "<td width=\"" << divider << "\" "; 335 text << "class=\"sudoku sudoku_initial " << style 336 << "\">\n"; 337 if (netPositiveFriendly) 338 text << "<font color=\"#000000\">"; 339 text << buff; 340 if (netPositiveFriendly) 341 text << "</font>"; 342 } else { 343 text << "<td width=\"" << divider << "\" "; 344 text << "class=\"sudoku sudoku_filled sudoku_" << style 345 << "\">\n"; 346 if (netPositiveFriendly) 347 text << "<font color=\"#0000ff\">"; 348 text << buff; 349 if (netPositiveFriendly) 350 text << "</font>"; 351 } 352 text << "</td>\n"; 353 } 354 text << "</tr>\n"; 355 } 356 text << "</table>\n\n"; 357 358 stream.Write(text.String(), text.Length()); 359 text = "</body></html>\n"; 360 stream.Write(text.String(), text.Length()); 361 if (file) 362 nodeInfo.SetType("text/html"); 363 return B_OK; 364 } 365 366 case kExportAsBitmap: 367 { 368 BMallocIO mallocIO; 369 status_t status = SaveTo(mallocIO, kExportAsPicture); 370 if (status < B_OK) 371 return status; 372 373 mallocIO.Seek(0LL, SEEK_SET); 374 BPicture picture; 375 status = picture.Unflatten(&mallocIO); 376 if (status < B_OK) 377 return status; 378 379 BBitmap* bitmap = new BBitmap(Bounds(), B_BITMAP_ACCEPTS_VIEWS, 380 B_RGB32); 381 BView* view = new BView(Bounds(), "bitmap", B_FOLLOW_NONE, 382 B_WILL_DRAW); 383 bitmap->AddChild(view); 384 385 if (bitmap->Lock()) { 386 view->DrawPicture(&picture); 387 view->Sync(); 388 389 view->RemoveSelf(); 390 delete view; 391 // it should not become part of the archive 392 bitmap->Unlock(); 393 } 394 395 BMessage archive; 396 status = bitmap->Archive(&archive); 397 if (status >= B_OK) 398 status = archive.Flatten(&stream); 399 400 delete bitmap; 401 return status; 402 } 403 404 case kExportAsPicture: 405 { 406 BPicture picture; 407 BeginPicture(&picture); 408 Draw(Bounds()); 409 410 status_t status = B_ERROR; 411 if (EndPicture()) 412 status = picture.Flatten(&stream); 413 414 return status; 415 } 416 417 default: 418 return B_BAD_VALUE; 419 } 420 } 421 422 423 status_t 424 SudokuView::CopyToClipboard() 425 { 426 if (!be_clipboard->Lock()) 427 return B_ERROR; 428 429 be_clipboard->Clear(); 430 431 BMessage* clip = be_clipboard->Data(); 432 if (clip == NULL) { 433 be_clipboard->Unlock(); 434 return B_ERROR; 435 } 436 437 // As BBitmap 438 BMallocIO mallocIO; 439 status_t status = SaveTo(mallocIO, kExportAsBitmap); 440 if (status >= B_OK) { 441 mallocIO.Seek(0LL, SEEK_SET); 442 // ShowImage, ArtPaint & WonderBrush use that 443 status = clip->AddData("image/bitmap", B_MESSAGE_TYPE, 444 mallocIO.Buffer(), mallocIO.BufferLength()); 445 // Becasso uses that ? 446 clip->AddData("image/x-be-bitmap", B_MESSAGE_TYPE, mallocIO.Buffer(), 447 mallocIO.BufferLength()); 448 // Gobe Productive uses that... 449 // QuickRes as well, with a rect field. 450 clip->AddData("image/x-vnd.Be-bitmap", B_MESSAGE_TYPE, 451 mallocIO.Buffer(), mallocIO.BufferLength()); 452 } 453 mallocIO.Seek(0LL, SEEK_SET); 454 mallocIO.SetSize(0LL); 455 456 // As HTML 457 if (status >= B_OK) 458 status = SaveTo(mallocIO, kExportAsHTML); 459 if (status >= B_OK) { 460 status = clip->AddData("text/html", B_MIME_TYPE, mallocIO.Buffer(), 461 mallocIO.BufferLength()); 462 } 463 mallocIO.Seek(0LL, SEEK_SET); 464 mallocIO.SetSize(0LL); 465 466 // As plain text 467 if (status >= B_OK) 468 SaveTo(mallocIO, kExportAsText); 469 if (status >= B_OK) { 470 status = clip->AddData("text/plain", B_MIME_TYPE, mallocIO.Buffer(), 471 mallocIO.BufferLength()); 472 } 473 mallocIO.Seek(0LL, SEEK_SET); 474 mallocIO.SetSize(0LL); 475 476 // As flattened BPicture, anyone handles that? 477 if (status >= B_OK) 478 status = SaveTo(mallocIO, kExportAsPicture); 479 if (status >= B_OK) { 480 status = clip->AddData("image/x-vnd.Be-picture", B_MIME_TYPE, 481 mallocIO.Buffer(), mallocIO.BufferLength()); 482 } 483 mallocIO.SetSize(0LL); 484 485 be_clipboard->Commit(); 486 be_clipboard->Unlock(); 487 488 return status; 489 } 490 491 492 void 493 SudokuView::ClearChanged() 494 { 495 _PushUndo(); 496 497 for (uint32 y = 0; y < fField->Size(); y++) { 498 for (uint32 x = 0; x < fField->Size(); x++) { 499 if (!fField->IsInitialValue(x, y)) 500 fField->SetValueAt(x, y, 0); 501 } 502 } 503 504 Invalidate(); 505 } 506 507 508 void 509 SudokuView::ClearAll() 510 { 511 _PushUndo(); 512 fField->Reset(); 513 Invalidate(); 514 } 515 516 517 void 518 SudokuView::SetHintFlags(uint32 flags) 519 { 520 if (flags == fHintFlags) 521 return; 522 523 if ((flags & kMarkInvalid) ^ (fHintFlags & kMarkInvalid)) 524 Invalidate(); 525 526 fHintFlags = flags; 527 } 528 529 530 void 531 SudokuView::SetEditable(bool editable) 532 { 533 fEditable = editable; 534 } 535 536 537 void 538 SudokuView::Undo() 539 { 540 _UndoRedo(fUndos, fRedos); 541 } 542 543 544 void 545 SudokuView::Redo() 546 { 547 _UndoRedo(fRedos, fUndos); 548 } 549 550 551 // #pragma mark - BWindow methods 552 553 554 void 555 SudokuView::AttachedToWindow() 556 { 557 MakeFocus(true); 558 } 559 560 561 void 562 SudokuView::FrameResized(float /*width*/, float /*height*/) 563 { 564 // font for numbers 565 566 uint32 size = fField->Size(); 567 fWidth = (Bounds().Width() + 2 - kStrongLineSize * (fBlockSize - 1)) / size; 568 fHeight = (Bounds().Height() + 2 - kStrongLineSize * (fBlockSize - 1)) 569 / size; 570 _FitFont(fFieldFont, fWidth - 2, fHeight - 2); 571 572 font_height fontHeight; 573 fFieldFont.GetHeight(&fontHeight); 574 fBaseline = ceilf(fontHeight.ascent) / 2 575 + (fHeight - ceilf(fontHeight.descent)) / 2; 576 577 // font for hint 578 579 fHintWidth = (fWidth - 2) / fBlockSize; 580 fHintHeight = (fHeight - 2) / fBlockSize; 581 _FitFont(fHintFont, fHintWidth, fHintHeight); 582 583 fHintFont.GetHeight(&fontHeight); 584 fHintBaseline = ceilf(fontHeight.ascent) / 2 585 + (fHintHeight - ceilf(fontHeight.descent)) / 2; 586 587 // fix the dragger's position 588 BView *dragger = FindView("_dragger_"); 589 if (dragger) 590 dragger->MoveTo(Bounds().right - 7, Bounds().bottom - 7); 591 } 592 593 594 void 595 SudokuView::MouseDown(BPoint where) 596 { 597 uint32 x, y; 598 if (!fEditable || !_GetFieldFor(where, x, y)) 599 return; 600 601 int32 buttons = B_PRIMARY_MOUSE_BUTTON; 602 int32 clicks = 1; 603 if (Looper() != NULL && Looper()->CurrentMessage() != NULL) { 604 Looper()->CurrentMessage()->FindInt32("buttons", &buttons); 605 Looper()->CurrentMessage()->FindInt32("clicks", &clicks); 606 } 607 608 if (buttons == B_PRIMARY_MOUSE_BUTTON && clicks == 1) { 609 uint32 value = fField->ValueAt(x, y); 610 if (value != 0) { 611 _SetValueHintValue(value); 612 return; 613 } 614 } 615 616 uint32 hintX, hintY; 617 if (!_GetHintFieldFor(where, x, y, hintX, hintY)) 618 return; 619 620 uint32 value = hintX + hintY * fBlockSize; 621 uint32 field = x + y * fField->Size(); 622 _PushUndo(); 623 _SetValueHintValue(value + 1); 624 625 if ((clicks == 2 && fLastHintValue == value && fLastField == field) 626 || (buttons & (B_SECONDARY_MOUSE_BUTTON 627 | B_TERTIARY_MOUSE_BUTTON)) != 0) { 628 // Double click or other buttons set or remove a value 629 if (!fField->IsInitialValue(x, y)) 630 _SetValue(x, y, fField->ValueAt(x, y) == 0 ? value + 1 : 0); 631 632 return; 633 } 634 635 _ToggleHintValue(x, y, hintX, hintY, value, field); 636 } 637 638 639 void 640 SudokuView::MouseMoved(BPoint where, uint32 transit, 641 const BMessage* dragMessage) 642 { 643 if (transit == B_EXITED_VIEW || dragMessage != NULL) { 644 _RemoveHint(); 645 return; 646 } 647 648 if (fShowKeyboardFocus) { 649 fShowKeyboardFocus = false; 650 _InvalidateKeyboardFocus(fKeyboardX, fKeyboardY); 651 } 652 653 uint32 x, y; 654 bool isField = _GetFieldFor(where, x, y); 655 if (isField) { 656 fKeyboardX = x; 657 fKeyboardY = y; 658 } 659 660 if (!isField 661 || fField->IsInitialValue(x, y) 662 || (!fShowCursor && fField->ValueAt(x, y) != 0)) { 663 _RemoveHint(); 664 return; 665 } 666 667 if (fShowHintX == x && fShowHintY == y) 668 return; 669 670 int32 buttons = 0; 671 if (Looper() != NULL && Looper()->CurrentMessage() != NULL) 672 Looper()->CurrentMessage()->FindInt32("buttons", &buttons); 673 674 uint32 field = x + y * fField->Size(); 675 676 if (buttons != 0 && field != fLastField) { 677 // if a button is pressed, we drag the last hint selection 678 // (either set or removal) to the field under the mouse 679 uint32 hintMask = fField->HintMaskAt(x, y); 680 uint32 valueMask = 1UL << fLastHintValue; 681 if (fLastHintValueSet) 682 hintMask |= valueMask; 683 else 684 hintMask &= ~valueMask; 685 686 fField->SetHintMaskAt(x, y, hintMask); 687 } 688 689 _RemoveHint(); 690 fShowHintX = x; 691 fShowHintY = y; 692 _InvalidateField(x, y); 693 } 694 695 696 void 697 SudokuView::KeyDown(const char *bytes, int32 /*numBytes*/) 698 { 699 be_app->ObscureCursor(); 700 701 uint32 x = fKeyboardX, y = fKeyboardY; 702 703 switch (bytes[0]) { 704 case B_UP_ARROW: 705 if (fKeyboardY == 0) 706 fKeyboardY = fField->Size() - 1; 707 else 708 fKeyboardY--; 709 break; 710 case B_DOWN_ARROW: 711 if (fKeyboardY == fField->Size() - 1) 712 fKeyboardY = 0; 713 else 714 fKeyboardY++; 715 break; 716 717 case B_LEFT_ARROW: 718 if (fKeyboardX == 0) 719 fKeyboardX = fField->Size() - 1; 720 else 721 fKeyboardX--; 722 break; 723 case B_RIGHT_ARROW: 724 if (fKeyboardX == fField->Size() - 1) 725 fKeyboardX = 0; 726 else 727 fKeyboardX++; 728 break; 729 730 case B_BACKSPACE: 731 case B_DELETE: 732 case B_SPACE: 733 // clear value 734 _InsertKey(_BaseCharacter(), 0); 735 break; 736 737 default: 738 int32 rawKey = bytes[0]; 739 int32 modifiers = 0; 740 if (Looper() != NULL && Looper()->CurrentMessage() != NULL) { 741 Looper()->CurrentMessage()->FindInt32("raw_char", &rawKey); 742 Looper()->CurrentMessage()->FindInt32("modifiers", &modifiers); 743 } 744 745 _InsertKey(rawKey, modifiers); 746 break; 747 } 748 749 if (!fShowKeyboardFocus && fShowHintX != UINT32_MAX) { 750 // always start at last mouse position, if any 751 fKeyboardX = fShowHintX; 752 fKeyboardY = fShowHintY; 753 } 754 755 _RemoveHint(); 756 757 // remove old focus, if any 758 if (fShowKeyboardFocus && (x != fKeyboardX || y != fKeyboardY)) 759 _InvalidateKeyboardFocus(x, y); 760 761 fShowKeyboardFocus = true; 762 _InvalidateKeyboardFocus(fKeyboardX, fKeyboardY); 763 } 764 765 766 void 767 SudokuView::MessageReceived(BMessage* message) 768 { 769 switch (message->what) { 770 case kMsgCheckSolved: 771 if (fField->IsSolved()) { 772 // notify window 773 Looper()->PostMessage(kMsgSudokuSolved); 774 } 775 break; 776 777 case B_UNDO: 778 Undo(); 779 break; 780 781 case B_REDO: 782 Redo(); 783 break; 784 785 case kMsgSetAllHints: 786 _SetAllHints(); 787 break; 788 789 case kMsgSolveSudoku: 790 _Solve(); 791 break; 792 793 case kMsgSolveSingle: 794 _SolveSingle(); 795 break; 796 797 default: 798 BView::MessageReceived(message); 799 break; 800 } 801 } 802 803 804 void 805 SudokuView::Draw(BRect /*updateRect*/) 806 { 807 // draw lines 808 809 uint32 size = fField->Size(); 810 811 SetLowColor(fBackgroundColor); 812 SetHighColor(0, 0, 0); 813 814 float width = fWidth - 1; 815 for (uint32 x = 1; x < size; x++) { 816 if (x % fBlockSize == 0) { 817 FillRect(BRect(width, 0, width + kStrongLineSize, 818 Bounds().Height())); 819 width += kStrongLineSize; 820 } else { 821 StrokeLine(BPoint(width, 0), BPoint(width, Bounds().Height())); 822 } 823 width += fWidth; 824 } 825 826 float height = fHeight - 1; 827 for (uint32 y = 1; y < size; y++) { 828 if (y % fBlockSize == 0) { 829 FillRect(BRect(0, height, Bounds().Width(), 830 height + kStrongLineSize)); 831 height += kStrongLineSize; 832 } else { 833 StrokeLine(BPoint(0, height), BPoint(Bounds().Width(), height)); 834 } 835 height += fHeight; 836 } 837 838 // draw text 839 840 for (uint32 y = 0; y < size; y++) { 841 for (uint32 x = 0; x < size; x++) { 842 uint32 value = fField->ValueAt(x, y); 843 844 rgb_color backgroundColor = fBackgroundColor; 845 if (value == fValueHintValue) 846 backgroundColor = kValueHintBackgroundColor; 847 else if (value == 0 && fField->HasHint(x, y, fValueHintValue)) 848 backgroundColor = kHintValueHintBackgroundColor; 849 850 if (((fShowCursor && x == fShowHintX && y == fShowHintY) 851 || (fShowKeyboardFocus && x == fKeyboardX 852 && y == fKeyboardY)) 853 && !fField->IsInitialValue(x, y)) { 854 // TODO: make color more intense 855 SetLowColor(tint_color(backgroundColor, B_DARKEN_2_TINT)); 856 FillRect(_Frame(x, y), B_SOLID_LOW); 857 } else { 858 SetLowColor(backgroundColor); 859 FillRect(_Frame(x, y), B_SOLID_LOW); 860 } 861 862 if (fShowKeyboardFocus && x == fKeyboardX && y == fKeyboardY) 863 _DrawKeyboardFocus(); 864 865 if (value == 0) { 866 _DrawHints(x, y); 867 continue; 868 } 869 870 SetFont(&fFieldFont); 871 if (fField->IsInitialValue(x, y)) 872 SetHighColor(0, 0, 0); 873 else { 874 if ((fHintFlags & kMarkInvalid) == 0 875 || fField->IsValid(x, y, value)) { 876 if (fField->IsValueCompleted(value)) 877 SetHighColor(kValueCompletedColor); 878 else 879 SetHighColor(kValueColor); 880 } else 881 SetHighColor(kInvalidValueColor); 882 } 883 884 char text[2]; 885 _SetText(text, value); 886 DrawString(text, _LeftTop(x, y) 887 + BPoint((fWidth - StringWidth(text)) / 2, fBaseline)); 888 } 889 } 890 } 891 892 893 // #pragma mark - Private methods 894 895 896 void 897 SudokuView::_InitObject(const BMessage* archive) 898 { 899 fField = NULL; 900 fShowHintX = UINT32_MAX; 901 fValueHintValue = UINT32_MAX; 902 fLastHintValue = UINT32_MAX; 903 fLastField = UINT32_MAX; 904 fKeyboardX = 0; 905 fKeyboardY = 0; 906 fShowKeyboardFocus = false; 907 fEditable = true; 908 909 BMessage field; 910 if (archive->FindMessage("field", &field) == B_OK) { 911 fField = new SudokuField(&field); 912 if (fField->InitCheck() != B_OK) { 913 delete fField; 914 fField = NULL; 915 } else if (fField->IsSolved()) 916 ClearAll(); 917 } 918 if (fField == NULL) 919 fField = new SudokuField(3); 920 921 fBlockSize = fField->BlockSize(); 922 923 if (archive->FindInt32("hint flags", (int32*)&fHintFlags) != B_OK) 924 fHintFlags = kMarkInvalid; 925 if (archive->FindBool("show cursor", &fShowCursor) != B_OK) 926 fShowCursor = false; 927 928 SetViewColor(B_TRANSPARENT_COLOR); 929 // to avoid flickering 930 fBackgroundColor = kBackgroundColor; 931 SetLowColor(fBackgroundColor); 932 FrameResized(0, 0); 933 } 934 935 936 status_t 937 SudokuView::_FilterString(const char* data, size_t dataLength, char* buffer, 938 uint32& out, bool& ignore) 939 { 940 uint32 maxOut = fField->Size() * fField->Size(); 941 942 for (uint32 i = 0; i < dataLength && data[i]; i++) { 943 if (data[i] == '#') 944 ignore = true; 945 else if (data[i] == '\n') 946 ignore = false; 947 948 if (ignore || isspace(data[i])) 949 continue; 950 951 if (!_ValidCharacter(data[i])) { 952 return B_BAD_VALUE; 953 } 954 955 buffer[out++] = data[i]; 956 if (out == maxOut) 957 break; 958 } 959 960 buffer[out] = '\0'; 961 return B_OK; 962 } 963 964 965 void 966 SudokuView::_SetText(char* text, uint32 value) 967 { 968 text[0] = value + _BaseCharacter(); 969 text[1] = '\0'; 970 } 971 972 973 char 974 SudokuView::_BaseCharacter() 975 { 976 return fField->Size() > 9 ? '@' : '0'; 977 } 978 979 980 bool 981 SudokuView::_ValidCharacter(char c) 982 { 983 char min = _BaseCharacter(); 984 char max = min + fField->Size(); 985 return c >= min && c <= max; 986 } 987 988 989 BPoint 990 SudokuView::_LeftTop(uint32 x, uint32 y) 991 { 992 return BPoint(x * fWidth - 1 + x / fBlockSize * kStrongLineSize + 1, 993 y * fHeight - 1 + y / fBlockSize * kStrongLineSize + 1); 994 } 995 996 997 BRect 998 SudokuView::_Frame(uint32 x, uint32 y) 999 { 1000 BPoint leftTop = _LeftTop(x, y); 1001 BPoint rightBottom = leftTop + BPoint(fWidth - 2, fHeight - 2); 1002 1003 return BRect(leftTop, rightBottom); 1004 } 1005 1006 1007 void 1008 SudokuView::_InvalidateHintField(uint32 x, uint32 y, uint32 hintX, 1009 uint32 hintY) 1010 { 1011 BPoint leftTop = _LeftTop(x, y); 1012 leftTop.x += hintX * fHintWidth; 1013 leftTop.y += hintY * fHintHeight; 1014 BPoint rightBottom = leftTop; 1015 rightBottom.x += fHintWidth; 1016 rightBottom.y += fHintHeight; 1017 1018 Invalidate(BRect(leftTop, rightBottom)); 1019 } 1020 1021 1022 void 1023 SudokuView::_InvalidateField(uint32 x, uint32 y) 1024 { 1025 Invalidate(_Frame(x, y)); 1026 } 1027 1028 1029 void 1030 SudokuView::_InvalidateValue(uint32 value, bool invalidateHint, 1031 uint32 fieldX, uint32 fieldY) 1032 { 1033 for (uint32 y = 0; y < fField->Size(); y++) { 1034 for (uint32 x = 0; x < fField->Size(); x++) { 1035 if (fField->ValueAt(x, y) == value || (x == fieldX && y == fieldY)) 1036 Invalidate(_Frame(x, y)); 1037 else if (invalidateHint && fField->ValueAt(x, y) == 0 1038 && fField->HasHint(x, y, value)) 1039 Invalidate(_Frame(x, y)); 1040 } 1041 } 1042 } 1043 1044 1045 void 1046 SudokuView::_InvalidateKeyboardFocus(uint32 x, uint32 y) 1047 { 1048 BRect frame = _Frame(x, y); 1049 frame.InsetBy(-1, -1); 1050 Invalidate(frame); 1051 } 1052 1053 1054 void 1055 SudokuView::_InsertKey(char rawKey, int32 modifiers) 1056 { 1057 if (!fEditable || !_ValidCharacter(rawKey) 1058 || fField->IsInitialValue(fKeyboardX, fKeyboardY)) 1059 return; 1060 1061 uint32 value = rawKey - _BaseCharacter(); 1062 1063 if (modifiers & (B_SHIFT_KEY | B_OPTION_KEY)) { 1064 // set or remove hint 1065 if (value == 0) 1066 return; 1067 1068 _PushUndo(); 1069 uint32 hintMask = fField->HintMaskAt(fKeyboardX, fKeyboardY); 1070 uint32 valueMask = 1UL << (value - 1); 1071 if (modifiers & B_OPTION_KEY) 1072 hintMask &= ~valueMask; 1073 else 1074 hintMask |= valueMask; 1075 1076 fField->SetValueAt(fKeyboardX, fKeyboardY, 0); 1077 fField->SetHintMaskAt(fKeyboardX, fKeyboardY, hintMask); 1078 } else { 1079 _PushUndo(); 1080 _SetValue(fKeyboardX, fKeyboardY, value); 1081 } 1082 } 1083 1084 1085 bool 1086 SudokuView::_GetHintFieldFor(BPoint where, uint32 x, uint32 y, 1087 uint32& hintX, uint32& hintY) 1088 { 1089 BPoint leftTop = _LeftTop(x, y); 1090 hintX = (uint32)floor((where.x - leftTop.x) / fHintWidth); 1091 hintY = (uint32)floor((where.y - leftTop.y) / fHintHeight); 1092 1093 if (hintX >= fBlockSize || hintY >= fBlockSize) 1094 return false; 1095 1096 return true; 1097 } 1098 1099 1100 bool 1101 SudokuView::_GetFieldFor(BPoint where, uint32& x, uint32& y) 1102 { 1103 float block = fWidth * fBlockSize + kStrongLineSize; 1104 x = (uint32)floor(where.x / block); 1105 uint32 offsetX = (uint32)floor((where.x - x * block) / fWidth); 1106 x = x * fBlockSize + offsetX; 1107 1108 block = fHeight * fBlockSize + kStrongLineSize; 1109 y = (uint32)floor(where.y / block); 1110 uint32 offsetY = (uint32)floor((where.y - y * block) / fHeight); 1111 y = y * fBlockSize + offsetY; 1112 1113 if (offsetX >= fBlockSize || offsetY >= fBlockSize 1114 || x >= fField->Size() || y >= fField->Size()) 1115 return false; 1116 1117 return true; 1118 } 1119 1120 1121 void 1122 SudokuView::_SetValue(uint32 x, uint32 y, uint32 value) 1123 { 1124 bool wasCompleted; 1125 if (value == 0) { 1126 // Remove value 1127 value = fField->ValueAt(x, y); 1128 wasCompleted = fField->IsValueCompleted(value); 1129 1130 fField->SetValueAt(x, y, 0); 1131 fShowHintX = x; 1132 fShowHintY = y; 1133 } else { 1134 // Set value 1135 wasCompleted = fField->IsValueCompleted(value); 1136 1137 fField->SetValueAt(x, y, value); 1138 BMessenger(this).SendMessage(kMsgCheckSolved); 1139 1140 _RemoveHintValues(x, y, value); 1141 1142 // allow dragging to remove the hint from other fields 1143 fLastHintValueSet = false; 1144 fLastHintValue = value - 1; 1145 fLastField = x + y * fField->Size(); 1146 } 1147 1148 if (value != fValueHintValue && fValueHintValue != UINT32_MAX) 1149 _SetValueHintValue(value); 1150 1151 if (wasCompleted != fField->IsValueCompleted(value)) 1152 _InvalidateValue(value, false, x, y); 1153 else 1154 _InvalidateField(x, y); 1155 } 1156 1157 1158 void 1159 SudokuView::_ToggleHintValue(uint32 x, uint32 y, uint32 hintX, uint32 hintY, 1160 uint32 value, uint32 field) 1161 { 1162 uint32 hintMask = fField->HintMaskAt(x, y); 1163 uint32 valueMask = 1UL << value; 1164 fLastHintValueSet = (hintMask & valueMask) == 0; 1165 1166 if (fLastHintValueSet) 1167 hintMask |= valueMask; 1168 else 1169 hintMask &= ~valueMask; 1170 1171 fField->SetHintMaskAt(x, y, hintMask); 1172 1173 if (value + 1 != fValueHintValue) { 1174 _SetValueHintValue(UINT32_MAX); 1175 _InvalidateHintField(x, y, hintX, hintY); 1176 } else 1177 _InvalidateField(x, y); 1178 1179 fLastHintValue = value; 1180 fLastField = field; 1181 } 1182 1183 1184 void 1185 SudokuView::_RemoveHintValues(uint32 atX, uint32 atY, uint32 value) 1186 { 1187 // Remove all hints in the same block 1188 uint32 blockSize = fField->BlockSize(); 1189 uint32 blockX = (atX / blockSize) * blockSize; 1190 uint32 blockY = (atY / blockSize) * blockSize; 1191 uint32 valueMask = 1UL << (value - 1); 1192 1193 for (uint32 y = blockY; y < blockY + blockSize; y++) { 1194 for (uint32 x = blockX; x < blockX + blockSize; x++) { 1195 if (x != atX && y != atY) 1196 _RemoveHintValue(x, y, valueMask); 1197 } 1198 } 1199 1200 // Remove all hints from the vertical and horizontal lines 1201 1202 for (uint32 i = 0; i < fField->Size(); i++) { 1203 if (i != atX) 1204 _RemoveHintValue(i, atY, valueMask); 1205 if (i != atY) 1206 _RemoveHintValue(atX, i, valueMask); 1207 } 1208 } 1209 1210 1211 void 1212 SudokuView::_RemoveHintValue(uint32 x, uint32 y, uint32 valueMask) 1213 { 1214 uint32 hintMask = fField->HintMaskAt(x, y); 1215 if ((hintMask & valueMask) != 0) { 1216 fField->SetHintMaskAt(x, y, hintMask & ~valueMask); 1217 _InvalidateField(x, y); 1218 } 1219 } 1220 1221 1222 void 1223 SudokuView::_SetAllHints() 1224 { 1225 uint32 size = fField->Size(); 1226 1227 for (uint32 y = 0; y < size; y++) { 1228 for (uint32 x = 0; x < size; x++) { 1229 uint32 validMask = fField->ValidMaskAt(x, y); 1230 fField->SetHintMaskAt(x, y, validMask); 1231 } 1232 } 1233 Invalidate(); 1234 } 1235 1236 1237 void 1238 SudokuView::_Solve() 1239 { 1240 SudokuSolver solver; 1241 if (_GetSolutions(solver)) { 1242 _PushUndo(); 1243 fField->SetTo(solver.SolutionAt(0)); 1244 Invalidate(); 1245 } else 1246 beep(); 1247 } 1248 1249 1250 void 1251 SudokuView::_SolveSingle() 1252 { 1253 if (fField->IsSolved()) { 1254 beep(); 1255 return; 1256 } 1257 1258 SudokuSolver solver; 1259 if (_GetSolutions(solver)) { 1260 _PushUndo(); 1261 1262 // find free spot 1263 uint32 x, y; 1264 do { 1265 x = rand() % fField->Size(); 1266 y = rand() % fField->Size(); 1267 } while (fField->ValueAt(x, y)); 1268 1269 uint32 value = solver.SolutionAt(0)->ValueAt(x, y); 1270 _SetValue(x, y, value); 1271 } else 1272 beep(); 1273 } 1274 1275 1276 bool 1277 SudokuView::_GetSolutions(SudokuSolver& solver) 1278 { 1279 solver.SetTo(fField); 1280 bigtime_t start = system_time(); 1281 solver.ComputeSolutions(); 1282 1283 printf("found %" B_PRIu32 " solutions in %g msecs\n", 1284 solver.CountSolutions(), (system_time() - start) / 1000.0); 1285 1286 return solver.CountSolutions() > 0; 1287 } 1288 1289 1290 void 1291 SudokuView::_UndoRedo(BObjectList<BMessage>& undos, 1292 BObjectList<BMessage>& redos) 1293 { 1294 if (undos.IsEmpty()) 1295 return; 1296 1297 BMessage* undo = undos.RemoveItemAt(undos.CountItems() - 1); 1298 1299 BMessage* redo = new BMessage; 1300 if (fField->Archive(redo, true) == B_OK) 1301 redos.AddItem(redo); 1302 1303 SudokuField field(undo); 1304 delete undo; 1305 1306 fField->SetTo(&field); 1307 1308 SendNotices(kUndoRedoChanged); 1309 Invalidate(); 1310 } 1311 1312 1313 void 1314 SudokuView::_PushUndo() 1315 { 1316 fRedos.MakeEmpty(); 1317 1318 BMessage* undo = new BMessage; 1319 if (fField->Archive(undo, true) == B_OK 1320 && fUndos.AddItem(undo)) 1321 SendNotices(kUndoRedoChanged); 1322 } 1323 1324 1325 void 1326 SudokuView::_SetValueHintValue(uint32 value) 1327 { 1328 if (value == fValueHintValue) 1329 return; 1330 1331 if (fValueHintValue != UINT32_MAX) 1332 _InvalidateValue(fValueHintValue, true); 1333 1334 fValueHintValue = value; 1335 1336 if (fValueHintValue != UINT32_MAX) 1337 _InvalidateValue(fValueHintValue, true); 1338 } 1339 1340 1341 void 1342 SudokuView::_RemoveHint() 1343 { 1344 if (fShowHintX == UINT32_MAX) 1345 return; 1346 1347 uint32 x = fShowHintX; 1348 uint32 y = fShowHintY; 1349 fShowHintX = UINT32_MAX; 1350 fShowHintY = UINT32_MAX; 1351 1352 _InvalidateField(x, y); 1353 } 1354 1355 1356 void 1357 SudokuView::_FitFont(BFont& font, float fieldWidth, float fieldHeight) 1358 { 1359 font.SetSize(100); 1360 1361 font_height fontHeight; 1362 font.GetHeight(&fontHeight); 1363 1364 float width = font.StringWidth("W"); 1365 float height = ceilf(fontHeight.ascent) + ceilf(fontHeight.descent); 1366 1367 float factor = fieldWidth != fHintWidth ? 4.f / 5.f : 1.f; 1368 float widthFactor = fieldWidth / (width / factor); 1369 float heightFactor = fieldHeight / (height / factor); 1370 font.SetSize(100 * min_c(widthFactor, heightFactor)); 1371 } 1372 1373 1374 void 1375 SudokuView::_DrawKeyboardFocus() 1376 { 1377 BRect frame = _Frame(fKeyboardX, fKeyboardY); 1378 SetHighColor(ui_color(B_KEYBOARD_NAVIGATION_COLOR)); 1379 StrokeRect(frame); 1380 frame.InsetBy(-1, -1); 1381 StrokeRect(frame); 1382 frame.InsetBy(2, 2); 1383 StrokeRect(frame); 1384 } 1385 1386 1387 void 1388 SudokuView::_DrawHints(uint32 x, uint32 y) 1389 { 1390 bool showAll = fShowHintX == x && fShowHintY == y; 1391 uint32 hintMask = fField->HintMaskAt(x, y); 1392 if (hintMask == 0 && !showAll) 1393 return; 1394 1395 uint32 validMask = fField->ValidMaskAt(x, y); 1396 BPoint leftTop = _LeftTop(x, y); 1397 SetFont(&fHintFont); 1398 1399 for (uint32 j = 0; j < fBlockSize; j++) { 1400 for (uint32 i = 0; i < fBlockSize; i++) { 1401 uint32 value = j * fBlockSize + i; 1402 if ((hintMask & (1UL << value)) != 0) { 1403 SetHighColor(kHintColor); 1404 } else { 1405 if (!showAll) 1406 continue; 1407 1408 if ((fHintFlags & kMarkValidHints) == 0 1409 || (validMask & (1UL << value)) != 0) 1410 SetHighColor(110, 110, 80); 1411 else 1412 SetHighColor(180, 180, 120); 1413 } 1414 1415 char text[2]; 1416 _SetText(text, value + 1); 1417 DrawString(text, leftTop + BPoint((i + 0.5f) * fHintWidth 1418 - StringWidth(text) / 2, floorf(j * fHintHeight) 1419 + fHintBaseline)); 1420 } 1421 } 1422 } 1423