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 "SudokuWindow.h" 8 9 #include <stdio.h> 10 11 #include <Alert.h> 12 #include <Application.h> 13 #include <Catalog.h> 14 #include <File.h> 15 #include <FilePanel.h> 16 #include <FindDirectory.h> 17 #include <LayoutBuilder.h> 18 #include <Menu.h> 19 #include <MenuBar.h> 20 #include <MenuItem.h> 21 #include <Path.h> 22 #include <Roster.h> 23 24 #include <be_apps/Tracker/RecentItems.h> 25 26 #include "ProgressWindow.h" 27 #include "Sudoku.h" 28 #include "SudokuField.h" 29 #include "SudokuGenerator.h" 30 #include "SudokuView.h" 31 32 33 #undef B_TRANSLATION_CONTEXT 34 #define B_TRANSLATION_CONTEXT "SudokuWindow" 35 36 37 const uint32 kMsgOpenFilePanel = 'opfp'; 38 const uint32 kMsgGenerateSudoku = 'gnsu'; 39 const uint32 kMsgAbortSudokuGenerator = 'asgn'; 40 const uint32 kMsgSudokuGenerated = 'sugn'; 41 const uint32 kMsgMarkInvalid = 'minv'; 42 const uint32 kMsgMarkValidHints = 'mvht'; 43 const uint32 kMsgStoreState = 'stst'; 44 const uint32 kMsgRestoreState = 'rest'; 45 const uint32 kMsgNewBlank = 'new '; 46 const uint32 kMsgStartAgain = 'stag'; 47 const uint32 kMsgExportAs = 'expt'; 48 49 50 enum sudoku_level { 51 kEasyLevel = 0, 52 kAdvancedLevel = 2, 53 kHardLevel = 4, 54 }; 55 56 57 class GenerateSudoku { 58 public: 59 GenerateSudoku(SudokuField& field, int32 level, 60 BMessenger progress, BMessenger target); 61 ~GenerateSudoku(); 62 63 void Abort(); 64 65 private: 66 void _Generate(); 67 static status_t _GenerateThread(void* self); 68 69 SudokuField fField; 70 BMessenger fTarget; 71 BMessenger fProgress; 72 thread_id fThread; 73 int32 fLevel; 74 bool fQuit; 75 }; 76 77 78 GenerateSudoku::GenerateSudoku(SudokuField& field, int32 level, 79 BMessenger progress, BMessenger target) 80 : 81 fField(field), 82 fTarget(target), 83 fProgress(progress), 84 fLevel(level), 85 fQuit(false) 86 { 87 fThread = spawn_thread(_GenerateThread, "sudoku generator", 88 B_LOW_PRIORITY, this); 89 if (fThread >= B_OK) 90 resume_thread(fThread); 91 else 92 _Generate(); 93 } 94 95 96 GenerateSudoku::~GenerateSudoku() 97 { 98 Abort(); 99 } 100 101 102 void 103 GenerateSudoku::Abort() 104 { 105 fQuit = true; 106 107 status_t status; 108 wait_for_thread(fThread, &status); 109 } 110 111 112 void 113 GenerateSudoku::_Generate() 114 { 115 SudokuGenerator generator; 116 117 bigtime_t start = system_time(); 118 generator.Generate(&fField, 40 - fLevel * 5, fProgress, &fQuit); 119 printf("generated in %g msecs\n", (system_time() - start) / 1000.0); 120 121 BMessage done(kMsgSudokuGenerated); 122 if (!fQuit) { 123 BMessage field; 124 if (fField.Archive(&field, true) == B_OK) 125 done.AddMessage("field", &field); 126 } 127 128 fTarget.SendMessage(&done); 129 } 130 131 132 /*static*/ status_t 133 GenerateSudoku::_GenerateThread(void* _self) 134 { 135 GenerateSudoku* self = (GenerateSudoku*)_self; 136 self->_Generate(); 137 return B_OK; 138 } 139 140 141 // #pragma mark - 142 143 144 SudokuWindow::SudokuWindow() 145 : 146 BWindow(BRect(-1, -1, 400, 420), B_TRANSLATE_SYSTEM_NAME("Sudoku"), 147 B_TITLED_WINDOW, B_ASYNCHRONOUS_CONTROLS | B_QUIT_ON_WINDOW_CLOSE), 148 fGenerator(NULL), 149 fStoredState(NULL), 150 fExportFormat(kExportAsText) 151 { 152 BMessage settings; 153 _LoadSettings(settings); 154 155 BRect frame; 156 if (settings.FindRect("window frame", &frame) == B_OK) { 157 MoveTo(frame.LeftTop()); 158 ResizeTo(frame.Width(), frame.Height()); 159 frame.OffsetTo(B_ORIGIN); 160 } else { 161 float scaling = std::max(1.0f, be_plain_font->Size() / 12.0f); 162 ResizeTo(Frame().Width() * scaling, Frame().Height() * scaling); 163 frame = Bounds(); 164 } 165 166 MoveOnScreen(); 167 168 if (settings.HasMessage("stored state")) { 169 fStoredState = new BMessage; 170 if (settings.FindMessage("stored state", fStoredState) != B_OK) { 171 delete fStoredState; 172 fStoredState = NULL; 173 } 174 } 175 176 int32 level = 0; 177 settings.FindInt32("level", &level); 178 179 // Create GUI 180 181 BMenuBar* menuBar = new BMenuBar("menu"); 182 fSudokuView = new SudokuView("sudoku view", settings); 183 184 BLayoutBuilder::Group<>(this, B_VERTICAL, 0) 185 .Add(menuBar) 186 .Add(fSudokuView); 187 188 // Build menu 189 190 // "File" menu 191 BMenu* menu = new BMenu(B_TRANSLATE("File")); 192 fNewMenu = new BMenu(B_TRANSLATE("New")); 193 menu->AddItem(new BMenuItem(fNewMenu, new BMessage(kMsgGenerateSudoku))); 194 fNewMenu->Superitem()->SetShortcut('N', B_COMMAND_KEY); 195 196 BMessage* message = new BMessage(kMsgGenerateSudoku); 197 message->AddInt32("level", kEasyLevel); 198 fNewMenu->AddItem(new BMenuItem(B_TRANSLATE("Easy"), message)); 199 message = new BMessage(kMsgGenerateSudoku); 200 message->AddInt32("level", kAdvancedLevel); 201 fNewMenu->AddItem(new BMenuItem(B_TRANSLATE("Advanced"), message)); 202 message = new BMessage(kMsgGenerateSudoku); 203 message->AddInt32("level", kHardLevel); 204 fNewMenu->AddItem(new BMenuItem(B_TRANSLATE("Hard"), message)); 205 206 fNewMenu->AddSeparatorItem(); 207 fNewMenu->AddItem(new BMenuItem(B_TRANSLATE("Blank"), 208 new BMessage(kMsgNewBlank))); 209 210 menu->AddItem(new BMenuItem(B_TRANSLATE("Start again"), 211 new BMessage(kMsgStartAgain))); 212 menu->AddSeparatorItem(); 213 BMenu* recentsMenu = BRecentFilesList::NewFileListMenu( 214 B_TRANSLATE("Open file" B_UTF8_ELLIPSIS), NULL, NULL, this, 10, false, 215 NULL, kSignature); 216 BMenuItem *item; 217 menu->AddItem(item = new BMenuItem(recentsMenu, 218 new BMessage(kMsgOpenFilePanel))); 219 item->SetShortcut('O', B_COMMAND_KEY); 220 221 menu->AddSeparatorItem(); 222 223 BMenu* subMenu = new BMenu(B_TRANSLATE("Export as" B_UTF8_ELLIPSIS)); 224 message = new BMessage(kMsgExportAs); 225 message->AddInt32("as", kExportAsText); 226 subMenu->AddItem(new BMenuItem(B_TRANSLATE("Text"), message)); 227 message= new BMessage(kMsgExportAs); 228 message->AddInt32("as", kExportAsHTML); 229 subMenu->AddItem(new BMenuItem(B_TRANSLATE("HTML"), message)); 230 menu->AddItem(subMenu); 231 232 menu->AddItem(item = new BMenuItem(B_TRANSLATE("Copy"), 233 new BMessage(B_COPY), 'C')); 234 235 menu->AddSeparatorItem(); 236 237 menu->AddItem(new BMenuItem(B_TRANSLATE("Quit"), 238 new BMessage(B_QUIT_REQUESTED), 'Q')); 239 menu->SetTargetForItems(this); 240 item->SetTarget(be_app); 241 menuBar->AddItem(menu); 242 243 // "View" menu 244 menu = new BMenu(B_TRANSLATE("View")); 245 menu->AddItem(item = new BMenuItem(B_TRANSLATE("Mark invalid values"), 246 new BMessage(kMsgMarkInvalid))); 247 if ((fSudokuView->HintFlags() & kMarkInvalid) != 0) 248 item->SetMarked(true); 249 menu->AddItem(item = new BMenuItem(B_TRANSLATE("Mark valid hints"), 250 new BMessage(kMsgMarkValidHints))); 251 if ((fSudokuView->HintFlags() & kMarkValidHints) != 0) 252 item->SetMarked(true); 253 menu->SetTargetForItems(this); 254 menuBar->AddItem(menu); 255 256 // "Help" menu 257 menu = new BMenu(B_TRANSLATE("Help")); 258 menu->AddItem(fUndoItem = new BMenuItem(B_TRANSLATE("Undo"), 259 new BMessage(B_UNDO), 'Z')); 260 fUndoItem->SetEnabled(false); 261 menu->AddItem(fRedoItem = new BMenuItem(B_TRANSLATE("Redo"), 262 new BMessage(B_REDO), 'Z', B_SHIFT_KEY)); 263 fRedoItem->SetEnabled(false); 264 menu->AddSeparatorItem(); 265 266 menu->AddItem(new BMenuItem(B_TRANSLATE("Snapshot current"), 267 new BMessage(kMsgStoreState))); 268 menu->AddItem(fRestoreStateItem = new BMenuItem( 269 B_TRANSLATE("Restore snapshot"), new BMessage(kMsgRestoreState))); 270 fRestoreStateItem->SetEnabled(fStoredState != NULL); 271 menu->AddSeparatorItem(); 272 273 menu->AddItem(new BMenuItem(B_TRANSLATE("Set all hints"), 274 new BMessage(kMsgSetAllHints))); 275 menu->AddSeparatorItem(); 276 277 menu->AddItem(new BMenuItem(B_TRANSLATE("Solve"), 278 new BMessage(kMsgSolveSudoku))); 279 menu->AddItem(new BMenuItem(B_TRANSLATE("Solve single field"), 280 new BMessage(kMsgSolveSingle))); 281 menu->SetTargetForItems(fSudokuView); 282 menuBar->AddItem(menu); 283 284 fOpenPanel = new BFilePanel(B_OPEN_PANEL); 285 fOpenPanel->SetTarget(this); 286 fSavePanel = new BFilePanel(B_SAVE_PANEL); 287 fSavePanel->SetTarget(this); 288 289 _SetLevel(level); 290 291 fSudokuView->StartWatching(this, kUndoRedoChanged); 292 // we like to know whenever the undo/redo state changes 293 294 fProgressWindow = new ProgressWindow(this, 295 new BMessage(kMsgAbortSudokuGenerator)); 296 297 if (fSudokuView->Field()->IsEmpty()) 298 PostMessage(kMsgGenerateSudoku); 299 } 300 301 302 SudokuWindow::~SudokuWindow() 303 { 304 delete fOpenPanel; 305 delete fSavePanel; 306 delete fGenerator; 307 308 if (fProgressWindow->Lock()) 309 fProgressWindow->Quit(); 310 } 311 312 313 status_t 314 SudokuWindow::_OpenSettings(BFile& file, uint32 mode) 315 { 316 BPath path; 317 if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK) 318 return B_ERROR; 319 320 path.Append("Sudoku settings"); 321 322 return file.SetTo(path.Path(), mode); 323 } 324 325 326 status_t 327 SudokuWindow::_LoadSettings(BMessage& settings) 328 { 329 BFile file; 330 status_t status = _OpenSettings(file, B_READ_ONLY); 331 if (status != B_OK) 332 return status; 333 334 return settings.Unflatten(&file); 335 } 336 337 338 status_t 339 SudokuWindow::_SaveSettings() 340 { 341 BFile file; 342 status_t status = _OpenSettings(file, B_WRITE_ONLY | B_CREATE_FILE 343 | B_ERASE_FILE); 344 if (status != B_OK) 345 return status; 346 347 BMessage settings('sudo'); 348 status = settings.AddRect("window frame", Frame()); 349 if (status == B_OK) 350 status = fSudokuView->SaveState(settings); 351 if (status == B_OK && fStoredState != NULL) 352 status = settings.AddMessage("stored state", fStoredState); 353 if (status == B_OK) 354 status = settings.AddInt32("level", _Level()); 355 if (status == B_OK) 356 status = settings.Flatten(&file); 357 358 return status; 359 } 360 361 362 void 363 SudokuWindow::_ResetStoredState() 364 { 365 delete fStoredState; 366 fStoredState = NULL; 367 fRestoreStateItem->SetEnabled(false); 368 } 369 370 371 void 372 SudokuWindow::_MessageDropped(BMessage* message) 373 { 374 status_t status = B_MESSAGE_NOT_UNDERSTOOD; 375 bool hasRef = false; 376 377 entry_ref ref; 378 if (message->FindRef("refs", &ref) != B_OK) { 379 const void* data; 380 ssize_t size; 381 if (message->FindData("text/plain", B_MIME_TYPE, &data, 382 &size) == B_OK) { 383 status = fSudokuView->SetTo((const char*)data); 384 } else 385 return; 386 } else { 387 status = fSudokuView->SetTo(ref); 388 if (status == B_OK) 389 be_roster->AddToRecentDocuments(&ref, kSignature); 390 391 BEntry entry(&ref); 392 entry_ref parent; 393 if (entry.GetParent(&entry) == B_OK 394 && entry.GetRef(&parent) == B_OK) 395 fSavePanel->SetPanelDirectory(&parent); 396 397 hasRef = true; 398 } 399 400 if (status < B_OK) { 401 char buffer[1024]; 402 if (hasRef) { 403 snprintf(buffer, sizeof(buffer), 404 B_TRANSLATE("Could not open \"%s\":\n%s\n"), ref.name, 405 strerror(status)); 406 } else { 407 snprintf(buffer, sizeof(buffer), 408 B_TRANSLATE("Could not set Sudoku:\n%s\n"), 409 strerror(status)); 410 } 411 412 BAlert* alert = new BAlert(B_TRANSLATE("Sudoku request"), 413 buffer, B_TRANSLATE("OK"), NULL, NULL, 414 B_WIDTH_AS_USUAL, B_STOP_ALERT); 415 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); 416 alert->Go(); 417 } 418 } 419 420 421 void 422 SudokuWindow::_Generate(int32 level) 423 { 424 if (fGenerator != NULL) 425 delete fGenerator; 426 427 fSudokuView->SetEditable(false); 428 fProgressWindow->Start(this); 429 _ResetStoredState(); 430 431 fGenerator = new GenerateSudoku(*fSudokuView->Field(), level, 432 fProgressWindow, this); 433 } 434 435 436 void 437 SudokuWindow::MessageReceived(BMessage* message) 438 { 439 if (message->WasDropped()) { 440 _MessageDropped(message); 441 return; 442 } 443 444 switch (message->what) { 445 case kMsgOpenFilePanel: 446 fOpenPanel->Show(); 447 break; 448 449 case B_REFS_RECEIVED: 450 case B_SIMPLE_DATA: 451 _MessageDropped(message); 452 break; 453 454 case kMsgGenerateSudoku: 455 { 456 int32 level; 457 if (message->FindInt32("level", &level) != B_OK) 458 level = _Level(); 459 460 _SetLevel(level); 461 _Generate(level); 462 break; 463 } 464 case kMsgAbortSudokuGenerator: 465 if (fGenerator != NULL) 466 fGenerator->Abort(); 467 break; 468 case kMsgSudokuGenerated: 469 { 470 BMessage archive; 471 if (message->FindMessage("field", &archive) == B_OK) { 472 SudokuField* field = new SudokuField(&archive); 473 fSudokuView->SetTo(field); 474 } 475 fSudokuView->SetEditable(true); 476 fProgressWindow->Stop(); 477 478 delete fGenerator; 479 fGenerator = NULL; 480 break; 481 } 482 483 case kMsgExportAs: 484 { 485 if (message->FindInt32("as", (int32 *)&fExportFormat) < B_OK) 486 fExportFormat = kExportAsText; 487 fSavePanel->Show(); 488 break; 489 } 490 491 case B_COPY: 492 fSudokuView->CopyToClipboard(); 493 break; 494 495 case B_SAVE_REQUESTED: 496 { 497 entry_ref directoryRef; 498 const char* name; 499 if (message->FindRef("directory", &directoryRef) != B_OK 500 || message->FindString("name", &name) != B_OK) 501 break; 502 503 BDirectory directory(&directoryRef); 504 BEntry entry(&directory, name); 505 506 entry_ref ref; 507 if (entry.GetRef(&ref) == B_OK) 508 fSudokuView->SaveTo(ref, fExportFormat); 509 break; 510 } 511 512 case kMsgNewBlank: 513 _ResetStoredState(); 514 fSudokuView->ClearAll(); 515 break; 516 517 case kMsgStartAgain: 518 fSudokuView->ClearChanged(); 519 break; 520 521 case kMsgMarkInvalid: 522 case kMsgMarkValidHints: 523 { 524 BMenuItem* item; 525 if (message->FindPointer("source", (void**)&item) != B_OK) 526 return; 527 528 uint32 flag = message->what == kMsgMarkInvalid 529 ? kMarkInvalid : kMarkValidHints; 530 531 item->SetMarked(!item->IsMarked()); 532 if (item->IsMarked()) 533 fSudokuView->SetHintFlags(fSudokuView->HintFlags() | flag); 534 else 535 fSudokuView->SetHintFlags(fSudokuView->HintFlags() & ~flag); 536 break; 537 } 538 539 case kMsgStoreState: 540 delete fStoredState; 541 fStoredState = new BMessage; 542 fSudokuView->Field()->Archive(fStoredState, true); 543 fRestoreStateItem->SetEnabled(true); 544 break; 545 546 case kMsgRestoreState: 547 { 548 if (fStoredState == NULL) 549 break; 550 551 SudokuField* field = new SudokuField(fStoredState); 552 fSudokuView->SetTo(field); 553 break; 554 } 555 556 case kMsgSudokuSolved: 557 { 558 BAlert* alert = new BAlert(B_TRANSLATE("Sudoku request"), 559 B_TRANSLATE("Sudoku solved - congratulations!\n"), 560 B_TRANSLATE("OK"), NULL, NULL, 561 B_WIDTH_AS_USUAL, B_IDEA_ALERT); 562 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); 563 alert->Go(); 564 break; 565 } 566 567 case B_OBSERVER_NOTICE_CHANGE: 568 { 569 int32 what; 570 if (message->FindInt32(B_OBSERVE_WHAT_CHANGE, &what) != B_OK) 571 break; 572 573 if (what == kUndoRedoChanged) { 574 fUndoItem->SetEnabled(fSudokuView->CanUndo()); 575 fRedoItem->SetEnabled(fSudokuView->CanRedo()); 576 } 577 break; 578 } 579 580 default: 581 BWindow::MessageReceived(message); 582 break; 583 } 584 } 585 586 587 bool 588 SudokuWindow::QuitRequested() 589 { 590 _SaveSettings(); 591 be_app->PostMessage(B_QUIT_REQUESTED); 592 return true; 593 } 594 595 596 int32 597 SudokuWindow::_Level() const 598 { 599 BMenuItem* item = fNewMenu->FindMarked(); 600 if (item == NULL) 601 return 0; 602 603 BMessage* message = item->Message(); 604 if (message == NULL) 605 return 0; 606 607 return message->FindInt32("level"); 608 } 609 610 611 void 612 SudokuWindow::_SetLevel(int32 level) 613 { 614 for (int32 i = 0; i < fNewMenu->CountItems(); i++) { 615 BMenuItem* item = fNewMenu->ItemAt(i); 616 617 BMessage* message = item->Message(); 618 if (message != NULL && message->HasInt32("level") 619 && message->FindInt32("level") == level) 620 item->SetMarked(true); 621 else 622 item->SetMarked(false); 623 } 624 } 625