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
37const uint32 kMsgOpenFilePanel = 'opfp';
38const uint32 kMsgGenerateSudoku = 'gnsu';
39const uint32 kMsgAbortSudokuGenerator = 'asgn';
40const uint32 kMsgSudokuGenerated = 'sugn';
41const uint32 kMsgMarkInvalid = 'minv';
42const uint32 kMsgMarkValidHints = 'mvht';
43const uint32 kMsgStoreState = 'stst';
44const uint32 kMsgRestoreState = 'rest';
45const uint32 kMsgNewBlank = 'new ';
46const uint32 kMsgStartAgain = 'stag';
47const uint32 kMsgExportAs = 'expt';
48
49
50enum sudoku_level {
51	kEasyLevel		= 0,
52	kAdvancedLevel	= 2,
53	kHardLevel		= 4,
54};
55
56
57class GenerateSudoku {
58public:
59								GenerateSudoku(SudokuField& field, int32 level,
60									BMessenger progress, BMessenger target);
61								~GenerateSudoku();
62
63			void				Abort();
64
65private:
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
78GenerateSudoku::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
96GenerateSudoku::~GenerateSudoku()
97{
98	Abort();
99}
100
101
102void
103GenerateSudoku::Abort()
104{
105	fQuit = true;
106
107	status_t status;
108	wait_for_thread(fThread, &status);
109}
110
111
112void
113GenerateSudoku::_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
133GenerateSudoku::_GenerateThread(void* _self)
134{
135	GenerateSudoku* self = (GenerateSudoku*)_self;
136	self->_Generate();
137	return B_OK;
138}
139
140
141//	#pragma mark -
142
143
144SudokuWindow::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
302SudokuWindow::~SudokuWindow()
303{
304	delete fOpenPanel;
305	delete fSavePanel;
306	delete fGenerator;
307
308	if (fProgressWindow->Lock())
309		fProgressWindow->Quit();
310}
311
312
313status_t
314SudokuWindow::_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
326status_t
327SudokuWindow::_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
338status_t
339SudokuWindow::_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
362void
363SudokuWindow::_ResetStoredState()
364{
365	delete fStoredState;
366	fStoredState = NULL;
367	fRestoreStateItem->SetEnabled(false);
368}
369
370
371void
372SudokuWindow::_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
421void
422SudokuWindow::_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
436void
437SudokuWindow::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
587bool
588SudokuWindow::QuitRequested()
589{
590	_SaveSettings();
591	be_app->PostMessage(B_QUIT_REQUESTED);
592	return true;
593}
594
595
596int32
597SudokuWindow::_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
611void
612SudokuWindow::_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