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