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
SudokuView(BRect frame,const char * name,const BMessage & settings,uint32 resizingMode)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
SudokuView(const char * name,const BMessage & settings)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
SudokuView(BMessage * archive)72 SudokuView::SudokuView(BMessage* archive)
73 :
74 BView(archive)
75 {
76 _InitObject(archive);
77 }
78
79
~SudokuView()80 SudokuView::~SudokuView()
81 {
82 delete fField;
83 }
84
85
86 status_t
Archive(BMessage * into,bool deep) const87 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*
Instantiate(BMessage * archive)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
SaveState(BMessage & state) const120 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
SetTo(entry_ref & ref)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
SetTo(const char * data)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
SetTo(SudokuField * field)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
SaveTo(entry_ref & ref,uint32 exportAs)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
SaveTo(BDataIO & stream,uint32 exportAs)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
CopyToClipboard()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
ClearChanged()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
ClearAll()508 SudokuView::ClearAll()
509 {
510 _PushUndo();
511 fField->Reset();
512 Invalidate();
513 }
514
515
516 void
SetHintFlags(uint32 flags)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
SetEditable(bool editable)530 SudokuView::SetEditable(bool editable)
531 {
532 fEditable = editable;
533 }
534
535
536 void
Undo()537 SudokuView::Undo()
538 {
539 _UndoRedo(fUndos, fRedos);
540 }
541
542
543 void
Redo()544 SudokuView::Redo()
545 {
546 _UndoRedo(fRedos, fUndos);
547 }
548
549
550 // #pragma mark - BWindow methods
551
552
553 void
AttachedToWindow()554 SudokuView::AttachedToWindow()
555 {
556 MakeFocus(true);
557 }
558
559
560 void
FrameResized(float,float)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
MouseDown(BPoint where)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
MouseMoved(BPoint where,uint32 transit,const BMessage * dragMessage)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
KeyDown(const char * bytes,int32)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
MessageReceived(BMessage * message)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
Draw(BRect)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
_InitObject(const BMessage * archive)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
_FilterString(const char * data,size_t dataLength,char * buffer,uint32 & out,bool & ignore)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
_SetText(char * text,uint32 value)977 SudokuView::_SetText(char* text, uint32 value)
978 {
979 text[0] = value + _BaseCharacter();
980 text[1] = '\0';
981 }
982
983
984 char
_BaseCharacter()985 SudokuView::_BaseCharacter()
986 {
987 return fField->Size() > 9 ? '@' : '0';
988 }
989
990
991 bool
_ValidCharacter(char c)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
_LeftTop(uint32 x,uint32 y)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
_Frame(uint32 x,uint32 y)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
_InvalidateHintField(uint32 x,uint32 y,uint32 hintX,uint32 hintY)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
_InvalidateField(uint32 x,uint32 y)1034 SudokuView::_InvalidateField(uint32 x, uint32 y)
1035 {
1036 Invalidate(_Frame(x, y));
1037 }
1038
1039
1040 void
_InvalidateValue(uint32 value,bool invalidateHint,uint32 fieldX,uint32 fieldY)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
_InvalidateKeyboardFocus(uint32 x,uint32 y)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
_InsertKey(char rawKey,int32 modifiers)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
_GetHintFieldFor(BPoint where,uint32 x,uint32 y,uint32 & hintX,uint32 & hintY)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
_GetFieldFor(BPoint where,uint32 & x,uint32 & y)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
_SetValue(uint32 x,uint32 y,uint32 value)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
_ToggleHintValue(uint32 x,uint32 y,uint32 hintX,uint32 hintY,uint32 value,uint32 field)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
_RemoveHintValues(uint32 atX,uint32 atY,uint32 value)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
_RemoveHintValue(uint32 x,uint32 y,uint32 valueMask)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
_SetAllHints()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
_Solve()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
_SolveSingle()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
_GetSolutions(SudokuSolver & solver)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
_UndoRedo(BObjectList<BMessage> & undos,BObjectList<BMessage> & redos)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
_PushUndo()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
_SetValueHintValue(uint32 value)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
_RemoveHint()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
_FitFont(BFont & font,float fieldWidth,float fieldHeight)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
_DrawKeyboardFocus()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
_DrawHints(uint32 x,uint32 y)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