haiku/src/apps/sudoku/SudokuView.cpp

1423 lines
30 KiB
C++

/*
* Copyright 2007-2015, Axel Dörfler, axeld@pinc-software.de.
* Distributed under the terms of the MIT License.
*/
#include "SudokuView.h"
#include "Sudoku.h"
#include "SudokuField.h"
#include "SudokuSolver.h"
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <Application.h>
#include <Beep.h>
#include <Bitmap.h>
#include <Clipboard.h>
#include <DataIO.h>
#include <Dragger.h>
#include <File.h>
#include <NodeInfo.h>
#include <Path.h>
#include <Picture.h>
#include <String.h>
static const uint32 kMsgCheckSolved = 'chks';
static const uint32 kStrongLineSize = 2;
static const rgb_color kBackgroundColor = {255, 255, 240};
static const rgb_color kHintColor = {255, 115, 0};
static const rgb_color kValueColor = {0, 91, 162};
static const rgb_color kValueCompletedColor = {55, 140, 35};
static const rgb_color kInvalidValueColor = {200, 0, 0};
static const rgb_color kValueHintBackgroundColor = {255, 215, 127};
static const rgb_color kHintValueHintBackgroundColor = {255, 235, 185};
extern const char* kSignature;
SudokuView::SudokuView(BRect frame, const char* name,
const BMessage& settings, uint32 resizingMode)
:
BView(frame, name, resizingMode,
B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS)
{
_InitObject(&settings);
#if 0
BRect rect(Bounds());
rect.top = rect.bottom - 7;
rect.left = rect.right - 7;
BDragger* dragger = new BDragger(rect, this);
AddChild(dragger);
#endif
}
SudokuView::SudokuView(const char* name, const BMessage& settings)
:
BView(name,
B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS)
{
_InitObject(&settings);
}
SudokuView::SudokuView(BMessage* archive)
:
BView(archive)
{
_InitObject(archive);
}
SudokuView::~SudokuView()
{
delete fField;
}
status_t
SudokuView::Archive(BMessage* into, bool deep) const
{
status_t status;
status = BView::Archive(into, deep);
if (status < B_OK)
return status;
status = into->AddString("add_on", kSignature);
if (status < B_OK)
return status;
status = into->AddRect("bounds", Bounds());
if (status < B_OK)
return status;
status = SaveState(*into);
if (status < B_OK)
return status;
return B_OK;
}
BArchivable*
SudokuView::Instantiate(BMessage* archive)
{
if (!validate_instantiation(archive, "SudokuView"))
return NULL;
return new SudokuView(archive);
}
status_t
SudokuView::SaveState(BMessage& state) const
{
BMessage field;
status_t status = fField->Archive(&field, true);
if (status == B_OK)
status = state.AddMessage("field", &field);
if (status == B_OK)
status = state.AddInt32("hint flags", fHintFlags);
if (status == B_OK)
status = state.AddBool("show cursor", fShowCursor);
return status;
}
status_t
SudokuView::SetTo(entry_ref& ref)
{
BPath path;
status_t status = path.SetTo(&ref);
if (status < B_OK)
return status;
FILE* file = fopen(path.Path(), "r");
if (file == NULL)
return errno;
uint32 maxOut = fField->Size() * fField->Size();
char buffer[1024];
char line[1024];
bool ignore = false;
uint32 out = 0;
while (fgets(line, sizeof(line), file) != NULL
&& out < maxOut) {
status = _FilterString(line, sizeof(line), buffer, out, ignore);
if (status < B_OK) {
fclose(file);
return status;
}
}
_PushUndo();
status = fField->SetTo(_BaseCharacter(), buffer);
fValueHintValue = UINT32_MAX;
Invalidate();
fclose(file);
return status;
}
status_t
SudokuView::SetTo(const char* data)
{
if (data == NULL)
return B_BAD_VALUE;
char buffer[1024];
bool ignore = false;
uint32 out = 0;
status_t status = _FilterString(data, 65536, buffer, out, ignore);
if (status < B_OK)
return B_BAD_VALUE;
_PushUndo();
status = fField->SetTo(_BaseCharacter(), buffer);
fValueHintValue = UINT32_MAX;
Invalidate();
return status;
}
status_t
SudokuView::SetTo(SudokuField* field)
{
if (field == NULL || field == fField)
return B_BAD_VALUE;
_PushUndo();
delete fField;
fField = field;
fBlockSize = fField->BlockSize();
fValueHintValue = UINT32_MAX;
FrameResized(0, 0);
Invalidate();
return B_OK;
}
status_t
SudokuView::SaveTo(entry_ref& ref, uint32 exportAs)
{
BFile file;
status_t status = file.SetTo(&ref, B_WRITE_ONLY | B_CREATE_FILE
| B_ERASE_FILE);
if (status < B_OK)
return status;
return SaveTo(file, exportAs);
}
status_t
SudokuView::SaveTo(BDataIO& stream, uint32 exportAs)
{
BFile* file = dynamic_cast<BFile*>(&stream);
uint32 i = 0;
BNodeInfo nodeInfo;
if (file)
nodeInfo.SetTo(file);
switch (exportAs) {
case kExportAsText:
{
BString text = "# Written by Sudoku\n\n";
stream.Write(text.String(), text.Length());
char* line = text.LockBuffer(1024);
memset(line, 0, 1024);
for (uint32 y = 0; y < fField->Size(); y++) {
for (uint32 x = 0; x < fField->Size(); x++) {
if (x != 0 && x % fBlockSize == 0)
line[i++] = ' ';
_SetText(&line[i++], fField->ValueAt(x, y));
}
line[i++] = '\n';
}
text.UnlockBuffer();
stream.Write(text.String(), text.Length());
if (file)
nodeInfo.SetType("text/plain");
return B_OK;
}
case kExportAsHTML:
{
bool netPositiveFriendly = false;
BString text = "<html>\n<head>\n<!-- Written by Sudoku -->\n"
"<style type=\"text/css\">\n"
"table.sudoku { background: #000000; border:0; border-collapse: "
"collapse; cellpadding: 10px; cellspacing: 10px; width: "
"300px; height: 300px; }\n"
"td.sudoku { background: #ffffff; border-color: black; "
"border-left: none ; border-top: none ; /*border: none;*/ "
"text-align: center; }\n"
"td.sudoku_initial { }\n"
"td.sudoku_filled { color: blue; }\n"
"td.sudoku_empty { }\n";
// border styles: right bottom (none, small or large)
const char* kStyles[] = {"none", "small", "large"};
const char* kCssStyles[] = {"none", "solid 1px black",
"solid 3px black"};
enum style_type { kNone = 0, kSmall, kLarge };
for (int32 right = 0; right < 3; right++) {
for (int32 bottom = 0; bottom < 3; bottom++) {
text << "td.sudoku_";
if (right != bottom)
text << kStyles[right] << "_" << kStyles[bottom];
else
text << kStyles[right];
text << " { border-right: " << kCssStyles[right]
<< "; border-bottom: " << kCssStyles[bottom] << "; }\n";
}
}
text << "</style>\n"
"</head>\n<body>\n\n";
stream.Write(text.String(), text.Length());
text = "<table";
if (netPositiveFriendly)
text << " border=\"1\"";
text << " class=\"sudoku\">";
stream.Write(text.String(), text.Length());
text = "";
BString divider;
divider << (int)(100.0 / fField->Size()) << "%";
for (uint32 y = 0; y < fField->Size(); y++) {
text << "<tr height=\"" << divider << "\">\n";
for (uint32 x = 0; x < fField->Size(); x++) {
char buff[2];
_SetText(buff, fField->ValueAt(x, y));
BString style = "sudoku_";
style_type right = kSmall;
style_type bottom = kSmall;
if ((x + 1) % fField->BlockSize() == 0)
right = kLarge;
if ((y + 1) % fField->BlockSize() == 0)
bottom = kLarge;
if (x == fField->Size() - 1)
right = kNone;
if (y == fField->Size() - 1)
bottom = kNone;
if (right != bottom)
style << kStyles[right] << "_" << kStyles[bottom];
else
style << kStyles[right];
if (fField->ValueAt(x, y) == 0) {
text << "<td width=\"" << divider << "\" ";
text << "class=\"sudoku sudoku_empty " << style;
text << "\">\n&nbsp;";
} else if (fField->IsInitialValue(x, y)) {
text << "<td width=\"" << divider << "\" ";
text << "class=\"sudoku sudoku_initial " << style
<< "\">\n";
if (netPositiveFriendly)
text << "<font color=\"#000000\">";
text << buff;
if (netPositiveFriendly)
text << "</font>";
} else {
text << "<td width=\"" << divider << "\" ";
text << "class=\"sudoku sudoku_filled sudoku_" << style
<< "\">\n";
if (netPositiveFriendly)
text << "<font color=\"#0000ff\">";
text << buff;
if (netPositiveFriendly)
text << "</font>";
}
text << "</td>\n";
}
text << "</tr>\n";
}
text << "</table>\n\n";
stream.Write(text.String(), text.Length());
text = "</body></html>\n";
stream.Write(text.String(), text.Length());
if (file)
nodeInfo.SetType("text/html");
return B_OK;
}
case kExportAsBitmap:
{
BMallocIO mallocIO;
status_t status = SaveTo(mallocIO, kExportAsPicture);
if (status < B_OK)
return status;
mallocIO.Seek(0LL, SEEK_SET);
BPicture picture;
status = picture.Unflatten(&mallocIO);
if (status < B_OK)
return status;
BBitmap* bitmap = new BBitmap(Bounds(), B_BITMAP_ACCEPTS_VIEWS,
B_RGB32);
BView* view = new BView(Bounds(), "bitmap", B_FOLLOW_NONE,
B_WILL_DRAW);
bitmap->AddChild(view);
if (bitmap->Lock()) {
view->DrawPicture(&picture);
view->Sync();
view->RemoveSelf();
delete view;
// it should not become part of the archive
bitmap->Unlock();
}
BMessage archive;
status = bitmap->Archive(&archive);
if (status >= B_OK)
status = archive.Flatten(&stream);
delete bitmap;
return status;
}
case kExportAsPicture:
{
BPicture picture;
BeginPicture(&picture);
Draw(Bounds());
status_t status = B_ERROR;
if (EndPicture())
status = picture.Flatten(&stream);
return status;
}
default:
return B_BAD_VALUE;
}
}
status_t
SudokuView::CopyToClipboard()
{
if (!be_clipboard->Lock())
return B_ERROR;
be_clipboard->Clear();
BMessage* clip = be_clipboard->Data();
if (clip == NULL) {
be_clipboard->Unlock();
return B_ERROR;
}
// As BBitmap
BMallocIO mallocIO;
status_t status = SaveTo(mallocIO, kExportAsBitmap);
if (status >= B_OK) {
mallocIO.Seek(0LL, SEEK_SET);
// ShowImage, ArtPaint & WonderBrush use that
status = clip->AddData("image/bitmap", B_MESSAGE_TYPE,
mallocIO.Buffer(), mallocIO.BufferLength());
// Becasso uses that ?
clip->AddData("image/x-be-bitmap", B_MESSAGE_TYPE, mallocIO.Buffer(),
mallocIO.BufferLength());
// Gobe Productive uses that...
// QuickRes as well, with a rect field.
clip->AddData("image/x-vnd.Be-bitmap", B_MESSAGE_TYPE,
mallocIO.Buffer(), mallocIO.BufferLength());
}
mallocIO.Seek(0LL, SEEK_SET);
mallocIO.SetSize(0LL);
// As HTML
if (status >= B_OK)
status = SaveTo(mallocIO, kExportAsHTML);
if (status >= B_OK) {
status = clip->AddData("text/html", B_MIME_TYPE, mallocIO.Buffer(),
mallocIO.BufferLength());
}
mallocIO.Seek(0LL, SEEK_SET);
mallocIO.SetSize(0LL);
// As plain text
if (status >= B_OK)
SaveTo(mallocIO, kExportAsText);
if (status >= B_OK) {
status = clip->AddData("text/plain", B_MIME_TYPE, mallocIO.Buffer(),
mallocIO.BufferLength());
}
mallocIO.Seek(0LL, SEEK_SET);
mallocIO.SetSize(0LL);
// As flattened BPicture, anyone handles that?
if (status >= B_OK)
status = SaveTo(mallocIO, kExportAsPicture);
if (status >= B_OK) {
status = clip->AddData("image/x-vnd.Be-picture", B_MIME_TYPE,
mallocIO.Buffer(), mallocIO.BufferLength());
}
mallocIO.SetSize(0LL);
be_clipboard->Commit();
be_clipboard->Unlock();
return status;
}
void
SudokuView::ClearChanged()
{
_PushUndo();
for (uint32 y = 0; y < fField->Size(); y++) {
for (uint32 x = 0; x < fField->Size(); x++) {
if (!fField->IsInitialValue(x, y))
fField->SetValueAt(x, y, 0);
}
}
Invalidate();
}
void
SudokuView::ClearAll()
{
_PushUndo();
fField->Reset();
Invalidate();
}
void
SudokuView::SetHintFlags(uint32 flags)
{
if (flags == fHintFlags)
return;
if ((flags & kMarkInvalid) ^ (fHintFlags & kMarkInvalid))
Invalidate();
fHintFlags = flags;
}
void
SudokuView::SetEditable(bool editable)
{
fEditable = editable;
}
void
SudokuView::Undo()
{
_UndoRedo(fUndos, fRedos);
}
void
SudokuView::Redo()
{
_UndoRedo(fRedos, fUndos);
}
// #pragma mark - BWindow methods
void
SudokuView::AttachedToWindow()
{
MakeFocus(true);
}
void
SudokuView::FrameResized(float /*width*/, float /*height*/)
{
// font for numbers
uint32 size = fField->Size();
fWidth = (Bounds().Width() + 2 - kStrongLineSize * (fBlockSize - 1)) / size;
fHeight = (Bounds().Height() + 2 - kStrongLineSize * (fBlockSize - 1))
/ size;
_FitFont(fFieldFont, fWidth - 2, fHeight - 2);
font_height fontHeight;
fFieldFont.GetHeight(&fontHeight);
fBaseline = ceilf(fontHeight.ascent) / 2
+ (fHeight - ceilf(fontHeight.descent)) / 2;
// font for hint
fHintWidth = (fWidth - 2) / fBlockSize;
fHintHeight = (fHeight - 2) / fBlockSize;
_FitFont(fHintFont, fHintWidth, fHintHeight);
fHintFont.GetHeight(&fontHeight);
fHintBaseline = ceilf(fontHeight.ascent) / 2
+ (fHintHeight - ceilf(fontHeight.descent)) / 2;
// fix the dragger's position
BView *dragger = FindView("_dragger_");
if (dragger)
dragger->MoveTo(Bounds().right - 7, Bounds().bottom - 7);
}
void
SudokuView::MouseDown(BPoint where)
{
uint32 x, y;
if (!fEditable || !_GetFieldFor(where, x, y))
return;
int32 buttons = B_PRIMARY_MOUSE_BUTTON;
int32 clicks = 1;
if (Looper() != NULL && Looper()->CurrentMessage() != NULL) {
Looper()->CurrentMessage()->FindInt32("buttons", &buttons);
Looper()->CurrentMessage()->FindInt32("clicks", &clicks);
}
if (buttons == B_PRIMARY_MOUSE_BUTTON && clicks == 1) {
uint32 value = fField->ValueAt(x, y);
if (value != 0) {
_SetValueHintValue(value);
return;
}
}
uint32 hintX, hintY;
if (!_GetHintFieldFor(where, x, y, hintX, hintY))
return;
uint32 value = hintX + hintY * fBlockSize;
uint32 field = x + y * fField->Size();
_PushUndo();
_SetValueHintValue(value + 1);
if ((clicks == 2 && fLastHintValue == value && fLastField == field)
|| (buttons & (B_SECONDARY_MOUSE_BUTTON
| B_TERTIARY_MOUSE_BUTTON)) != 0) {
// Double click or other buttons set or remove a value
if (!fField->IsInitialValue(x, y))
_SetValue(x, y, fField->ValueAt(x, y) == 0 ? value + 1 : 0);
return;
}
_ToggleHintValue(x, y, hintX, hintY, value, field);
}
void
SudokuView::MouseMoved(BPoint where, uint32 transit,
const BMessage* dragMessage)
{
if (transit == B_EXITED_VIEW || dragMessage != NULL) {
_RemoveHint();
return;
}
if (fShowKeyboardFocus) {
fShowKeyboardFocus = false;
_InvalidateKeyboardFocus(fKeyboardX, fKeyboardY);
}
uint32 x, y;
bool isField = _GetFieldFor(where, x, y);
if (isField) {
fKeyboardX = x;
fKeyboardY = y;
}
if (!isField
|| fField->IsInitialValue(x, y)
|| (!fShowCursor && fField->ValueAt(x, y) != 0)) {
_RemoveHint();
return;
}
if (fShowHintX == x && fShowHintY == y)
return;
int32 buttons = 0;
if (Looper() != NULL && Looper()->CurrentMessage() != NULL)
Looper()->CurrentMessage()->FindInt32("buttons", &buttons);
uint32 field = x + y * fField->Size();
if (buttons != 0 && field != fLastField) {
// if a button is pressed, we drag the last hint selection
// (either set or removal) to the field under the mouse
uint32 hintMask = fField->HintMaskAt(x, y);
uint32 valueMask = 1UL << fLastHintValue;
if (fLastHintValueSet)
hintMask |= valueMask;
else
hintMask &= ~valueMask;
fField->SetHintMaskAt(x, y, hintMask);
}
_RemoveHint();
fShowHintX = x;
fShowHintY = y;
_InvalidateField(x, y);
}
void
SudokuView::KeyDown(const char *bytes, int32 /*numBytes*/)
{
be_app->ObscureCursor();
uint32 x = fKeyboardX, y = fKeyboardY;
switch (bytes[0]) {
case B_UP_ARROW:
if (fKeyboardY == 0)
fKeyboardY = fField->Size() - 1;
else
fKeyboardY--;
break;
case B_DOWN_ARROW:
if (fKeyboardY == fField->Size() - 1)
fKeyboardY = 0;
else
fKeyboardY++;
break;
case B_LEFT_ARROW:
if (fKeyboardX == 0)
fKeyboardX = fField->Size() - 1;
else
fKeyboardX--;
break;
case B_RIGHT_ARROW:
if (fKeyboardX == fField->Size() - 1)
fKeyboardX = 0;
else
fKeyboardX++;
break;
case B_BACKSPACE:
case B_DELETE:
case B_SPACE:
// clear value
_InsertKey(_BaseCharacter(), 0);
break;
default:
int32 rawKey = bytes[0];
int32 modifiers = 0;
if (Looper() != NULL && Looper()->CurrentMessage() != NULL) {
Looper()->CurrentMessage()->FindInt32("raw_char", &rawKey);
Looper()->CurrentMessage()->FindInt32("modifiers", &modifiers);
}
_InsertKey(rawKey, modifiers);
break;
}
if (!fShowKeyboardFocus && fShowHintX != UINT32_MAX) {
// always start at last mouse position, if any
fKeyboardX = fShowHintX;
fKeyboardY = fShowHintY;
}
_RemoveHint();
// remove old focus, if any
if (fShowKeyboardFocus && (x != fKeyboardX || y != fKeyboardY))
_InvalidateKeyboardFocus(x, y);
fShowKeyboardFocus = true;
_InvalidateKeyboardFocus(fKeyboardX, fKeyboardY);
}
void
SudokuView::MessageReceived(BMessage* message)
{
switch (message->what) {
case kMsgCheckSolved:
if (fField->IsSolved()) {
// notify window
Looper()->PostMessage(kMsgSudokuSolved);
}
break;
case B_UNDO:
Undo();
break;
case B_REDO:
Redo();
break;
case kMsgSetAllHints:
_SetAllHints();
break;
case kMsgSolveSudoku:
_Solve();
break;
case kMsgSolveSingle:
_SolveSingle();
break;
default:
BView::MessageReceived(message);
break;
}
}
void
SudokuView::Draw(BRect /*updateRect*/)
{
// draw lines
uint32 size = fField->Size();
SetLowColor(fBackgroundColor);
SetHighColor(0, 0, 0);
float width = fWidth - 1;
for (uint32 x = 1; x < size; x++) {
if (x % fBlockSize == 0) {
FillRect(BRect(width, 0, width + kStrongLineSize,
Bounds().Height()));
width += kStrongLineSize;
} else {
StrokeLine(BPoint(width, 0), BPoint(width, Bounds().Height()));
}
width += fWidth;
}
float height = fHeight - 1;
for (uint32 y = 1; y < size; y++) {
if (y % fBlockSize == 0) {
FillRect(BRect(0, height, Bounds().Width(),
height + kStrongLineSize));
height += kStrongLineSize;
} else {
StrokeLine(BPoint(0, height), BPoint(Bounds().Width(), height));
}
height += fHeight;
}
// draw text
for (uint32 y = 0; y < size; y++) {
for (uint32 x = 0; x < size; x++) {
uint32 value = fField->ValueAt(x, y);
rgb_color backgroundColor = fBackgroundColor;
if (value == fValueHintValue)
backgroundColor = kValueHintBackgroundColor;
else if (value == 0 && fField->HasHint(x, y, fValueHintValue))
backgroundColor = kHintValueHintBackgroundColor;
if (((fShowCursor && x == fShowHintX && y == fShowHintY)
|| (fShowKeyboardFocus && x == fKeyboardX
&& y == fKeyboardY))
&& !fField->IsInitialValue(x, y)) {
// TODO: make color more intense
SetLowColor(tint_color(backgroundColor, B_DARKEN_2_TINT));
FillRect(_Frame(x, y), B_SOLID_LOW);
} else {
SetLowColor(backgroundColor);
FillRect(_Frame(x, y), B_SOLID_LOW);
}
if (fShowKeyboardFocus && x == fKeyboardX && y == fKeyboardY)
_DrawKeyboardFocus();
if (value == 0) {
_DrawHints(x, y);
continue;
}
SetFont(&fFieldFont);
if (fField->IsInitialValue(x, y))
SetHighColor(0, 0, 0);
else {
if ((fHintFlags & kMarkInvalid) == 0
|| fField->IsValid(x, y, value)) {
if (fField->IsValueCompleted(value))
SetHighColor(kValueCompletedColor);
else
SetHighColor(kValueColor);
} else
SetHighColor(kInvalidValueColor);
}
char text[2];
_SetText(text, value);
DrawString(text, _LeftTop(x, y)
+ BPoint((fWidth - StringWidth(text)) / 2, fBaseline));
}
}
}
// #pragma mark - Private methods
void
SudokuView::_InitObject(const BMessage* archive)
{
fField = NULL;
fShowHintX = UINT32_MAX;
fValueHintValue = UINT32_MAX;
fLastHintValue = UINT32_MAX;
fLastField = UINT32_MAX;
fKeyboardX = 0;
fKeyboardY = 0;
fShowKeyboardFocus = false;
fEditable = true;
BMessage field;
if (archive->FindMessage("field", &field) == B_OK) {
fField = new SudokuField(&field);
if (fField->InitCheck() != B_OK) {
delete fField;
fField = NULL;
} else if (fField->IsSolved())
ClearAll();
}
if (fField == NULL)
fField = new SudokuField(3);
fBlockSize = fField->BlockSize();
if (archive->FindInt32("hint flags", (int32*)&fHintFlags) != B_OK)
fHintFlags = kMarkInvalid;
if (archive->FindBool("show cursor", &fShowCursor) != B_OK)
fShowCursor = false;
SetViewColor(B_TRANSPARENT_COLOR);
// to avoid flickering
fBackgroundColor = kBackgroundColor;
SetLowColor(fBackgroundColor);
FrameResized(0, 0);
}
status_t
SudokuView::_FilterString(const char* data, size_t dataLength, char* buffer,
uint32& out, bool& ignore)
{
uint32 maxOut = fField->Size() * fField->Size();
for (uint32 i = 0; i < dataLength && data[i]; i++) {
if (data[i] == '#')
ignore = true;
else if (data[i] == '\n')
ignore = false;
if (ignore || isspace(data[i]))
continue;
if (!_ValidCharacter(data[i])) {
return B_BAD_VALUE;
}
buffer[out++] = data[i];
if (out == maxOut)
break;
}
buffer[out] = '\0';
return B_OK;
}
void
SudokuView::_SetText(char* text, uint32 value)
{
text[0] = value + _BaseCharacter();
text[1] = '\0';
}
char
SudokuView::_BaseCharacter()
{
return fField->Size() > 9 ? '@' : '0';
}
bool
SudokuView::_ValidCharacter(char c)
{
char min = _BaseCharacter();
char max = min + fField->Size();
return c >= min && c <= max;
}
BPoint
SudokuView::_LeftTop(uint32 x, uint32 y)
{
return BPoint(x * fWidth - 1 + x / fBlockSize * kStrongLineSize + 1,
y * fHeight - 1 + y / fBlockSize * kStrongLineSize + 1);
}
BRect
SudokuView::_Frame(uint32 x, uint32 y)
{
BPoint leftTop = _LeftTop(x, y);
BPoint rightBottom = leftTop + BPoint(fWidth - 2, fHeight - 2);
return BRect(leftTop, rightBottom);
}
void
SudokuView::_InvalidateHintField(uint32 x, uint32 y, uint32 hintX,
uint32 hintY)
{
BPoint leftTop = _LeftTop(x, y);
leftTop.x += hintX * fHintWidth;
leftTop.y += hintY * fHintHeight;
BPoint rightBottom = leftTop;
rightBottom.x += fHintWidth;
rightBottom.y += fHintHeight;
Invalidate(BRect(leftTop, rightBottom));
}
void
SudokuView::_InvalidateField(uint32 x, uint32 y)
{
Invalidate(_Frame(x, y));
}
void
SudokuView::_InvalidateValue(uint32 value, bool invalidateHint,
uint32 fieldX, uint32 fieldY)
{
for (uint32 y = 0; y < fField->Size(); y++) {
for (uint32 x = 0; x < fField->Size(); x++) {
if (fField->ValueAt(x, y) == value || (x == fieldX && y == fieldY))
Invalidate(_Frame(x, y));
else if (invalidateHint && fField->ValueAt(x, y) == 0
&& fField->HasHint(x, y, value))
Invalidate(_Frame(x, y));
}
}
}
void
SudokuView::_InvalidateKeyboardFocus(uint32 x, uint32 y)
{
BRect frame = _Frame(x, y);
frame.InsetBy(-1, -1);
Invalidate(frame);
}
void
SudokuView::_InsertKey(char rawKey, int32 modifiers)
{
if (!fEditable || !_ValidCharacter(rawKey)
|| fField->IsInitialValue(fKeyboardX, fKeyboardY))
return;
uint32 value = rawKey - _BaseCharacter();
if (modifiers & (B_SHIFT_KEY | B_OPTION_KEY)) {
// set or remove hint
if (value == 0)
return;
_PushUndo();
uint32 hintMask = fField->HintMaskAt(fKeyboardX, fKeyboardY);
uint32 valueMask = 1UL << (value - 1);
if (modifiers & B_OPTION_KEY)
hintMask &= ~valueMask;
else
hintMask |= valueMask;
fField->SetValueAt(fKeyboardX, fKeyboardY, 0);
fField->SetHintMaskAt(fKeyboardX, fKeyboardY, hintMask);
} else {
_PushUndo();
_SetValue(fKeyboardX, fKeyboardY, value);
}
}
bool
SudokuView::_GetHintFieldFor(BPoint where, uint32 x, uint32 y,
uint32& hintX, uint32& hintY)
{
BPoint leftTop = _LeftTop(x, y);
hintX = (uint32)floor((where.x - leftTop.x) / fHintWidth);
hintY = (uint32)floor((where.y - leftTop.y) / fHintHeight);
if (hintX >= fBlockSize || hintY >= fBlockSize)
return false;
return true;
}
bool
SudokuView::_GetFieldFor(BPoint where, uint32& x, uint32& y)
{
float block = fWidth * fBlockSize + kStrongLineSize;
x = (uint32)floor(where.x / block);
uint32 offsetX = (uint32)floor((where.x - x * block) / fWidth);
x = x * fBlockSize + offsetX;
block = fHeight * fBlockSize + kStrongLineSize;
y = (uint32)floor(where.y / block);
uint32 offsetY = (uint32)floor((where.y - y * block) / fHeight);
y = y * fBlockSize + offsetY;
if (offsetX >= fBlockSize || offsetY >= fBlockSize
|| x >= fField->Size() || y >= fField->Size())
return false;
return true;
}
void
SudokuView::_SetValue(uint32 x, uint32 y, uint32 value)
{
bool wasCompleted;
if (value == 0) {
// Remove value
value = fField->ValueAt(x, y);
wasCompleted = fField->IsValueCompleted(value);
fField->SetValueAt(x, y, 0);
fShowHintX = x;
fShowHintY = y;
} else {
// Set value
wasCompleted = fField->IsValueCompleted(value);
fField->SetValueAt(x, y, value);
BMessenger(this).SendMessage(kMsgCheckSolved);
_RemoveHintValues(x, y, value);
// allow dragging to remove the hint from other fields
fLastHintValueSet = false;
fLastHintValue = value - 1;
fLastField = x + y * fField->Size();
}
if (value != fValueHintValue && fValueHintValue != UINT32_MAX)
_SetValueHintValue(value);
if (wasCompleted != fField->IsValueCompleted(value))
_InvalidateValue(value, false, x, y);
else
_InvalidateField(x, y);
}
void
SudokuView::_ToggleHintValue(uint32 x, uint32 y, uint32 hintX, uint32 hintY,
uint32 value, uint32 field)
{
uint32 hintMask = fField->HintMaskAt(x, y);
uint32 valueMask = 1UL << value;
fLastHintValueSet = (hintMask & valueMask) == 0;
if (fLastHintValueSet)
hintMask |= valueMask;
else
hintMask &= ~valueMask;
fField->SetHintMaskAt(x, y, hintMask);
if (value + 1 != fValueHintValue) {
_SetValueHintValue(UINT32_MAX);
_InvalidateHintField(x, y, hintX, hintY);
} else
_InvalidateField(x, y);
fLastHintValue = value;
fLastField = field;
}
void
SudokuView::_RemoveHintValues(uint32 atX, uint32 atY, uint32 value)
{
// Remove all hints in the same block
uint32 blockSize = fField->BlockSize();
uint32 blockX = (atX / blockSize) * blockSize;
uint32 blockY = (atY / blockSize) * blockSize;
uint32 valueMask = 1UL << (value - 1);
for (uint32 y = blockY; y < blockY + blockSize; y++) {
for (uint32 x = blockX; x < blockX + blockSize; x++) {
if (x != atX && y != atY)
_RemoveHintValue(x, y, valueMask);
}
}
// Remove all hints from the vertical and horizontal lines
for (uint32 i = 0; i < fField->Size(); i++) {
if (i != atX)
_RemoveHintValue(i, atY, valueMask);
if (i != atY)
_RemoveHintValue(atX, i, valueMask);
}
}
void
SudokuView::_RemoveHintValue(uint32 x, uint32 y, uint32 valueMask)
{
uint32 hintMask = fField->HintMaskAt(x, y);
if ((hintMask & valueMask) != 0) {
fField->SetHintMaskAt(x, y, hintMask & ~valueMask);
_InvalidateField(x, y);
}
}
void
SudokuView::_SetAllHints()
{
uint32 size = fField->Size();
for (uint32 y = 0; y < size; y++) {
for (uint32 x = 0; x < size; x++) {
uint32 validMask = fField->ValidMaskAt(x, y);
fField->SetHintMaskAt(x, y, validMask);
}
}
Invalidate();
}
void
SudokuView::_Solve()
{
SudokuSolver solver;
if (_GetSolutions(solver)) {
_PushUndo();
fField->SetTo(solver.SolutionAt(0));
Invalidate();
} else
beep();
}
void
SudokuView::_SolveSingle()
{
if (fField->IsSolved()) {
beep();
return;
}
SudokuSolver solver;
if (_GetSolutions(solver)) {
_PushUndo();
// find free spot
uint32 x, y;
do {
x = rand() % fField->Size();
y = rand() % fField->Size();
} while (fField->ValueAt(x, y));
uint32 value = solver.SolutionAt(0)->ValueAt(x, y);
_SetValue(x, y, value);
} else
beep();
}
bool
SudokuView::_GetSolutions(SudokuSolver& solver)
{
solver.SetTo(fField);
bigtime_t start = system_time();
solver.ComputeSolutions();
printf("found %" B_PRIu32 " solutions in %g msecs\n",
solver.CountSolutions(), (system_time() - start) / 1000.0);
return solver.CountSolutions() > 0;
}
void
SudokuView::_UndoRedo(BObjectList<BMessage>& undos,
BObjectList<BMessage>& redos)
{
if (undos.IsEmpty())
return;
BMessage* undo = undos.RemoveItemAt(undos.CountItems() - 1);
BMessage* redo = new BMessage;
if (fField->Archive(redo, true) == B_OK)
redos.AddItem(redo);
SudokuField field(undo);
delete undo;
fField->SetTo(&field);
SendNotices(kUndoRedoChanged);
Invalidate();
}
void
SudokuView::_PushUndo()
{
fRedos.MakeEmpty();
BMessage* undo = new BMessage;
if (fField->Archive(undo, true) == B_OK
&& fUndos.AddItem(undo))
SendNotices(kUndoRedoChanged);
}
void
SudokuView::_SetValueHintValue(uint32 value)
{
if (value == fValueHintValue)
return;
if (fValueHintValue != UINT32_MAX)
_InvalidateValue(fValueHintValue, true);
fValueHintValue = value;
if (fValueHintValue != UINT32_MAX)
_InvalidateValue(fValueHintValue, true);
}
void
SudokuView::_RemoveHint()
{
if (fShowHintX == UINT32_MAX)
return;
uint32 x = fShowHintX;
uint32 y = fShowHintY;
fShowHintX = UINT32_MAX;
fShowHintY = UINT32_MAX;
_InvalidateField(x, y);
}
void
SudokuView::_FitFont(BFont& font, float fieldWidth, float fieldHeight)
{
font.SetSize(100);
font_height fontHeight;
font.GetHeight(&fontHeight);
float width = font.StringWidth("W");
float height = ceilf(fontHeight.ascent) + ceilf(fontHeight.descent);
float factor = fieldWidth != fHintWidth ? 4.f / 5.f : 1.f;
float widthFactor = fieldWidth / (width / factor);
float heightFactor = fieldHeight / (height / factor);
font.SetSize(100 * min_c(widthFactor, heightFactor));
}
void
SudokuView::_DrawKeyboardFocus()
{
BRect frame = _Frame(fKeyboardX, fKeyboardY);
SetHighColor(ui_color(B_KEYBOARD_NAVIGATION_COLOR));
StrokeRect(frame);
frame.InsetBy(-1, -1);
StrokeRect(frame);
frame.InsetBy(2, 2);
StrokeRect(frame);
}
void
SudokuView::_DrawHints(uint32 x, uint32 y)
{
bool showAll = fShowHintX == x && fShowHintY == y;
uint32 hintMask = fField->HintMaskAt(x, y);
if (hintMask == 0 && !showAll)
return;
uint32 validMask = fField->ValidMaskAt(x, y);
BPoint leftTop = _LeftTop(x, y);
SetFont(&fHintFont);
for (uint32 j = 0; j < fBlockSize; j++) {
for (uint32 i = 0; i < fBlockSize; i++) {
uint32 value = j * fBlockSize + i;
if ((hintMask & (1UL << value)) != 0) {
SetHighColor(kHintColor);
} else {
if (!showAll)
continue;
if ((fHintFlags & kMarkValidHints) == 0
|| (validMask & (1UL << value)) != 0)
SetHighColor(110, 110, 80);
else
SetHighColor(180, 180, 120);
}
char text[2];
_SetText(text, value + 1);
DrawString(text, leftTop + BPoint((i + 0.5f) * fHintWidth
- StringWidth(text) / 2, floorf(j * fHintHeight)
+ fHintBaseline));
}
}
}